Files
nroxy/docs/ops/payment-system.md
2026-03-10 17:30:35 +03:00

6.0 KiB

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