485 lines
16 KiB
Markdown
485 lines
16 KiB
Markdown
# 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
|
|
|