> ## Documentation Index
> Fetch the complete documentation index at: https://docs.credibill.tech/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment System Overview

> Understanding CrediBill multi-tenant payment infrastructure

## Overview

CrediBill is a production-grade billing infrastructure for SaaS applications using African payment providers. It handles subscription billing, payment orchestration, and real-time webhook delivery while maintaining multi-tenant isolation and security.

### Key Capabilities

* **Multi-Tenant Architecture**: Each SaaS app maintains isolated payment provider credentials
* **4 African Payment Providers**: Flutterwave, PawaPay, Pesapal, DPO
* **Automated Billing Cycles**: Cron-based trial expirations, recurring payments, and retries
* **Production-Grade Security**: HMAC signature verification, replay attack prevention, encrypted credentials
* **Real-Time Webhooks**: Bi-directional webhook delivery with retry logic
* **Comprehensive Logging**: Full audit trail of all payment events

## Architecture

### Multi-Tenant Design

```
┌─────────────────────────────────────────────────┐
│         SaaS App 1          SaaS App 2          │
│    (Flutterwave + DPO)  (PawaPay + Pesapal)    │
└────────┬──────────────────────────┬──────────────┘
         │                          │
         └──────────┬───────────────┘
                    │
         ┌──────────▼──────────┐
         │    CrediBill        │
         │  Orchestration      │
         └──────────┬──────────┘
                    │
         ┌──────────┴────────────────┬──────────────┬─────────────┐
         │                           │              │             │
    ┌────▼─────┐  ┌────────────┐  ┌─▼────────┐  ┌─▼──────┐  ┌──▼──────┐
    │Flutterwave   │PawaPay    │  │Pesapal   │  │DPO    │  │Others   │
    └──────────┘  └────────────┘  └──────────┘  └───────┘  └─────────┘
```

Each SaaS app:

* Configures their own payment provider credentials
* Maintains encrypted credential storage
* Receives isolated webhook events
* Has payment transactions flow directly to their provider account

**CrediBill acts as orchestrator, not custodian** - funds never touch CrediBill accounts.

## Payment Lifecycle

### 1. Trial to Paid Conversion

**Trigger:** Customer's trial period expires

**Flow:**

```
Daily 2 AM UTC
    │
    ▼
Check for expired trials
    │
    ├─ Find subscriptions with trialEndsAt <= now
    │
    ▼
For each expired trial:
    │
    ├─ Create invoice from subscription + usage
    │
    ├─ Get app's primary payment provider
    │
    ├─ Call provider adapter (initiatePayment)
    │
    ├─ Store payment transaction record
    │
    ├─ Set transaction status: "initiated"
    │
    ▼
Update subscription status: "active"
```

**Implementation Functions:**

* `convex/cronHandlers.ts`: `processTrialExpirations()`
* `convex/payments.ts`: `initiateSubscriptionPayment()`
* `convex/paymentsNode.ts`: Provider-specific adapters

### 2. Recurring Payments

**Trigger:** Subscription renewal date reached

**Flow:**

```
Daily 3 AM UTC
    │
    ▼
Check for due subscriptions
    │
    ├─ Find where currentPeriodEnd <= now
    │
    ▼
For each due subscription:
    │
    ├─ Aggregate usage (if usage-based)
    │
    ├─ Create invoice for next period
    │
    ├─ Initiate payment via provider
    │
    ├─ Update billing dates
    │     ├─ currentPeriodStart = old.currentPeriodEnd
    │     └─ currentPeriodEnd += interval
    │
    ▼
Log payment transaction
```

**Implementation Functions:**

* `convex/cronHandlers.ts`: `processRecurringPayments()`
* `convex/payments.ts`: `processRecurringPayment()`

### 3. Payment Confirmation (Webhook)

**Trigger:** Payment provider sends webhook confirmation

**Flow:**

```
Provider sends POST /webhooks/{provider}
    │
    ▼
Verify signature
    │
    ├─ Get webhook secret from config
    │
    ├─ Compute HMAC-SHA256(timestamp.body)
    │
    ├─ Compare with header (timing-safe)
    │
    ├─ Reject if mismatch
    │
    ▼
Check replay attack
    │
    ├─ Validate timestamp within 5 minutes
    │
    ├─ Reject if too old or future
    │
    ▼
Check idempotency
    │
    ├─ Search webhookLogs for same provider event ID
    │
    ├─ Reject if already processed
    │
    ▼
Find matching transaction
    │
    ├─ Look up by providerTransactionId
    │
    ├─ Verify transaction exists
    │
    ▼
Update transaction status
    │
    ├─ Transaction: pending → success/failed
    │
    ├─ Guard against terminal states
    │     └─ Never overwrite: success/canceled/refunded
    │
    ├─ Mark invoice as paid (if success)
    │
    ▼
Send outgoing webhook to SaaS app
    │
    ├─ Event: "payment.success" or "payment.failed"
    │
    ├─ Include transaction details
    │
    ├─ Retry if SaaS app endpoint fails
    │
    ▼
Log webhook event
```

