# 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 1. [Critical Issues](#1-critical-issues) 2. [Account Status & Activation Flow](#2-account-status--activation-flow) 3. [Credit System Analysis](#3-credit-system-analysis) 4. [Invoice Management](#4-invoice-management) 5. [Payment Method Flows](#5-payment-method-flows) 6. [Subscription Renewal Flow](#6-subscription-renewal-flow) 7. [Credit Package Purchases](#7-credit-package-purchases) 8. [Data Model Issues](#8-data-model-issues) 9. [Missing Features](#9-missing-features) 10. [Recommended Fixes](#10-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.credits` from 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 `hasPendingInvoice` which 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: ```python 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()` calls `CreditService.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): 1. User signs up selecting paid plan with bank transfer → `status = pending_payment` ✓ 2. Invoice created → Invoice `status = pending` ✓ 3. User submits bank transfer confirmation → Payment `status = pending_approval` ✓ 4. Admin approves payment → Account `status = active` ✓ 5. **BUG:** Credits added = plan credits (not credit package credits if that's what was purchased) #### Existing Active User Buying Credits (Bank Transfer): 1. User has `status = active` 2. User creates credit package invoice → Invoice created with `status = pending` 3. **BUG:** Frontend incorrectly interprets this as "account has pending invoice" 4. **BUG:** Some UI elements get disabled unnecessarily 5. 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_after` field 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 1. **No Expiration:** Invoices stay pending forever 2. **No User Cancellation:** Users cannot cancel their own credit package invoices 3. **No Automatic Cleanup:** No task to void stale invoices 4. **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 ✓) 1. User initiates checkout → Stripe session created 2. User completes payment on Stripe 3. Webhook `checkout.session.completed` → Account activated, credits added 4. Webhook `invoice.paid` (renewal) → Credits reset ### PayPal Flow (Working ✓) 1. User initiates checkout → PayPal order created 2. User approves on PayPal 3. Payment captured → Account activated, credits added ### Bank Transfer Flow (BUGGY ❌) 1. User initiates → Invoice created, account status may change incorrectly 2. User submits confirmation → Payment created with `pending_approval` 3. Admin approves → **WRONG CREDITS ADDED** 4. User rejection flow exists but no automatic expiration --- ## 6. Subscription Renewal Flow ### Automatic (Stripe/PayPal) - Stripe handles automatic billing - Webhook `invoice.paid` triggers renewal - `reset_credits_for_renewal()` correctly resets credits ✓ ### Manual (Bank Transfer) 1. Task `process_subscription_renewals` creates renewal invoice 2. Subscription status → `pending_renewal` 3. Grace period (7 days) starts 4. User pays manually 5. **BUG:** Admin approval uses `add_credits()` instead of `reset_credits_for_renewal()` 6. Task `check_expired_renewals` expires after grace period ✓ --- ## 7. Credit Package Purchases ### Current Flow 1. User selects package 2. For Stripe/PayPal: Redirect to checkout 3. 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_type` enum: `subscription`, `credit_package`, `custom` - `expires_at` datetime for auto-void - `credit_package_id` FK (instead of in metadata) **Account Model:** - `subscription_credits` (monthly reset) - `purchased_credits` (expire after 1 month) - `credits_valid_until` datetime **CreditTransaction:** - `expires_at` datetime - `source_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_at` field 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 `void` on 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_type` enum 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:** 1. Review this audit with team 2. Prioritize fixes based on business impact 3. Implement Priority 1 fixes immediately 4. Create automated tests for all scenarios 5. Schedule remaining fixes