242 lines
8.7 KiB
Markdown
242 lines
8.7 KiB
Markdown
# Billing Invoice/Order Flow - Fix Summary
|
|
|
|
## Problem Statement
|
|
|
|
The billing system had several critical issues:
|
|
|
|
1. **JSON Error**: "Failed to execute 'json' on 'Response': Unexpected end of JSON input" when returning from PayPal payment
|
|
2. **Cart not clearing**: Items remained in cart after payment (invoices stayed as status='due')
|
|
3. **No order creation**: Orders were not being created after successful payment
|
|
4. **Missing renewal flow**: Renewal invoices (linked to existing orders) were not handled
|
|
5. **Free button errors**: The free/claim button was also experiencing errors
|
|
|
|
## Invoice-First Flow (Intended Design)
|
|
|
|
The system uses an invoice-first architecture:
|
|
|
|
1. **Add to Cart**: Creates INVOICE with status='due', order_id=0 (no order yet)
|
|
2. **View Cart**: Shows all invoices WHERE status='due'
|
|
3. **Payment**:
|
|
- For NEW orders (order_id=0): Mark invoice paid + CREATE new order
|
|
- For RENEWALS (order_id>0): Mark invoice paid + EXTEND existing order's end_date
|
|
4. **Provisioning**: Separate step that provisions servers for paid orders
|
|
|
|
## Root Causes Identified
|
|
|
|
### 1. Missing Function
|
|
- `process_payment_record()` was called but never defined
|
|
- Referenced in webhook.php, cart.php (free button), but didn't exist
|
|
- This prevented any payment processing from completing
|
|
|
|
### 2. JSON Response Corruption
|
|
- `capture_order.php` had PHP errors/warnings during DB operations
|
|
- These were being output to the response, corrupting the JSON
|
|
- JavaScript couldn't parse the malformed JSON → "Unexpected end of JSON input"
|
|
|
|
### 3. Incomplete Payment Processing
|
|
- `capture_order.php` was supposed to:
|
|
- Mark invoices as paid (status: 'due' → 'paid')
|
|
- Create new orders OR extend existing orders
|
|
- Link invoices to orders
|
|
- But the logic was incomplete and had issues
|
|
|
|
### 4. Session Compatibility
|
|
- capture_order.php used `$_SESSION['user_id']`
|
|
- cart.php used `$_SESSION['website_user_id']`
|
|
- This mismatch meant user couldn't be identified for payment processing
|
|
|
|
### 5. Hardcoded Table Names
|
|
- capture_order.php used hardcoded "ogp_billing_invoices" and "ogp_billing_orders"
|
|
- Should use `$table_prefix . "billing_invoices"` for flexibility
|
|
- Could cause failures if table prefix is different
|
|
|
|
## Solutions Implemented
|
|
|
|
### 1. Created payment_processor.php Helper
|
|
**File**: `modules/billing/includes/payment_processor.php`
|
|
|
|
**Function**: `process_payment_record($record)`
|
|
- Accepts payment record from webhook or direct capture
|
|
- Finds invoices to process by custom_id (invoice_id) or invoice reference
|
|
- For each invoice:
|
|
- Marks invoice as paid (status='due' → 'paid')
|
|
- If NEW order (order_id=0): Creates new order with calculated end_date
|
|
- If RENEWAL (order_id>0): Extends existing order's end_date by invoice duration
|
|
- Links invoice to order
|
|
- Returns true/false and logs all operations
|
|
- No HTML output (safe to require from webhook/API endpoints)
|
|
|
|
### 2. Fixed capture_order.php
|
|
**File**: `modules/billing/api/capture_order.php`
|
|
|
|
**Changes**:
|
|
- **Disabled error display**: `ini_set('display_errors', '0')` to prevent JSON corruption
|
|
- **Session compatibility**: Checks both `website_user_id` and `user_id`
|
|
- **Proper JSON errors**: Returns structured JSON on DB connection failure
|
|
- **Table prefix usage**: Uses `$table_prefix` instead of hardcoded names
|
|
- **Complete invoice processing**:
|
|
- Marks all due invoices as paid
|
|
- Handles both NEW orders and RENEWALS
|
|
- Proper end_date calculation (months from qty + invoice_duration)
|
|
- Links invoices to orders
|
|
|
|
### 3. Fixed payment_success.php
|
|
**File**: `modules/billing/payment_success.php`
|
|
|
|
**Changes**:
|
|
- Requires `payment_processor.php` helper
|
|
- Displays payment confirmation page
|
|
- Shows user's recent orders
|
|
- No longer contains duplicate/incomplete function definitions
|
|
|
|
### 4. Fixed webhook.php
|
|
**File**: `modules/billing/webhook.php`
|
|
|
|
**Changes**:
|
|
- Uses `payment_processor.php` instead of requiring full payment_success.php
|
|
- Prevents HTML output that would interfere with webhook response
|
|
- Processes payment record after verification
|
|
|
|
### 5. Fixed cart.php Free Button
|
|
**File**: `modules/billing/cart.php`
|
|
|
|
**Changes**:
|
|
- Uses `payment_processor.php` for consistent processing
|
|
- Free button now properly:
|
|
- Marks invoice as paid
|
|
- Creates order record
|
|
- Calculates end_date
|
|
- Processes payment record through shared function
|
|
|
|
## Payment Flow (After Fixes)
|
|
|
|
### PayPal Payment Flow
|
|
```
|
|
1. User clicks "Pay with PayPal" in cart.php
|
|
↓
|
|
2. JavaScript calls api/create_order.php
|
|
→ Creates PayPal order with custom_id = invoice_id
|
|
↓
|
|
3. User approves payment on PayPal
|
|
↓
|
|
4. JavaScript calls api/capture_order.php
|
|
→ PayPal captures payment
|
|
→ capture_order.php:
|
|
a) Marks invoices as paid (status='due' → 'paid')
|
|
b) For NEW: Creates order in billing_orders
|
|
c) For RENEW: Extends existing order's end_date
|
|
d) Links invoice to order (sets invoice.order_id)
|
|
→ Returns JSON: { status: "COMPLETED", ... }
|
|
↓
|
|
5. JavaScript redirects to payment_success.php
|
|
→ Shows confirmation page
|
|
→ Displays order details
|
|
↓
|
|
6. PayPal sends webhook to webhook.php (parallel)
|
|
→ Verifies signature
|
|
→ Calls process_payment_record()
|
|
→ Same processing as step 4 (idempotent)
|
|
↓
|
|
7. Cart is empty (invoices now have status='paid', not shown)
|
|
```
|
|
|
|
### Free/Claim Flow
|
|
```
|
|
1. User clicks "Claim (Free)" button in cart.php
|
|
↓
|
|
2. Cart.php POST handler:
|
|
→ Marks invoice as paid
|
|
→ Creates order record with calculated end_date
|
|
→ Links invoice to order
|
|
→ Creates simulated webhook file
|
|
→ Calls process_payment_record() for consistency
|
|
↓
|
|
3. Redirects to return.php
|
|
→ Shows payment confirmation
|
|
↓
|
|
4. Cart is empty (invoice marked paid)
|
|
```
|
|
|
|
### Renewal Flow
|
|
```
|
|
1. User has existing order (order_id > 0)
|
|
↓
|
|
2. System creates renewal invoice:
|
|
→ status = 'due'
|
|
→ order_id = <existing_order_id>
|
|
→ qty = renewal months
|
|
↓
|
|
3. Invoice appears in cart
|
|
↓
|
|
4. User pays (PayPal or Free)
|
|
↓
|
|
5. process_payment_record():
|
|
→ Detects order_id > 0 (renewal)
|
|
→ Fetches current end_date from existing order
|
|
→ Calculates new end_date:
|
|
- If current end_date > now: extend from current end_date
|
|
- Otherwise: extend from now
|
|
→ Updates order with new end_date
|
|
→ Marks invoice as paid
|
|
↓
|
|
6. Order subscription extended by renewal period
|
|
```
|
|
|
|
## Testing Checklist
|
|
|
|
Before deployment, verify:
|
|
|
|
- [ ] Config setup: Copy `config.inc.php.orig` to `config.inc.php` and configure
|
|
- [ ] Database: Ensure `ogp_billing_invoices` and `ogp_billing_orders` tables exist
|
|
- [ ] Test NEW order flow:
|
|
- [ ] Add item to cart (creates invoice with status='due')
|
|
- [ ] View cart (item appears)
|
|
- [ ] Click "Claim (Free)" for $0 item (creates order, clears cart)
|
|
- [ ] Verify order created in billing_orders
|
|
- [ ] Verify invoice marked paid, linked to order
|
|
- [ ] Test PayPal flow:
|
|
- [ ] Add paid item to cart
|
|
- [ ] Click PayPal button
|
|
- [ ] Complete payment on PayPal sandbox
|
|
- [ ] Verify returns to payment_success.php without errors
|
|
- [ ] Verify order created
|
|
- [ ] Verify invoice marked paid
|
|
- [ ] Verify cart is empty
|
|
- [ ] Test RENEWAL flow:
|
|
- [ ] Create renewal invoice for existing order
|
|
- [ ] Pay renewal invoice
|
|
- [ ] Verify order end_date extended correctly
|
|
- [ ] Verify invoice marked paid
|
|
|
|
## Security Considerations
|
|
|
|
All code changes maintain or improve security:
|
|
|
|
1. **SQL Injection Protection**: Uses prepared statements where possible
|
|
2. **Input Validation**: Validates all user inputs (invoice_id, user_id, etc.)
|
|
3. **Session Security**: Maintains separate website/panel sessions
|
|
4. **Webhook Verification**: PayPal signature verification still in place
|
|
5. **Error Logging**: Errors logged, not displayed to users (prevents information leakage)
|
|
6. **Database Credentials**: Configuration file outside web root (best practice)
|
|
|
|
## Files Changed
|
|
|
|
1. `modules/billing/includes/payment_processor.php` - NEW
|
|
2. `modules/billing/api/capture_order.php` - MODIFIED
|
|
3. `modules/billing/payment_success.php` - MODIFIED
|
|
4. `modules/billing/webhook.php` - MODIFIED
|
|
5. `modules/billing/cart.php` - MODIFIED
|
|
|
|
## Known Limitations
|
|
|
|
1. **Config file required**: System requires `includes/config.inc.php` to be created from .orig template
|
|
2. **Multi-item cart matching**: If cart has multiple items, all are processed together (could improve to match specific invoice_id)
|
|
3. **No transaction rollback**: If order creation fails, invoice may still be marked paid (could improve with DB transactions)
|
|
|
|
## Future Enhancements
|
|
|
|
1. Add database transactions for atomic invoice→order operations
|
|
2. Improve invoice matching in process_payment_record (more specific matching)
|
|
3. Add unit tests for payment processing logic
|
|
4. Add admin UI for viewing/managing invoice-order relationships
|
|
5. Add email notifications for payment confirmations
|