**Implementation Functions:**

* `convex/http.ts`: Webhook route handlers
* `convex/webhookActions.ts`: Event handlers
* `convex/webhookQueries.ts`: Transaction lookups
* `convex/webhookMutations.ts`: Status updates
* `convex/outgoingWebhooks.ts`: SaaS app notifications

### 4. Failed Payment Handling

**Trigger:** Payment fails or webhook indicates failure

**Flow:**

```
Payment marked as FAILED
    │
    ▼
Subscription status: ACTIVE → PAST_DUE
    │
    ├─ Increment failedAttempts counter
    │
    ├─ Record failure timestamp
    │
    ├─ Set nextRetryAt = now + exponentialBackoff
    │
    ▼
Send outgoing webhook
    │
    ├─ Event: "payment.failed"
    │
    ├─ Include failure reason from provider
    │
    ├─ SaaS app decides action (restrict access, etc)
    │
    ▼
Schedule retry (Cron: Daily 4 AM UTC)
    │
    ├─ Check subscriptions in PAST_DUE status
    │
    ├─ Where nextRetryAt <= now AND failedAttempts < 3
    │
    ├─ Re-initiate payment via provider
    │
    ├─ Update retry count
    │
    ▼
After 3 failures
    │
    ├─ Leave subscription in PAST_DUE
    │
    ├─ Send "final notice" webhook
    │
    ├─ SaaS app must disable access
    │
    ▼
If payment succeeds on retry:
    │
    ├─ Status: PAST_DUE → ACTIVE
    │
    ├─ Send "payment.success" webhook
    │
    ├─ Reset retry counter
    │
    ▼
Optional: Proration
    │
    └─ If retry delayed, calculate proration
        └─ Credit customer for failed period
```

**Implementation Functions:**

* `convex/cronHandlers.ts`: `retryFailedPayments()`
* `convex/webhookMutations.ts`: `updateTransactionFromWebhook()`

## Database Schema

### Payment Providers

```typescript theme={null}
// Table: paymentProviders
{
  _id: Id<"paymentProviders">,
  _creationTime: number,

  // Multi-tenant isolation
  organizationId: Id<"organizations">,
  appId: Id<"apps">,

  // Provider identification
  provider: "flutterwave" | "pawapay" | "pesapal" | "dpo",

  // Encrypted credentials
  credentials: {
    // Common fields
    secretKeyEncrypted: string, // AES-256-GCM encrypted

    // Provider-specific
    publicKey?: string,        // Flutterwave, DPO
    merchantId?: string,       // Some providers
    apiUrl?: string,           // Custom or provider-specific
    apiToken?: string,         // PawaPay
    consumerKey?: string,      // Pesapal
    consumerSecretEncrypted?: string, // Pesapal
  },

  // Configuration
  environment: "test" | "live",
  isPrimary: boolean,
  isActive: boolean,

  // Webhooks
  webhookSecret?: string,      // Provider's webhook signing secret
  webhookUrl: string,

  // Metadata
  lastTestedAt?: number,
  lastSuccessfulPaymentAt?: number,
  connectionStatus: "active" | "error" | "unconfigured",
  errorMessage?: string,
}
```

### Payment Transactions

```typescript theme={null}
// Table: paymentTransactions
{
  _id: Id<"paymentTransactions">,
  _creationTime: number,

  // Multi-tenant isolation
  organizationId: Id<"organizations">,
  appId: Id<"apps">,

  // Relationships
  customerId: Id<"customers">,
  subscriptionId?: Id<"subscriptions">,
  invoiceId?: Id<"invoices">,
  paymentProviderId: Id<"paymentProviders">,

  // Payment details
  amount: number,              // In smallest currency unit (e.g., cents)
  currency: string,            // ISO 4217 code
  paymentMethod: "mobile_money_mtn" | "mobile_money_airtel"
               | "card_visa" | "card_mastercard" | "bank_transfer",

  // Provider reference
  providerTransactionId?: string,   // From provider's response
  providerReference?: string,       // Transaction reference for customer
  merchantReference: string,        // Internal reference

  // Status flow: pending → initiated → processing → success/failed
  status: "pending" | "initiated" | "processing" | "success" | "failed" | "canceled" | "refunded",

  // Retry tracking
  attemptNumber: number,
  isRetry: boolean,
  failureReason?: string,
  failureCode?: string,
  nextRetryAt?: number,

  // Provider response
  providerResponse?: Record<string, any>,

  // Timestamps
  initiatedAt: number,
  completedAt?: number,
  expiresAt?: number,           // When transaction expires if not completed

  // Idempotency
  idempotencyKey?: string,
}
```

### Webhook Logs (Incoming)

