diff --git a/.agent/plans/affiliate-module.md b/.agent/plans/affiliate-module.md new file mode 100644 index 0000000..ca7e9c1 --- /dev/null +++ b/.agent/plans/affiliate-module.md @@ -0,0 +1,241 @@ +# Affiliate Module Plan + +## Overview +Referral tracking with hybrid customer/affiliate roles, integrated as a core plugin module. + +--- + +## Module Architecture + +### Core Features +- **Affiliate registration**: Customers can become affiliates +- **Approval workflow**: Manual or auto-approval of affiliates +- **Unique referral links/codes**: Each affiliate gets unique tracking +- **Commission tracking**: Track referrals and calculate earnings +- **Tiered commission rates**: Different rates per product/category/affiliate level +- **Payout management**: Track and process affiliate payouts +- **Affiliate dashboard**: Self-service stats and link generator + +### Hybrid Roles +- A customer can also be an affiliate +- No separate user type; affiliate data linked to existing user +- Affiliates can still make purchases (self-referral rules configurable) + +--- + +## Database Schema + +### Table: `woonoow_affiliates` +```sql +CREATE TABLE woonoow_affiliates ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL UNIQUE, + status ENUM('pending', 'active', 'rejected', 'suspended') DEFAULT 'pending', + referral_code VARCHAR(50) NOT NULL UNIQUE, + commission_rate DECIMAL(5,2) DEFAULT NULL, -- Override global rate + tier_id BIGINT UNSIGNED DEFAULT NULL, + payment_email VARCHAR(255) DEFAULT NULL, + payment_method VARCHAR(50) DEFAULT NULL, + total_earnings DECIMAL(15,2) DEFAULT 0, + total_unpaid DECIMAL(15,2) DEFAULT 0, + total_paid DECIMAL(15,2) DEFAULT 0, + referral_count INT UNSIGNED DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_status (status), + INDEX idx_referral_code (referral_code) +); +``` + +### Table: `woonoow_referrals` +```sql +CREATE TABLE woonoow_referrals ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + affiliate_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED DEFAULT NULL, + subtotal DECIMAL(15,2) NOT NULL, + commission_rate DECIMAL(5,2) NOT NULL, + commission_amount DECIMAL(15,2) NOT NULL, + status ENUM('pending', 'approved', 'rejected', 'paid') DEFAULT 'pending', + referral_type ENUM('link', 'code', 'coupon') DEFAULT 'link', + ip_address VARCHAR(45) DEFAULT NULL, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + approved_at DATETIME DEFAULT NULL, + paid_at DATETIME DEFAULT NULL, + INDEX idx_affiliate (affiliate_id), + INDEX idx_order (order_id), + INDEX idx_status (status) +); +``` + +### Table: `woonoow_payouts` +```sql +CREATE TABLE woonoow_payouts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + affiliate_id BIGINT UNSIGNED NOT NULL, + amount DECIMAL(15,2) NOT NULL, + method VARCHAR(50) DEFAULT NULL, + reference VARCHAR(255) DEFAULT NULL, -- Bank ref, PayPal transaction, etc. + status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending', + notes TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + completed_at DATETIME DEFAULT NULL, + INDEX idx_affiliate (affiliate_id), + INDEX idx_status (status) +); +``` + +### Table: `woonoow_affiliate_tiers` (Optional) +```sql +CREATE TABLE woonoow_affiliate_tiers ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + commission_rate DECIMAL(5,2) NOT NULL, + min_referrals INT UNSIGNED DEFAULT 0, -- Auto-promote at X referrals + min_earnings DECIMAL(15,2) DEFAULT 0, -- Auto-promote at X earnings + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +--- + +## Backend Structure + +``` +includes/Modules/Affiliate/ +├── AffiliateModule.php # Bootstrap, hooks, tracking +├── AffiliateManager.php # Core logic +├── ReferralTracker.php # Track referral cookies/links +└── AffiliateSettings.php # Settings schema + +includes/Api/AffiliatesController.php # REST endpoints +``` + +### AffiliateManager Methods +```php +- register($user_id) # Register user as affiliate +- approve($affiliate_id) # Approve pending affiliate +- reject($affiliate_id, $reason) # Reject application +- suspend($affiliate_id) # Suspend affiliate +- track_referral($order, $affiliate) # Create referral record +- calculate_commission($order, $affiliate) # Calculate earnings +- approve_referral($referral_id) # Approve pending referral +- create_payout($affiliate_id, $amount) # Create payout request +- process_payout($payout_id) # Mark payout complete +- get_affiliate_stats($affiliate_id) # Dashboard stats +``` + +### Referral Tracking +1. Affiliate shares link: `yourstore.com/?ref=CODE` +2. ReferralTracker sets cookie: `wnw_ref=CODE` (30 days default) +3. On checkout, check cookie and link to affiliate +4. On order completion, create referral record + +--- + +## REST API Endpoints + +### Admin Endpoints +``` +GET /affiliates # List affiliates +GET /affiliates/{id} # Affiliate details +PUT /affiliates/{id} # Update affiliate +POST /affiliates/{id}/approve # Approve affiliate +POST /affiliates/{id}/reject # Reject affiliate +GET /affiliates/referrals # All referrals +GET /affiliates/payouts # All payouts +POST /affiliates/payouts/{id}/complete # Complete payout +``` + +### Customer/Affiliate Endpoints +``` +GET /my-affiliate # Check if affiliate +POST /my-affiliate/register # Register as affiliate +GET /my-affiliate/stats # Dashboard stats +GET /my-affiliate/referrals # My referrals +GET /my-affiliate/payouts # My payouts +POST /my-affiliate/payout-request # Request payout +GET /my-affiliate/links # Generate referral links +``` + +--- + +## Admin SPA + +### Affiliates List (`/marketing/affiliates`) +- Table: Name, Email, Status, Referrals, Earnings, Actions +- Filters: Status, Date range +- Actions: Approve, Reject, View + +### Affiliate Detail (`/marketing/affiliates/:id`) +- Affiliate info card +- Stats summary +- Referrals list +- Payouts history +- Action buttons + +### Referrals List (`/marketing/affiliates/referrals`) +- All referrals across affiliates +- Filters: Status, Affiliate, Date + +### Payouts (`/marketing/affiliates/payouts`) +- Payout requests +- Process payouts +- Payment history + +--- + +## Customer SPA + +### Become an Affiliate (`/my-account/affiliate`) +- Registration form (if not affiliate) +- Dashboard (if affiliate) + +### Affiliate Dashboard +- Stats: Total Referrals, Pending, Approved, Earnings +- Referral link generator +- Recent referrals +- Payout request button + +### My Referrals +- List of referrals with status +- Commission amount + +### My Payouts +- Payout history +- Pending amount +- Request payout form + +--- + +## Settings Schema + +```php +return [ + 'enabled' => true, + 'registration_type' => 'open', // open, approval, invite + 'auto_approve' => false, + 'default_commission_rate' => 10, // 10% + 'commission_type' => 'percentage', // percentage, flat + 'cookie_duration' => 30, // days + 'min_payout_amount' => 50, + 'payout_methods' => ['bank_transfer', 'paypal'], + 'allow_self_referral' => false, + 'referral_approval' => 'auto', // auto, manual + 'approval_delay_days' => 14, // Wait X days before auto-approve +]; +``` + +--- + +## Implementation Priority +1. Database tables and AffiliateManager +2. ReferralTracker (cookie-based tracking) +3. Order hook to create referrals +4. Admin SPA affiliates management +5. Customer SPA affiliate dashboard +6. Payout management +7. Tier system (optional) diff --git a/.agent/plans/shipping-label.md b/.agent/plans/shipping-label.md new file mode 100644 index 0000000..9b47754 --- /dev/null +++ b/.agent/plans/shipping-label.md @@ -0,0 +1,84 @@ +# Shipping Label Plan + +## Overview +Standardized waybill data structure for shipping label generation. + +## Problem +- Different shipping carrier addons (JNE, JNT, SiCepat, etc.) store data differently +- No standard structure for label generation +- Label button needs waybill data to function + +## Proposed Solution + +### 1. Standardized Meta Key +Order meta: `_shipping_waybill` + +### 2. Data Structure +```json +{ + "tracking_number": "JNE123456789", + "carrier": "jne", + "carrier_name": "JNE Express", + "service": "REG", + "estimated_days": 3, + "sender": { + "name": "Store Name", + "address": "Full address line 1", + "city": "Jakarta", + "postcode": "12345", + "phone": "08123456789" + }, + "recipient": { + "name": "Customer Name", + "address": "Full address line 1", + "city": "Bandung", + "postcode": "40123", + "phone": "08987654321" + }, + "package": { + "weight": "1.5", + "weight_unit": "kg", + "dimensions": "20x15x10", + "dimensions_unit": "cm" + }, + "label_url": null, + "barcode": "JNE123456789", + "barcode_type": "128", + "created_at": "2026-01-05T12:00:00+07:00" +} +``` + +### 3. Addon Integration Contract + +Shipping addons MUST: +1. Call `update_post_meta($order_id, '_shipping_waybill', $waybill_data)` +2. Use the standard structure above +3. Set `label_url` if carrier provides downloadable PDF +4. Set `barcode` for local label generation + +### 4. Label Button Behavior +1. Check if `_shipping_waybill` meta exists on order +2. If `label_url` → open carrier's PDF +3. Otherwise → generate printable label from meta data + +### 5. UI Behavior +- Label button hidden if order is virtual-only +- Label button shows "Generate Label" if no waybill yet +- Label button shows "Print Label" if waybill exists + +## API Endpoint (Future) +``` +POST /woonoow/v1/orders/{id}/generate-waybill +- Calls shipping carrier API +- Stores waybill in standardized format +- Returns waybill data + +GET /woonoow/v1/orders/{id}/waybill +- Returns current waybill data +``` + +## Implementation Priority +1. Define standard structure (this document) +2. Implement Label UI conditional logic +3. Create waybill API endpoint +4. Document for addon developers diff --git a/.agent/plans/subscription-module.md b/.agent/plans/subscription-module.md new file mode 100644 index 0000000..29a4d39 --- /dev/null +++ b/.agent/plans/subscription-module.md @@ -0,0 +1,191 @@ +# Subscription Module Plan + +## Overview +Recurring product subscriptions with flexible billing, integrated as a core plugin module (like Newsletter/Wishlist/Licensing). + +--- + +## Module Architecture + +### Core Features +- **Recurring billing**: Weekly, monthly, yearly, custom intervals +- **Free trials**: X days free before billing starts +- **Sign-up fees**: One-time fee on first subscription +- **Automatic renewals**: Process payment on renewal date +- **Manual renewal**: Allow customers to renew manually +- **Proration**: Calculate prorated amounts on plan changes +- **Pause/Resume**: Allow customers to pause subscriptions + +### Product Integration +- Checkbox under "Additional Options": **Enable subscription for this product** +- When enabled, show subscription settings: + - Billing period (weekly/monthly/yearly/custom) + - Billing interval (every X periods) + - Free trial days + - Sign-up fee + - Subscription length (0 = unlimited) +- Variable products: Variation-level subscription settings (different durations/prices per variation) + +### Integration with Licensing +- Licenses can be bound to subscriptions +- When subscription is active → license is valid +- When subscription expires/cancelled → license is revoked +- Auto-renewal keeps license active + +--- + +## Database Schema + +### Table: `woonoow_subscriptions` +```sql +CREATE TABLE woonoow_subscriptions ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL, + product_id BIGINT UNSIGNED NOT NULL, + variation_id BIGINT UNSIGNED DEFAULT NULL, + status ENUM('pending', 'active', 'on-hold', 'cancelled', 'expired', 'pending-cancel') DEFAULT 'pending', + billing_period ENUM('day', 'week', 'month', 'year') NOT NULL, + billing_interval INT UNSIGNED DEFAULT 1, + start_date DATETIME NOT NULL, + trial_end_date DATETIME DEFAULT NULL, + next_payment_date DATETIME DEFAULT NULL, + end_date DATETIME DEFAULT NULL, + last_payment_date DATETIME DEFAULT NULL, + payment_method VARCHAR(100) DEFAULT NULL, + payment_meta LONGTEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_order_id (order_id), + INDEX idx_status (status), + INDEX idx_next_payment (next_payment_date) +); +``` + +### Table: `woonoow_subscription_orders` +Links subscription to renewal orders: +```sql +CREATE TABLE woonoow_subscription_orders ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + subscription_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL, + order_type ENUM('parent', 'renewal', 'switch', 'resubscribe') DEFAULT 'renewal', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_subscription (subscription_id), + INDEX idx_order (order_id) +); +``` + +--- + +## Backend Structure + +``` +includes/Modules/Subscription/ +├── SubscriptionModule.php # Bootstrap, hooks, product meta +├── SubscriptionManager.php # Core logic +├── SubscriptionScheduler.php # Cron jobs for renewals +└── SubscriptionSettings.php # Settings schema + +includes/Api/SubscriptionsController.php # REST endpoints +``` + +### SubscriptionManager Methods +```php +- create($order, $product, $user) # Create subscription from order +- renew($subscription_id) # Process renewal +- cancel($subscription_id, $reason) # Cancel subscription +- pause($subscription_id) # Pause subscription +- resume($subscription_id) # Resume paused subscription +- switch($subscription_id, $new_product) # Switch plan +- get_next_payment_date($subscription) # Calculate next date +- process_renewal_payment($subscription) # Charge payment +``` + +### Cron Jobs +- `woonoow_process_subscription_renewals`: Run daily, process due renewals +- `woonoow_check_expired_subscriptions`: Mark expired subscriptions + +--- + +## REST API Endpoints + +### Admin Endpoints +``` +GET /subscriptions # List all subscriptions +GET /subscriptions/{id} # Get subscription details +PUT /subscriptions/{id} # Update subscription +POST /subscriptions/{id}/cancel # Cancel subscription +POST /subscriptions/{id}/renew # Force renewal +``` + +### Customer Endpoints +``` +GET /my-subscriptions # Customer's subscriptions +GET /my-subscriptions/{id} # Subscription detail +POST /my-subscriptions/{id}/cancel # Request cancellation +POST /my-subscriptions/{id}/pause # Pause subscription +POST /my-subscriptions/{id}/resume # Resume subscription +``` + +--- + +## Admin SPA + +### Subscriptions List (`/subscriptions`) +- Table: ID, Customer, Product, Status, Next Payment, Actions +- Filters: Status, Product, Date range +- Actions: View, Cancel, Renew + +### Subscription Detail (`/subscriptions/:id`) +- Subscription info card +- Related orders list +- Payment history +- Action buttons: Cancel, Pause, Renew + +--- + +## Customer SPA + +### My Subscriptions (`/my-account/subscriptions`) +- List of active/past subscriptions +- Status badges +- Next payment info +- Actions: Cancel, Pause, View + +### Subscription Detail +- Product info +- Billing schedule +- Payment history +- Management actions + +--- + +## Settings Schema + +```php +return [ + 'default_status' => 'active', + 'button_text_subscribe' => 'Subscribe Now', + 'button_text_renew' => 'Renew Subscription', + 'allow_customer_cancel' => true, + 'allow_customer_pause' => true, + 'max_pause_count' => 3, + 'renewal_retry_days' => [1, 3, 5], // Retry failed payments + 'expire_after_failed_attempts' => 3, + 'send_renewal_reminder' => true, + 'reminder_days_before' => 3, +]; +``` + +--- + +## Implementation Priority +1. Database tables and SubscriptionManager +2. Product meta fields for subscription settings +3. Order hook to create subscription +4. Renewal cron job +5. Admin SPA list/detail pages +6. Customer SPA pages +7. Integration with Licensing module diff --git a/admin-spa/src/index.css b/admin-spa/src/index.css index 04d04db..ad94136 100644 --- a/admin-spa/src/index.css +++ b/admin-spa/src/index.css @@ -34,6 +34,7 @@ --chart-5: 27 87% 67%; --radius: 0.5rem; } + .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; @@ -63,10 +64,23 @@ } @layer base { - * { @apply border-border; } - body { @apply bg-background text-foreground; } - h1, h2, h3, h4, h5, h6 { @apply text-foreground; } - + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + @apply text-foreground; + } + /* Override WordPress common.css focus/active styles */ a:focus, a:active { @@ -126,11 +140,14 @@ /* Page defaults for print */ @page { - size: auto; /* let the browser choose */ - margin: 12mm; /* comfortable default */ + size: auto; + /* let the browser choose */ + margin: 12mm; + /* comfortable default */ } @media print { + /* Hide WordPress admin chrome */ #adminmenuback, #adminmenuwrap, @@ -139,44 +156,124 @@ #wpfooter, #screen-meta, .notice, - .update-nag { display: none !important; } + .update-nag { + display: none !important; + } /* Reset layout to full-bleed for our app */ - html, body, #wpwrap, #wpcontent { background: #fff !important; margin: 0 !important; padding: 0 !important; } - #woonoow-admin-app, #woonoow-admin-app > div { margin: 0 !important; padding: 0 !important; max-width: 100% !important; } + html, + body, + #wpwrap, + #wpcontent { + background: #fff !important; + margin: 0 !important; + padding: 0 !important; + } + + #woonoow-admin-app, + #woonoow-admin-app>div { + margin: 0 !important; + padding: 0 !important; + max-width: 100% !important; + } /* Hide elements flagged as no-print, reveal print-only */ - .no-print { display: none !important; } - .print-only { display: block !important; } + .no-print { + display: none !important; + } + + .print-only { + display: block !important; + } /* Improve table row density on paper */ - .print-tight tr > * { padding-top: 6px !important; padding-bottom: 6px !important; } + .print-tight tr>* { + padding-top: 6px !important; + padding-bottom: 6px !important; + } } /* By default, label-only content stays hidden unless in print or label mode */ -.print-only { display: none; } +.print-only { + display: none; +} /* Label mode toggled by router (?mode=label) */ -.woonoow-label-mode .print-only { display: block; } +.woonoow-label-mode .print-only { + display: block; +} + .woonoow-label-mode .no-print-label, .woonoow-label-mode .wp-header-end, -.woonoow-label-mode .wrap { display: none !important; } +.woonoow-label-mode .wrap { + display: none !important; +} /* Optional page presets (opt-in by adding the class to a wrapper before printing) */ -.print-a4 { } -.print-letter { } -.print-4x6 { } +.print-a4 {} + +.print-letter {} + +.print-4x6 {} + @media print { - .print-a4 { } - .print-letter { } + + /* A4 Invoice layout */ + @page { + size: A4; + margin: 0; + } + + .print-a4 { + width: 210mm !important; + min-height: 297mm !important; + padding: 20mm !important; + margin: 0 auto !important; + box-sizing: border-box !important; + background: white !important; + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + .print-a4 * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + } + + /* Ensure backgrounds print */ + .print-a4 .bg-gray-50 { + background-color: #f9fafb !important; + } + + .print-a4 .bg-gray-900 { + background-color: #111827 !important; + } + + .print-a4 .text-white { + color: white !important; + } + + .print-letter {} + /* Thermal label (4x6in) with minimal margins */ - .print-4x6 { width: 6in; } - .print-4x6 * { -webkit-print-color-adjust: exact; print-color-adjust: exact; } + .print-4x6 { + width: 6in; + } + + .print-4x6 * { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } } /* --- WooNooW: Popper menus & fullscreen fixes --- */ -[data-radix-popper-content-wrapper] { z-index: 2147483647 !important; } -body.woonoow-fullscreen .woonoow-app { overflow: visible; } +[data-radix-popper-content-wrapper] { + z-index: 2147483647 !important; +} + +body.woonoow-fullscreen .woonoow-app { + overflow: visible; +} /* --- WooCommerce Admin Notices --- */ .woocommerce-message, diff --git a/admin-spa/src/routes/Orders/Detail.tsx b/admin-spa/src/routes/Orders/Detail.tsx index c20ffef..276474f 100644 --- a/admin-spa/src/routes/Orders/Detail.tsx +++ b/admin-spa/src/routes/Orders/Detail.tsx @@ -205,9 +205,11 @@ export default function OrderShow() { - + {!isVirtualOnly && ( + + )} @@ -475,55 +477,116 @@ export default function OrderShow() { {order && (
{mode === 'invoice' && ( -
-
+
+ {/* Invoice Header */} +
-
Invoice
-
Order #{order.number} · {new Date((order.date_ts || 0) * 1000).toLocaleString()}
+
{__('INVOICE')}
+
#{order.number}
-
{siteTitle}
-
{window.location.origin}
+
{siteTitle}
+
{window.location.origin}
-
+ + {/* Invoice Meta */} +
-
{__('Bill To')}
-
+
{__('Invoice Date')}
+
{new Date((order.date_ts || 0) * 1000).toLocaleDateString()}
+ {order.payment_method && ( + <> +
{__('Payment Method')}
+
{order.payment_method}
+ + )}
-
- +
+
- + + {/* Billing & Shipping */} +
+
+
{__('Bill To')}
+
{order.billing?.name || '—'}
+ {order.billing?.email &&
{order.billing.email}
} + {order.billing?.phone &&
{order.billing.phone}
} +
+
+ {!isVirtualOnly && order.shipping?.name && ( +
+
{__('Ship To')}
+
{order.shipping?.name || '—'}
+
+
+ )} +
+ + {/* Items Table */} +
- - - - - + + + + + - {(order.items || []).map((it: any) => ( - - - - - + {(order.items || []).map((it: any, idx: number) => ( + + + + + ))}
ProductQtySubtotalTotal
{__('Product')}{__('Qty')}{__('Price')}{__('Total')}
{it.name}×{it.qty}
+
{it.name}
+ {it.sku &&
SKU: {it.sku}
} +
{it.qty}
-
-
-
Subtotal
-
Discount
-
Shipping
-
Tax
-
Total
+ + {/* Totals */} +
+
+
+ {__('Subtotal')} + +
+ {(order.totals?.discount || 0) > 0 && ( +
+ {__('Discount')} + - +
+ )} + {(order.totals?.shipping || 0) > 0 && ( +
+ {__('Shipping')} + +
+ )} + {(order.totals?.tax || 0) > 0 && ( +
+ {__('Tax')} + +
+ )} +
+ {__('Total')} + +
+ + {/* Footer */} +
+

{__('Thank you for your business!')}

+

{siteTitle} • {window.location.origin}

+
)} {mode === 'label' && (