# Invoice-First Billing Flow ## Overview The billing system now follows an **invoice-first** workflow where invoices are created BEFORE orders. Orders are only created after successful payment. ## Workflow ### 1. Add to Cart (order.php → add_to_cart.php) **What happens:** - User clicks "Add to Cart" button on order page - System creates a **billing_invoices** record with: - `status` = 'due' - `order_id` = 0 (no order exists yet) - All server details (service_id, home_name, ip, max_players, passwords, etc.) - Customer details (name, email from ogp_users) - Pricing (amount, qty, invoice_duration) - `due_date` = now + 3 days **Database changes:** - INSERT into `ogp_billing_invoices` - NO changes to `ogp_billing_orders` (order doesn't exist yet) ### 2. Cart Display (cart.php) **What shows:** - Query: `SELECT * FROM ogp_billing_invoices WHERE status = 'due' AND user_id = ?` - Displays all **unpaid invoices** (status='due') - Shows invoice_id, home_name, ip, max_players, amount, qty - Free items show "Claim (Free)" button - Paid items show PayPal button **Actions available:** - Delete invoice (removes from cart, no order cleanup needed) - Pay invoice (via PayPal or Free button) ### 3. Payment (PayPal or Free) #### 3a. Free/Claim Flow (cart.php POST handler) **When:** User clicks "Claim (Free)" or admin clicks "Create (Free)" **What happens:** 1. Mark invoice as paid: - UPDATE `ogp_billing_invoices` SET status='paid', paid_date=NOW() 2. Create order record: - Calculate end_date (qty * invoice_duration) - INSERT into `ogp_billing_orders` with status='paid' - Get new order_id from INSERT 3. Link invoice to order: - UPDATE `ogp_billing_invoices` SET order_id=? WHERE invoice_id=? **Database changes:** - UPDATE `ogp_billing_invoices`: status='due' → 'paid', paid_date=NOW(), order_id=(new) - INSERT `ogp_billing_orders`: New record with status='paid', end_date calculated #### 3b. PayPal Flow (api/capture_order.php) **When:** User pays via PayPal **What should happen:** 1. PayPal sends capture webhook 2. System marks invoice as paid (same as Free flow) 3. System creates order record (same as Free flow) 4. System links invoice to order (same as Free flow) **Database changes:** (Same as Free flow above) ### 4. Server Provisioning (create_servers.php) **What happens:** - Cron job or manual trigger finds orders with status='paid' - Creates actual game server (home_id) - Updates order: status='paid' → 'installed', home_id=(assigned) **Database changes:** - UPDATE `ogp_billing_orders`: status='paid' → 'installed', home_id=(assigned) ## Status Values ### Invoice Status - **'due'** - Unpaid invoice (shows in cart) - **'paid'** - Paid invoice (payment confirmed) - **'cancelled'** - Deleted/cancelled invoice ### Order Status - **'paid'** - Payment confirmed, awaiting provisioning - **'installed'** - Server provisioned and running - **'suspended'** - Server stopped for non-payment - **'expired'** - Service ended ## Database Schema ### ogp_billing_invoices (INVOICE-FIRST) ```sql invoice_id INT AUTO_INCREMENT PRIMARY KEY order_id INT DEFAULT 0 -- Links to order AFTER payment (0 = not yet paid) user_id INT NOT NULL service_id INT NOT NULL -- Server package being purchased home_name VARCHAR(255) -- Server name ip INT -- IP assignment max_players INT -- Player count remote_control_password VARCHAR(255) -- Server RCON password ftp_password VARCHAR(255) -- FTP password customer_name VARCHAR(255) -- Billing name customer_email VARCHAR(255) -- Billing email amount FLOAT(15,2) -- Total price currency VARCHAR(3) DEFAULT 'USD' status VARCHAR(16) DEFAULT 'due' -- 'due', 'paid', 'cancelled' invoice_date DATETIME DEFAULT NOW() due_date DATETIME -- Payment deadline paid_date DATETIME -- When paid payment_txid VARCHAR(255) -- PayPal transaction ID payment_method VARCHAR(50) -- 'paypal', 'free', etc. description VARCHAR(500) -- Invoice description invoice_duration VARCHAR(16) DEFAULT 'month' -- 'month', 'year', 'day' qty INT DEFAULT 1 -- Quantity/duration multiplier ``` ### ogp_billing_orders (ORDER-AFTER-PAYMENT) ```sql order_id INT AUTO_INCREMENT PRIMARY KEY user_id INT NOT NULL service_id INT NOT NULL home_name VARCHAR(255) home_id VARCHAR(255) -- Panel game server ID (after provisioning) ip INT max_players INT qty INT invoice_duration VARCHAR(16) price FLOAT(15,2) remote_control_password VARCHAR(255) ftp_password VARCHAR(255) status VARCHAR(16) DEFAULT 'paid' -- 'paid', 'installed', 'suspended', 'expired' order_date DATETIME DEFAULT NOW() end_date DATETIME -- Subscription expiration payment_txid VARCHAR(255) paid_ts DATETIME ``` ## Key Differences from Old Flow ### OLD (Order-First) 1. Add to cart → Create ORDER (status='in-cart') 2. View cart → Show orders WHERE status='in-cart' 3. Pay → UPDATE order status='in-cart' → 'paid' 4. Provision → UPDATE order status='paid' → 'installed' ### NEW (Invoice-First) 1. Add to cart → Create INVOICE (status='due', order_id=0) 2. View cart → Show invoices WHERE status='due' 3. Pay → Mark invoice paid + CREATE ORDER (status='paid') + Link invoice to order 4. Provision → UPDATE order status='paid' → 'installed' ## Benefits 1. **Clean Separation:** Invoices = payment requests, Orders = actual services 2. **Better Audit Trail:** Invoice IDs never change, order IDs created only after payment 3. **Renewal Support:** Can create multiple invoices for same order (renewals) 4. **Cart Simplicity:** Cart only shows unpaid invoices (single source of truth) 5. **Payment History:** All payments have invoice records, even free ones ## Migration Notes **Existing orders with status='in-cart' need to be migrated:** ```sql -- Convert existing cart items to invoices INSERT INTO ogp_billing_invoices ( order_id, user_id, service_id, home_name, ip, max_players, remote_control_password, ftp_password, customer_name, customer_email, amount, status, invoice_duration, qty, description ) SELECT 0, -- No order exists yet o.user_id, o.service_id, o.home_name, o.ip, o.max_players, o.remote_control_password, o.ftp_password, CONCAT(u.users_fname, ' ', u.users_lname), u.users_email, o.price, 'due', -- Convert 'in-cart' to 'due' o.invoice_duration, o.qty, CONCAT('Migrated cart item: ', o.home_name) FROM ogp_billing_orders o LEFT JOIN ogp_users u ON o.user_id = u.user_id WHERE o.status = 'in-cart'; -- Delete old cart items (now converted to invoices) DELETE FROM ogp_billing_orders WHERE status = 'in-cart'; ```