docs: describe current payment system

This commit is contained in:
sirily
2026-03-10 17:30:35 +03:00
parent f575e2395d
commit 064f76b5c0

170
docs/ops/payment-system.md Normal file
View File

@@ -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`