7 KiB
7 KiB
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:
- Mark invoice as paid:
- UPDATE
ogp_billing_invoicesSET status='paid', paid_date=NOW()
- UPDATE
- Create order record:
- Calculate end_date (qty * invoice_duration)
- INSERT into
ogp_billing_orderswith status='paid' - Get new order_id from INSERT
- Link invoice to order:
- UPDATE
ogp_billing_invoicesSET order_id=? WHERE invoice_id=?
- UPDATE
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:
- PayPal sends capture webhook
- System marks invoice as paid (same as Free flow)
- System creates order record (same as Free flow)
- 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)
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)
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)
- Add to cart → Create ORDER (status='in-cart')
- View cart → Show orders WHERE status='in-cart'
- Pay → UPDATE order status='in-cart' → 'paid'
- Provision → UPDATE order status='paid' → 'installed'
NEW (Invoice-First)
- Add to cart → Create INVOICE (status='due', order_id=0)
- View cart → Show invoices WHERE status='due'
- Pay → Mark invoice paid + CREATE ORDER (status='paid') + Link invoice to order
- Provision → UPDATE order status='paid' → 'installed'
Benefits
- Clean Separation: Invoices = payment requests, Orders = actual services
- Better Audit Trail: Invoice IDs never change, order IDs created only after payment
- Renewal Support: Can create multiple invoices for same order (renewals)
- Cart Simplicity: Cart only shows unpaid invoices (single source of truth)
- Payment History: All payments have invoice records, even free ones
Migration Notes
Existing orders with status='in-cart' need to be migrated:
-- 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';