8.7 KiB
8.7 KiB
Billing Invoice/Order Flow - Fix Summary
Problem Statement
The billing system had several critical issues:
- JSON Error: "Failed to execute 'json' on 'Response': Unexpected end of JSON input" when returning from PayPal payment
- Cart not clearing: Items remained in cart after payment (invoices stayed as status='due')
- No order creation: Orders were not being created after successful payment
- Missing renewal flow: Renewal invoices (linked to existing orders) were not handled
- Free button errors: The free/claim button was also experiencing errors
Invoice-First Flow (Intended Design)
The system uses an invoice-first architecture:
- Add to Cart: Creates INVOICE with status='due', order_id=0 (no order yet)
- View Cart: Shows all invoices WHERE status='due'
- 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
- 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.phphad 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.phpwas 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_idanduser_id - Proper JSON errors: Returns structured JSON on DB connection failure
- Table prefix usage: Uses
$table_prefixinstead 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.phphelper - 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.phpinstead 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.phpfor 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.origtoconfig.inc.phpand configure - Database: Ensure
ogp_billing_invoicesandogp_billing_orderstables 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:
- SQL Injection Protection: Uses prepared statements where possible
- Input Validation: Validates all user inputs (invoice_id, user_id, etc.)
- Session Security: Maintains separate website/panel sessions
- Webhook Verification: PayPal signature verification still in place
- Error Logging: Errors logged, not displayed to users (prevents information leakage)
- Database Credentials: Configuration file outside web root (best practice)
Files Changed
modules/billing/includes/payment_processor.php- NEWmodules/billing/api/capture_order.php- MODIFIEDmodules/billing/payment_success.php- MODIFIEDmodules/billing/webhook.php- MODIFIEDmodules/billing/cart.php- MODIFIED
Known Limitations
- Config file required: System requires
includes/config.inc.phpto be created from .orig template - Multi-item cart matching: If cart has multiple items, all are processed together (could improve to match specific invoice_id)
- No transaction rollback: If order creation fails, invoice may still be marked paid (could improve with DB transactions)
Future Enhancements
- Add database transactions for atomic invoice→order operations
- Improve invoice matching in process_payment_record (more specific matching)
- Add unit tests for payment processing logic
- Add admin UI for viewing/managing invoice-order relationships
- Add email notifications for payment confirmations