190 lines
7 KiB
Markdown
190 lines
7 KiB
Markdown
# 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';
|
|
```
|