diff --git a/docs/ops/payment-system.md b/docs/ops/payment-system.md new file mode 100644 index 0000000..7d9e5e2 --- /dev/null +++ b/docs/ops/payment-system.md @@ -0,0 +1,170 @@ +# 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`