200 lines
8.1 KiB
Markdown
200 lines
8.1 KiB
Markdown
# 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 after an operator verifies that the provider reported a final successful payment status
|
|
- automatic expiry of elapsed subscription periods during account and generation access checks
|
|
- 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
|
|
- `pending` means the invoice exists, but the payment is not yet considered final or accepted
|
|
- `paid` means the payment is considered final and safe to activate against
|
|
- 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.
|
|
|
|
## Payment status semantics
|
|
- `pending` does not count as paid.
|
|
- `pending` does not activate the subscription.
|
|
- `pending` does not reset quota.
|
|
- The system must treat an invoice as `paid` only after the payment provider reports a final successful status, meaning the funds are accepted strongly enough for access activation.
|
|
- The current implementation does not fetch or verify that provider-final status automatically yet.
|
|
|
|
## 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. This endpoint is intended to be used only after the operator has already verified that the provider reached a final successful payment state.
|
|
4. `markInvoicePaid` runs inside one database transaction.
|
|
5. 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`
|
|
6. The API returns the updated invoice.
|
|
|
|
Important constraint:
|
|
- `mark-paid` is not evidence by itself that a `pending` invoice became payable.
|
|
- It is only the current manual mechanism for recording that the provider has already given final confirmation outside the app.
|
|
|
|
## 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.
|
|
- When a subscription period has elapsed, user-facing quota is no longer shown as an active-cycle quota.
|
|
|
|
## Subscription period enforcement
|
|
- `currentPeriodEnd` is the hard end of paid access.
|
|
- At or after `currentPeriodEnd`, the runtime no longer treats the subscription as active.
|
|
- During generation access checks, an elapsed `active` subscription is transitioned to `expired` before access is denied.
|
|
- During account and billing reads, an elapsed `active` or `past_due` subscription is normalized to `expired` so the stored lifecycle is reflected consistently.
|
|
- There is no grace period after `currentPeriodEnd`.
|
|
|
|
## 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.
|
|
- Because provider-final status is not ingested automatically yet, the app currently relies on operator judgment when calling `mark-paid`.
|
|
- 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 is still incomplete on the invoice side because provider-driven expiry, cancelation, and reconciliation are not implemented yet.
|
|
|
|
## Required future direction
|
|
- Add provider callbacks or polling-based reconciliation.
|
|
- Persist provider-final status before activating access automatically.
|
|
- Reduce or remove the need for operator judgment in the normal payment-success path.
|
|
|
|
## 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`
|