```typescript theme={null}
// Table: webhookLogs
{
  _id: Id<"webhookLogs">,
  _creationTime: number,

  // Multi-tenant isolation
  organizationId: Id<"organizations">,
  appId: Id<"apps">,

  // Source
  provider: "flutterwave" | "pawapay" | "pesapal" | "dpo",

  // Event details
  eventId: string,              // Provider's unique event ID
  event: string,                // e.g., "charge.completed"
  payload: Record<string, any>,

  // Processing status
  status: "received" | "processing" | "processed" | "failed" | "ignored",

  // Validation
  signatureValid: boolean,
  signatureError?: string,
  replayCheckPassed: boolean,
  idempotencyCheckPassed: boolean,

  // Matching
  paymentTransactionId?: Id<"paymentTransactions">,
  subscriptionId?: Id<"subscriptions">,

  // Timing
  receivedAt: number,
  processedAt?: number,
  processingDurationMs?: number,

  // Error tracking
  error?: string,
}
```

### Outgoing Webhooks

```typescript theme={null}
// Table: outgoingWebhookDeliveries
{
  _id: Id<"outgoingWebhookDeliveries">,
  _creationTime: number,

  // Multi-tenant isolation
  organizationId: Id<"organizations">,
  appId: Id<"apps">,

  // Event details
  event: "payment.success" | "payment.failed" | "subscription.past_due" | ...,

  // Related entities
  customerId: Id<"customers">,
  paymentTransactionId?: Id<"paymentTransactions">,
  subscriptionId?: Id<"subscriptions">,

  // Payload
  payload: Record<string, any>,

  // Delivery tracking
  webhookUrl: string,
  status: "pending" | "delivered" | "failed" | "retrying",
  attemptCount: number,
  maxAttempts: number,

  // Response
  lastHttpStatus?: number,
  lastErrorMessage?: string,

  // Retry scheduling
  nextRetryAt?: number,

  // Timing
  firstAttemptAt?: number,
  lastAttemptAt?: number,
  deliveredAt?: number,
}
```

## Security Features

### Webhook Signature Verification

All incoming webhooks must be verified using HMAC-SHA256:

```typescript theme={null}
// Verification pseudocode
function verifyWebhookSignature(
  providedSignature: string,
  timestamp: string,
  body: string,
  webhookSecret: string
): boolean {
  const payload = `${timestamp}.${body}`;
  const expectedSignature = HMAC_SHA256(payload, webhookSecret);

  // Timing-safe comparison prevents timing attacks
  return timingSafeEqual(expectedSignature, providedSignature);
}
```

**Security considerations:**

* **Timing-safe comparison**: Prevents attackers from determining signature validity through response timing
* **Timestamp validation**: Reject webhooks older than 5 minutes (prevent replay attacks)
* **Signature verification**: Ensures webhook originated from provider, not attacker

### Credential Encryption

All stored provider credentials use AES-256-GCM encryption:

```typescript theme={null}
function encryptCredential(plaintext: string, appId: Id<"apps">): string {
  // Derive key from appId using SHA-256
  const key = SHA256(`${appId}:${process.env.MASTER_KEY}`);

  // Encrypt with AES-256-GCM
  const iv = randomBytes(16);
  const cipher = createCipheriv("aes-256-gcm", key, iv);
  const encrypted =
    cipher.update(plaintext, "utf-8", "hex") + cipher.final("hex");
  const authTag = cipher.getAuthTag().toString("hex");

  // Return: iv:encrypted:authTag (format for storage)
  return `${iv}:${encrypted}:${authTag}`;
}
```

**Security implications:**

* **Per-app encryption**: Each app has isolated encryption keys
* **AES-256-GCM**: Authenticated encryption prevents tampering
* **No central key storage**: Master key in environment variables

### Race Condition Protection

Critical updates use atomic transactions:

```typescript theme={null}
// Transaction status update (simplified)
async function updateTransactionStatus(
  transactionId: Id<"paymentTransactions">,
  newStatus: "success" | "failed"
) {
  const transaction = await db.paymentTransactions.get(transactionId);

  // Guard against overwriting terminal states
  const terminalStates = ["success", "canceled", "refunded"];
  if (terminalStates.includes(transaction.status)) {
    throw new Error("Cannot update transaction in terminal state");
  }

  // Atomic update
  await db.paymentTransactions.patch(transactionId, {
    status: newStatus,
    completedAt: Date.now(),
  });
}
```

## Cron Jobs Schedule

| Job                    | Frequency   | Time (UTC) | Purpose                        |
| ---------------------- | ----------- | ---------- | ------------------------------ |
| Trial Expirations      | Daily       | 2:00 AM    | Convert expired trials to paid |
| Recurring Payments     | Daily       | 3:00 AM    | Process subscription renewals  |
| Failed Payment Retries | Daily       | 4:00 AM    | Retry failed transactions      |
| Cleanup Expired        | Daily       | 5:00 AM    | Mark old pending as failed     |
| Webhook Retries        | Every 5 min | -          | Retry failed outgoing webhooks |

## Next Steps

* [Payment Providers](/docs/payments/providers) - Configure each provider
* [Webhooks](/docs/webhooks/overview) - Handle incoming webhooks
* [Integration Guide](/docs/integration/overview) - Integrate CrediBill
* [Security Best Practices](/docs/security/overview) - Production security
