# Payment System ## Purpose Describe how billing and payment processing currently work in `nproxy`. This document is about the implemented system, not the desired future state. ## Scope The current payment system covers: - default subscription plan bootstrap - user subscription creation at registration time - invoice creation through a provider adapter - manual admin activation when an invoice is confirmed as paid - quota-cycle reset on successful activation The current payment system does not yet cover: - provider webhooks - polling-based reconciliation - automatic `expired` or `canceled` transitions - recurring billing ## Main records ### `SubscriptionPlan` - one default active plan is bootstrapped by `packages/db/src/bootstrap.ts` - current default seed: - `code`: `monthly` - `displayName`: `Monthly` - `monthlyRequestLimit`: `100` - `monthlyPriceUsd`: `9.99` - `billingCurrency`: `USDT` ### `Subscription` - created for a new user during registration if the default plan exists - starts in `pending_activation` - becomes `active` only after successful invoice activation - stores: - `activatedAt` - `currentPeriodStart` - `currentPeriodEnd` - `renewsManually` ### `PaymentInvoice` - stores one provider-facing payment attempt for a subscription - important fields: - local `id` - `subscriptionId` - `provider` - `providerInvoiceId` - `status` - `currency` - `amountCrypto` - `amountUsd` - `paymentAddress` - `expiresAt` - `paidAt` ### `UsageLedgerEntry` - records quota-affecting events - on successful payment activation the system writes `cycle_reset` - successful generations later write `generation_success` ### `AdminAuditLog` - records state-changing admin actions - the current admin `mark-paid` flow writes: - `invoice_mark_paid` - `invoice_mark_paid_replayed` ## Provider boundary Payment-provider code stays behind `packages/providers/src/payments.ts`. Current provider contract: - `createInvoice(input) -> providerInvoiceId, paymentAddress, amountCrypto, amountUsd, currency, expiresAt` Current runtime note: - the provider adapter is still a placeholder adapter - provider callbacks and status lookups are not implemented yet - the rest of the payment flow is intentionally provider-agnostic ## Registration flow 1. User registers through `POST /api/auth/register`. 2. `packages/db/src/auth-store.ts` creates the user and session. 3. If the default plan exists, the store also creates a `Subscription` in `pending_activation`. 4. At this point the user has an account and a plan assignment, but not an active paid cycle. ## Invoice creation flow 1. Authenticated user calls `POST /api/billing/invoices`. 2. `packages/db/src/billing-store.ts` loads the latest subscription for that user. 3. If there is already a non-expired `pending` invoice for the same subscription, it is reused. 4. Otherwise the app calls the payment-provider adapter `createInvoice`. 5. The returned provider invoice data is persisted as a new local `PaymentInvoice` in `pending`. 6. The API returns invoice details, including provider invoice id, amount, address, and expiry time. ## Invoice listing flow - `GET /api/billing/invoices` returns the user's invoices ordered by newest first. - This is a read-only view over persisted `PaymentInvoice` rows. ## Current activation flow The implemented activation path is manual and admin-driven. 1. An authenticated admin calls `POST /api/admin/invoices/:id/mark-paid`. 2. The web app resolves the admin session and passes actor metadata into the billing store. 3. `markInvoicePaid` runs inside one database transaction. 4. If the invoice is `pending`, the store: - updates the invoice to `paid` - sets `paidAt` - updates the related subscription to `active` - sets `activatedAt` if it was not already set - sets `currentPeriodStart = paidAt` - sets `currentPeriodEnd = paidAt + 30 days` - clears `canceledAt` - writes a `UsageLedgerEntry` with `entryType = cycle_reset` - writes an `AdminAuditLog` entry `invoice_mark_paid` 5. The API returns the updated invoice. ## Idempotency and transition rules `markInvoicePaid` is replay-safe. ### Allowed cases - `pending -> paid` - `paid -> paid` as a no-op replay ### Replay behavior If the invoice is already `paid`: - the subscription is not mutated again - no extra `cycle_reset` entry is created - an audit event `invoice_mark_paid_replayed` is written ### Rejected cases If the invoice is already terminal: - `expired` - `canceled` the store rejects the request with `invoice_transition_not_allowed`. If the invoice does not exist, the store returns `invoice_not_found`. ## How quota ties to payment - A user can create generation requests only with an active subscription. - Approximate quota shown to the user is derived from `generation_success` entries since the current billing cycle start. - A successful payment activation starts a new cycle by writing `cycle_reset` and moving the subscription window forward. - Failed generations do not consume quota. ## HTTP surface - `POST /api/billing/invoices` - create or reuse the current pending invoice for the authenticated user - `GET /api/billing/invoices` - list the authenticated user's invoices - `POST /api/admin/invoices/:id/mark-paid` - admin-only manual payment activation path ## Error behavior Current payment-specific errors surfaced by the web app: - `invoice_not_found` -> `404` - `invoice_transition_not_allowed` -> `409` ## Current limitations - The system still depends on manual admin confirmation to activate access. - No provider callback or reconciliation job updates invoice state automatically. - No runtime path currently moves invoices to `expired` or `canceled`. - The provider adapter does not yet verify external status or signatures. - Subscription lifecycle beyond the current `mark-paid` path is still incomplete. ## Code references - `packages/db/src/bootstrap.ts` - `packages/db/src/auth-store.ts` - `packages/db/src/billing-store.ts` - `packages/db/src/account-store.ts` - `packages/providers/src/payments.ts` - `apps/web/src/main.ts`