16 KiB
IGNY8 Billing System Comprehensive Audit
Date: January 20, 2026
Status: CRITICAL - Multiple issues identified
Reviewer: System Audit
Executive Summary
The billing system has fundamental architectural flaws that cause incorrect behavior across multiple payment methods, credit management, and account activation flows. This audit identifies all issues and provides a detailed analysis of the current implementation.
Table of Contents
- Critical Issues
- Account Status & Activation Flow
- Credit System Analysis
- Invoice Management
- Payment Method Flows
- Subscription Renewal Flow
- Credit Package Purchases
- Data Model Issues
- Missing Features
- Recommended Fixes
1. Critical Issues
Issue #1: Credit Package Approval Adds WRONG Credits (CRITICAL)
Location: AdminBillingViewSet.approve_payment() and BillingViewSet.approve_payment()
Current Behavior:
- When admin approves a credit package payment, the code adds
subscription.plan.included_credits(e.g., 200 credits from the plan) - It should add the
credit_package.creditsfrom the purchased package (e.g., 500 credits)
Evidence:
User bought 500-credit package → Admin approved → System added 200 credits (plan credits)
Root Cause: Both approval methods look for subscription/plan credits instead of checking if the invoice is for a credit package purchase via invoice.metadata.credit_package_id.
Issue #2: Invoice Types Not Differentiated (CRITICAL)
Problem: No clear distinction between:
- Subscription invoices (for plan activation/renewal)
- Credit package invoices (for additional credit purchases)
Impact:
- Frontend checks
hasPendingInvoicewhich includes BOTH types - Creating a credit package invoice should NOT affect account status
- Creating a credit package invoice should NOT block other purchases
Issue #3: Credits Have No Validity/Expiration (CRITICAL)
Current State: Credits are a simple integer counter on Account model:
credits = models.IntegerField(default=0)
Missing:
- No expiration date for credits
- No validity period tracking (1 month as business rule)
- No automatic reset mechanism
- Purchased credits vs subscription credits not tracked separately
Impact: Users can accumulate credits indefinitely, no monthly reset logic.
Issue #4: Subscription Renewal Does NOT Reset Credits Correctly (CRITICAL)
Current Behavior:
- Stripe webhook
_handle_invoice_paid()callsCreditService.reset_credits_for_renewal()✓ - Manual payment approval uses
CreditService.add_credits()instead of reset ✗
Problem with add_credits:
- If user had 50 credits remaining
- Plan gives 200 credits monthly
add_credits(200)→ User now has 250 credits- Should be: Reset to 200 credits
Issue #5: No Invoice Expiration/Cancellation Mechanism
Current State:
- Pending invoices remain pending indefinitely
- No automatic void/cancellation after timeout (e.g., 24-48 hours)
- User cannot cancel/void their own pending credit package invoices
- No cleanup task for stale invoices
2. Account Status & Activation Flow
Status Values
| Status | Description | When Set |
|---|---|---|
active |
Account fully operational | After successful payment or trial activation |
trial |
Free trial period | On signup with free plan |
pending_payment |
Awaiting first payment | After signup with paid plan, before payment |
suspended |
Manually suspended | Admin action |
cancelled |
Subscription cancelled | After cancellation |
Current Flow Problems
New User with Paid Plan (Bank Transfer):
- User signs up selecting paid plan with bank transfer →
status = pending_payment✓ - Invoice created → Invoice
status = pending✓ - User submits bank transfer confirmation → Payment
status = pending_approval✓ - Admin approves payment → Account
status = active✓ - BUG: Credits added = plan credits (not credit package credits if that's what was purchased)
Existing Active User Buying Credits (Bank Transfer):
- User has
status = active - User creates credit package invoice → Invoice created with
status = pending - BUG: Frontend incorrectly interprets this as "account has pending invoice"
- BUG: Some UI elements get disabled unnecessarily
- Admin approves → BUG: Wrong credits added (plan credits instead of package credits)
3. Credit System Analysis
Current Data Model
Account.credits (integer)
- Single counter for all credits
- No distinction between:
- Plan/subscription credits
- Purchased credit packages
- Bonus/promotional credits
- No expiration tracking
CreditTransaction (history log)
- Records additions and deductions
- Transaction types:
purchase,subscription,refund,deduction,adjustment - Has
balance_afterfield for audit trail
CreditUsageLog (detailed usage)
- Per-operation usage tracking
- Links to site, operation type, model used
- Used for analytics and billing reports
Credit Flow Issues
| Scenario | Expected Behavior | Current Behavior | Status |
|---|---|---|---|
| New subscription | Reset credits to plan amount | Add credits to existing | ❌ BUG |
| Subscription renewal (Stripe) | Reset credits to plan amount | Reset credits | ✓ OK |
| Subscription renewal (Manual) | Reset credits to plan amount | Add credits | ❌ BUG |
| Credit package purchase | Add package credits | Add plan credits | ❌ BUG |
| Monthly reset | Reset subscription credits, keep purchased | No reset logic | ❌ MISSING |
Credit Validity Rules (Business Requirement - NOT IMPLEMENTED)
Per business rules, credits should have a 1-month validity period:
- Subscription credits reset monthly on billing cycle
- Purchased credits have 1-month validity from purchase date
- Expired credits should be voided
Current Implementation: None - credits never expire or reset.
4. Invoice Management
Invoice Types (Implicit, Not Enforced)
| Type | Identifier | Purpose |
|---|---|---|
| Subscription | invoice.subscription IS NOT NULL |
Plan activation/renewal |
| Credit Package | invoice.metadata.credit_package_id EXISTS |
Additional credit purchase |
| Custom | Neither | Admin-created invoices |
Invoice Status Flow
draft → pending → paid/void/uncollectible
Issues
- No Expiration: Invoices stay pending forever
- No User Cancellation: Users cannot cancel their own credit package invoices
- No Automatic Cleanup: No task to void stale invoices
- No Type Enforcement: Invoice type determined by checking multiple fields
Recommended Invoice Statuses
| Status | Description | Auto-Transition |
|---|---|---|
draft |
Created, not sent | → pending when finalized |
pending |
Awaiting payment | → void after 48h (credit packages) |
paid |
Payment received | Terminal |
void |
Cancelled/expired | Terminal |
uncollectible |
Cannot be collected | Terminal |
5. Payment Method Flows
Supported Payment Methods
| Method | Countries | Auto-Approval | Account Activation |
|---|---|---|---|
| Stripe (Card) | All except PK | Yes | Immediate |
| PayPal | All except PK | Yes | Immediate |
| Bank Transfer | PK only | No (Manual) | After admin approval |
Stripe Flow (Working ✓)
- User initiates checkout → Stripe session created
- User completes payment on Stripe
- Webhook
checkout.session.completed→ Account activated, credits added - Webhook
invoice.paid(renewal) → Credits reset
PayPal Flow (Working ✓)
- User initiates checkout → PayPal order created
- User approves on PayPal
- Payment captured → Account activated, credits added
Bank Transfer Flow (BUGGY ❌)
- User initiates → Invoice created, account status may change incorrectly
- User submits confirmation → Payment created with
pending_approval - Admin approves → WRONG CREDITS ADDED
- User rejection flow exists but no automatic expiration
6. Subscription Renewal Flow
Automatic (Stripe/PayPal)
- Stripe handles automatic billing
- Webhook
invoice.paidtriggers renewal reset_credits_for_renewal()correctly resets credits ✓
Manual (Bank Transfer)
- Task
process_subscription_renewalscreates renewal invoice - Subscription status →
pending_renewal - Grace period (7 days) starts
- User pays manually
- BUG: Admin approval uses
add_credits()instead ofreset_credits_for_renewal() - Task
check_expired_renewalsexpires after grace period ✓
7. Credit Package Purchases
Current Flow
- User selects package
- For Stripe/PayPal: Redirect to checkout
- For Bank Transfer: Create invoice → Show payment form
Issues
| Step | Issue | Severity |
|---|---|---|
| Invoice Creation | Uses InvoiceService.create_credit_package_invoice() |
OK |
Invoice stores credit_package_id in metadata |
OK | OK |
Approval reads subscription.plan.included_credits |
WRONG | CRITICAL |
Should read invoice.metadata.credit_package_id → get package → add package.credits |
Missing | CRITICAL |
Stripe Credit Purchase (Partially Working)
- Webhook handler
_handle_credit_purchase()correctly reads metadata - Uses
CreditService.add_credits()with correct amount ✓
Manual Credit Purchase (BROKEN)
approve_payment()does NOT check if invoice is credit package- Adds plan credits instead of package credits
- Completely wrong behavior
8. Data Model Issues
Missing Fields
Invoice Model:
invoice_typeenum:subscription,credit_package,customexpires_atdatetime for auto-voidcredit_package_idFK (instead of in metadata)
Account Model:
subscription_credits(monthly reset)purchased_credits(expire after 1 month)credits_valid_untildatetime
CreditTransaction:
expires_atdatetimesource_type:subscription,purchase,bonus
Proposed Schema Changes
Account:
+ subscription_credits: int (resets monthly)
+ purchased_credits: int (separate pool)
+ credits_last_reset: datetime
Invoice:
+ invoice_type: enum('subscription', 'credit_package', 'custom')
+ expires_at: datetime (nullable)
CreditPurchase: (NEW MODEL)
- account: FK
- credit_package: FK
- credits: int
- purchase_date: datetime
- expires_at: datetime
- credits_remaining: int
- status: enum('active', 'expired', 'used')
9. Missing Features
Invoice Management
- User can cancel own pending credit package invoices
- Auto-void invoices after 24-48 hours
- Invoice expiration emails
- Invoice reminder emails (before expiration)
Credit Management
- Credit expiration tracking
- Monthly credit reset task
- Separate subscription vs purchased credits
- Credit validity period configuration
- Low credit warnings before operations
Payment Management
- Payment retry for failed payments
- Automatic payment reminder emails
- Payment receipt PDFs
- Refund processing
Admin Features
- Bulk invoice void
- Credit adjustment with reason
- Payment history export
- Revenue reports
10. Recommended Fixes
Priority 1: Critical (Immediate Fix Required)
Fix 1.1: Credit Package Approval Logic
File: billing_views.py - approve_payment() method
Change: Before adding credits, check if invoice has credit_package_id in metadata:
IF invoice.metadata.credit_package_id EXISTS:
package = CreditPackage.get(invoice.metadata.credit_package_id)
credits_to_add = package.credits
ELSE:
credits_to_add = subscription.plan.included_credits
Fix 1.2: Manual Renewal Should Reset Credits
File: billing_views.py - approve_payment() method
Change: For subscription renewals, use reset_credits_for_renewal() instead of add_credits().
Fix 1.3: Separate Invoice Type Checking in Frontend
File: PlansAndBillingPage.tsx
Already Fixed: hasPendingSubscriptionInvoice and hasPendingCreditInvoice now separate.
Priority 2: High (Fix Within 1 Week)
Fix 2.1: Add Invoice Expiration
- Add
expires_atfield to Invoice model - Set expiration on creation (48 hours for credit packages)
- Create Celery task to void expired invoices
- Send reminder email 24 hours before expiration
Fix 2.2: User Invoice Cancellation
- Add API endpoint for user to cancel own pending credit package invoices
- Add "Cancel" button in frontend invoice list
- Set invoice status to
voidon cancellation
Priority 3: Medium (Fix Within 1 Month)
Fix 3.1: Credit Expiration System
- Add credit validity tracking
- Create monthly reset task for subscription credits
- Track purchased credits separately with expiration
Fix 3.2: Invoice Type Field
- Add
invoice_typeenum field - Migrate existing invoices based on metadata
- Update all queries to use type field
Priority 4: Low (Future Enhancement)
- Payment retry automation
- Revenue reporting
- Credit pool separation
- Advanced analytics
Appendix A: Current Code Locations
| Component | File Path |
|---|---|
| Account Model | auth/models.py |
| Plan Model | auth/models.py |
| Subscription Model | auth/models.py |
| Invoice Model | business/billing/models.py |
| Payment Model | business/billing/models.py |
| CreditPackage Model | business/billing/models.py |
| CreditTransaction Model | business/billing/models.py |
| CreditService | business/billing/services/credit_service.py |
| InvoiceService | business/billing/services/invoice_service.py |
| PaymentService | business/billing/services/payment_service.py |
| Admin Approval (v1) | business/billing/billing_views.py |
| Admin Approval (v2) | modules/billing/views.py |
| Stripe Webhooks | business/billing/views/stripe_views.py |
| PayPal Webhooks | business/billing/views/paypal_views.py |
| Renewal Tasks | business/billing/tasks/subscription_renewal.py |
Appendix B: Test Scenarios Required
| Scenario | Current Result | Expected Result |
|---|---|---|
| PK user buys 500 credit package via bank transfer | Gets 200 credits (plan) | Gets 500 credits |
| Active user creates credit invoice | Account blocked | Account stays active |
| Subscription renewal (manual) | Credits added | Credits reset |
| Invoice unpaid after 48 hours | Stays pending | Auto-void |
| User wants to cancel own invoice | No option | Can cancel |
| Credits unused for 1 month | Stay forever | Should expire |
Appendix C: Affected User Flows
Flow 1: New PK User Subscription
Signup → Select Plan → Bank Transfer → Submit Confirmation → Wait for Approval → Account Active
Issues: None if subscription only
Flow 2: Active PK User Buys Credits
Go to Credits → Select Package → Bank Transfer → Invoice Created → Submit Confirmation → Wait → WRONG CREDITS ADDED
Issues:
- Invoice creation may show incorrect UI states
- Wrong credits added on approval
Flow 3: Active Non-PK User Buys Credits
Go to Credits → Select Package → Stripe Checkout → Payment → Credits Added
Issues: None - Stripe handler works correctly
Flow 4: Subscription Renewal (Manual)
Renewal Date → Invoice Created → User Pays → Admin Approves → CREDITS ADDED (not reset)
Issues: Should reset credits, not add
End of Audit Report
Next Steps:
- Review this audit with team
- Prioritize fixes based on business impact
- Implement Priority 1 fixes immediately
- Create automated tests for all scenarios
- Schedule remaining fixes