Status: implementation specification — 2026-07-16

Scope: HU2/Launchpass evidence and Boja's in-repo NMI/value.io billing service. HU2 is prior-art evidence only; Boja billing is a greenfield specification.

Executive Conclusion

HU2 does not use features/nmi/nmi.ts as its primary payment client. That file is a thin NMI query API used to recover transaction and order identifiers. The production transaction plane is Value.io: HU2 sends Value.io a vaulted card identifier, a destination/MID, an amount, NMI-shaped gateway options, and optional stored-credential or 3DS fields.

HU2 contains valuable operating knowledge:

HU2's persistence model is not a financial ledger. Boja should retain the business lessons while replacing that persistence and orchestration architecture. The proposed Boja service therefore has four deliberately separate records:

  1. immutable, balanced journal entries for money;
  2. payment intents and provider attempts for command execution;
  3. subscriptions, dunning attempts, refunds, and disputes for lifecycle state;
  4. a transactional outbox whose events drive the safe entitlement projection.

Part 1 — How HU2/Launchpass Works Today (NMI Paths Only)

1. Scope and Terminology

This study includes Titanium, Luqra EMS, and Luqra Inspire because their controllers inherit the NMI implementation. It includes Value.io because HU2 uses Value.io as the vault/payment orchestration layer in front of those destinations.

2. Current Architecture at a Glance

HU2 checkout client
  |
  | card data / client tokenization credentials from checkout manifest
  v
Value.io credit-card endpoint
  |
  | single-use or persistent Value.io card identifier
  v
HU2 payment-method API ------------------------------+
  |                                                   |
  | PaymentMethod.id == Value.io identifier           |
  v                                                   |
HU2 payment API                                       |
  |                                                   |
  +-> volume rule chooses Merchant/destination       |
  +-> BasePaymentController shared flow              |
  +-> Titanium/Luqra NMI subclass maps response      |
  |                                                   |
  v                                                   |
Value.io /payments                                    |
  | destination + vaulted card + sale/rebill/refund   |
  v                                                   |
NMI-adjacent gateway/MID                              |
  |                                                   |
  +-> synchronous Value.io response --> PaymentEntry |
  |                                                   |
  +-> NMI or Value.io webhook --> BullMQ ------------+
                                     |
                                     v
                              purchase/subscription
                              affiliate/TRW side effects

3. The NMI Client is a Query Client, Not the Transaction Plane

features/nmi/nmi.ts posts form-encoded requests to https://secure.nmi.com/api/query.php. It appends Merchant.gatewayProviderPrivateKey as security_key and returns the raw response text. retrieveByOrderId extracts a transaction identifier from XML with a regular expression. retrieveByTransactionId similarly extracts original_transaction_id and order_id.

The studied file exposes none of the following NMI operations: customer-vault create/update/delete; sale; authorization; capture; void; refund; gateway-managed recurring schedules; tokenization; 3DS authentication. It has no typed XML parser, timeout, retry policy, response-code taxonomy, or structured gateway error.

NMI Gateway.js appears separately. features/nmi/gateway.ts defines 3DS authentication options and results such as CAVV, ECI, authentication version, directory-server transaction ID, and ACS transaction ID, and loads https://<gatewayHost>/js/v1/Gateway.js in the browser. This is evidence of NMI 3DS integration, not evidence of NMI Collect.js card tokenization.

4. Value.io's Exact Role

Value.io is both the card-identifier boundary and the payment orchestration facade.

4.1 Card/vault surface

features/valueio/adminValueioApi.ts exposes list, create, retrieve, update, and delete operations for /credit_cards. The create response yields a Value.io identifier or single-use token. The checkout manifest supplies a Value.io username and write key to the client.

PCI risk: CVV storage

An alternate add-card route receives two opaque-looking values, decodes them into PAN and CVV, and calls Value.io itself. It then stores a reversibly encoded verification value. util/verificationCypher.ts explicitly implements encode/decode of CVVs rather than one-way destruction. That path materially expands PCI scope and stores sensitive authentication data in a form Boja must never reproduce.

4.2 Destination and payment orchestration surface

For a payment, HU2 posts to /payments with: data.kind (sale, rebill, or refund); destination (selected merchant); credit_card (Value.io vaulted identifier); decimal amount; action/passthrough metadata; gateway/stored-credential/3DS options; refunded_payment for refunds; and optional account-updater execution.

4.3 Authentication and error behavior

The Value.io request wrapper uses HTTP Basic authentication and JSON against https://api.value.io/v1. It parses an error body for HTTP status 300 or greater and throws an augmented error. It does not set a request timeout or implement a bounded transport retry policy. createAndParsePayment can perform one immediate callback-driven retry, but the recursive call does not forward shouldRetry, has no delay, and has no provider idempotency key.

4.4 Account updater and 3RI

For kind=rebill, HU2 may ask Value.io to run account updater if the method has not been checked for 27 days. ENABLE_3RI is hard-coded false. The controller still stores reusable rebill 3DS data — this is partially implemented capability, not a dependable production guarantee.

5. Controller Abstraction

BasePaymentController owns almost all behavior: payment-method lookup and fallbacks; Value.io destination lookup; sale, refund, and purchase persistence; initial checkout side effects; subscription rebill request construction; rebill error conversion; and recurring success side effects.

The NMI subclass varies three main concerns: which Merchant row/destination to use; how a Value.io response is unwrapped into the NMI response shape; and how to extract the underlying NMI transaction ID. LuqraEmsPaymentController and LuqraInspirePaymentController are effectively configuration subclasses of TitaniumPaymentController, overriding only getMerchantId. This design achieves reuse, but configuration has escaped into class identity.

6. Checkout Flow

1. Client fetches checkout manifest.
2. Manifest supplies product, merchant choice, and Value.io tokenization credentials.
3. Browser obtains a Value.io source/card identifier.
4. POST /api/payment-method receives source_id.
5. HU2 retrieves card metadata and stores PaymentMethod with the Value.io ID.
6. POST /api/payment selects the effective merchant/controller.
7. BasePaymentController posts a sale to Value.io /payments.
8. Value.io routes to the selected NMI-adjacent destination/MID.
9. Synchronous success creates PaymentEntry, Purchase, and subscription effects.
10. Delayed success is recovered through Value.io or NMI webhook handling.
11. Product hooks directly signal TRW and other downstream effects.
Success persistence is not atomic

createPurchase catches insertion errors and then looks up the existing purchase by ID — this is ad hoc duplicate recovery rather than an end-to-end idempotency contract. Recurring success updates the subscription, creates a purchase, records an affiliate sale, adds an action log, and writes a successful RebillAttempt in parallel — not in one PostgreSQL transaction. A crash can therefore produce a successful provider charge with only a subset of local effects.

7. Merchant and MID Routing

7.1 Checkout-volume selection

Merchant routing begins with VolumeRule rows: priority, probability, daily limit, country filters, and a merchant. The volume limiter orders matching rules, checks probability and country, and reserves a short Redis ticket before returning a rule. Successful sale counts are cached for ten minutes. This is probabilistic load allocation at checkout — not a general cascade that retries every decline across MIDs.

7.2 Effective merchant and rebill fallback

Rebilling first tries the subscription/default or last-successful merchant, then traverses each merchant's configured fallback list, retaining only routes compatible with the payment method. Stored-credential continuity matters: the rebill code finds an original successful sale and sends its transaction ID as network stored-credential evidence. If the gateway says the initial transaction is invalid, subscriptionRebillController retries once without that identifier.

8. NMI and Value.io Webhook Processing

8.1 Ingress and queueing

pages/api/webhooks/[name].ts accepts named webhooks behind the shared adminHandler token scheme and queues the raw body. The queue job is configured for three attempts with exponential backoff.

8.2 NMI event behavior

The NMI handler supports only transaction.refund.success and transaction.sale.success. The handler logs the body, sleeps four seconds, and invokes its inner run() without awaiting it. Its recursive retry is also not awaited. A BullMQ job can finish before the actual mutation, and failures can escape the queue's retry contract. There is no NMI signature/HMAC verification, no durable provider-event row, and no unique constraint on NMI event_id.

8.3 Value.io event behavior

features/stripe/handleValueioWebhook.ts finds a pending PaymentEntry by payment_identifier. Signature verification is a TODO/commented block. The handler is oriented to pending checkout completion; it is not a complete normalized provider-event projector for every rebill/refund/dispute lifecycle.

9. Refunds, Voids, and Disputes

BasePaymentController.refund loads the original Purchase, reverses affiliate credit before gateway success, and calls Value.io. The model provides no first-class partial-refund object or remaining-refundable calculation. The external refund and local state transition are not committed atomically.

There is no first-class dispute lifecycle. Purchase has disputed and disputeWon flags. Ethoca handlers parse loosely structured alert/email content and heuristically match a purchase by card/date details. The model does not capture provider dispute ID, disputed amount, reason code, evidence deadline, evidence submissions, won/lost transitions, processor debits, fee, reserve impact, or financial reversal.

10. Subscription Lifecycle

The repository's own AGENTS.md warns that status, isActive, and canceled_at are historically unreliable. Current code treats endedAt, cancelAtPeriodEnd, and currentPeriodEndAt as the more trustworthy access signals.

Cancellation normally sets a cancelled status and cancelAtPeriodEnd=true; it does not immediately set endedAt. There is no clean, first-class trialing lifecycle.

Upgrade quotes charge the target recurring amount minus the unused value of the current period. Downgrades are free. There is no immutable invoice, credit memo, proration line item, or ledger allocation explaining the change.

11. Rebilling and Dunning

The rebill finder runs every 60 seconds, finding four categories: first attempts for subscriptions whose effective due date passed; attempts whose nextActionAt is due; quick retries; and orphaned subscriptions with missing or stale attempt state.

The calendar schedule: choose the first business day after the billing/reference date; during the first 60 days, target business days after the 1st and 15th; after 60 days, target one monthly slot near the 1st or 15th; skip a candidate less than four hours away; use a pseudo-random time between 10:00 and 14:59 in a country-derived time zone.

Declines are classified by NMI response text and regular expressions. Outcomes include: retry later; require a new card; amend transaction data; blacklist/disable a method; stop a merchant route; terminate recovery. The trust decision uses roughly 180 days of history and 14-day recovery observations. It estimates recovery probability and expected net value using a 75-cent attempt cost.

16. Pain Points and Debt

  1. No financial ledger. Mutable flags and semi-structured payment rows cannot prove conservation of money.
  2. Value.io, not nmi.ts, is the actual transaction boundary.
  3. Webhook naming debt. NMI and Value.io handlers live under features/stripe/.
  4. No provider-event inbox. Events lack verified, unique, immutable ingestion rows.
  5. Weak authenticity. NMI uses a shared admin token and Value.io signature verification is a TODO.
  6. Async handler bug. handleNmiWebhook does not await its inner processing or recursive retry.
  7. Split-brain writes. Provider charge, PaymentEntry, Purchase, Subscription, RebillAttempt, affiliate, and entitlement effects are non-atomic.
  8. Repair as normal operation. An hourly job reconciles unattached rebill entries after split writes.
  9. Outbound ambiguity. Value.io calls show no stable idempotency key and have no timeout/reconcile-before-retry contract.
  10. Error swallowing. Value.io and internal refund paths can convert or suppress exceptions.
  11. Controller sprawl. Configuration-only NMI MIDs are subclasses and switch cases.
  12. Routing opacity. Rules, percentages, cache windows, Redis tickets, fallbacks, and compatibility live in different layers.
  13. Volume oversubscription. Reserved ticket count is not used in the capacity decision.
  14. Dynamic dunning is hard to audit. Attempt caps and dates emerge from live probability logic without policy versioning.
  15. Ad hoc subscription state. Status and timestamps can disagree.
  16. Grace is implicit. Access during dunning is an accident of endedAt, not an explicit entitlement policy.
  17. Refund semantics are incomplete. Full-only refund plus Boolean; access revocation is commented.
  18. No void/capture lifecycle.
  19. No dispute lifecycle. Heuristic alerts mutate flags without evidence/amount/deadline/ledger entries.
  20. PCI scope is excessive. One route decodes PAN/CVV server-side and stores recoverable CVV data.
  21. Tokenization cannot be fully proven from this repo.
  22. 3RI is modeled but disabled.
  23. Entitlement signaling is fragmented. Product hooks, legacy expiration, direct TRW calls, queues, and Redis events disagree.
  24. Expiration ordering is unsafe.
  25. No transactional outbox.
  26. Raw provider payload exposure.
  27. Money types are inconsistent. Purchase uses strings while PaymentEntry uses integer amounts.
  28. Dead and legacy paths remain live-looking.
  29. Secrets in source. Gateway/captcha/analytics credentials must not be code constants.
  30. Escrow top-up is fire-and-forget.

Part 2 — What Boja Takes Versus Leaves

HU2 mechanismDecisionBoja treatment and reason
NMI + custom TSYS production routeUseContracted processing economics behind one typed adapter.
Value.io vault/payment orchestrationKeep, formalizePreserve the known destination/vault/updater path, but make Value.io a provider adapter with strict request/response types, timeouts, idempotency, and reconciliation.
Direct NMI query APIKeep, expand carefullyRetain query/reconciliation; add typed XML parsing, redaction, timeout, retry classes, and explicit operation support.
Browser tokenizationKeep goal, revalidateUse NMI Collect.js or confirmed Value.io hosted fields/iframe so PAN/CVV go directly to the PCI provider. The HU2 add-card server path is forbidden.
NMI Gateway.js 3DSKeep behind adapterNormalize challenge/result fields and persist an attempt audit; do not scatter provider-specific field names through checkout code.
Value.io 3RI supportRedesign/defer flagEnable only after acquirer certification, test fixtures, and an explicit stored-credential policy.
VolumeRule priority/country/chance/capRedesignUse versioned routing policy, transactional/reservable capacity, deterministic selection, and a recorded decision trace.
Merchant fallback arraysKeep idea with guardrailsData-driven candidate graph; only retry safe/transient route failures, preserve stored-credential lineage, and prohibit blind MID laundering of hard declines.
Purchase mutable refund/dispute flagsDropReplace with refund/dispute aggregates and immutable journal reversals.
Shared-token webhook ingressDropVerify provider signatures/authenticity, preserve raw bytes/hash, rotate secrets, and record verification outcome.
BullMQ at-least-once processingKeep principle, not authorityA queue can wake workers; PostgreSQL inbox/outbox rows remain the durable source of truth.
Redis payment/purchase pubsubDrop as reliability layerUI notifications derive from committed outbox/projection events; Redis may remain an acceleration layer.
Calendar-aware dunningKeep ideaMake schedules typed, bounded, versioned, explainable, and independent from access grace.
Decline taxonomyKeep and curateConvert HU2's learned NMI strings/codes into versioned provider decline classes with fixture coverage.
Trust/expected-value retry decisionRedesign after v1Begin with explicit policies and caps; later add scored recovery only with policy snapshots, observability, and compliance review.
Value.io account updaterKeepRun proactively and during recovery; persist updater events and method lifecycle.
Implicit grace through endedAtDropStore grace policy/version and grace_ends_at; publish deterministic entitlement validity.
Product-specific TRW hooksDropEmit normalized billing facts; Boja's entitlement projector owns access.
Recoverable CVV storageDrop absolutelyCVV is never persisted after authorization, encrypted or otherwise.
Hard-coded secretsDrop absolutelyBilling service receives scoped secrets from the deployment secret manager and owns rotation.

Part 3 — Boja Billing Service Specification

17. Goals, Non-Goals, and Invariants

17.1 Goals

17.2 Non-goals for the first implementation

17.3 Hard invariants

  1. Every posted journal entry balances by currency: total debits equal total credits.
  2. Posted journal entries and postings are immutable; correction is a new reversing entry.
  3. One provider economic event produces at most one semantic journal entry per event type.
  4. One idempotency key cannot be reused with a different request fingerprint.
  5. No provider success can advance subscription state without its required ledger entry and outbox row in the same PostgreSQL transaction.
  6. No webhook body is processed until authenticity is recorded as verified under a named verification scheme.
  7. No PAN, full track data, PIN block, or CVV is stored or logged.
  8. Provider references and billing tables are server-only and excluded from Zero publication.
  9. Entitlement changes are derived from all active sources, never a single "expired" Boolean.
  10. Queue delivery can duplicate or disappear without losing the durable command/event record.
  11. External subscription-source adapters can supply verified events but can never write entitlements directly.
  12. The tax API is the only narrow non-Cloudflare production SaaS dependency and is isolated behind a typed adapter.

18. Ledger Architecture

18.1 Standard pattern

Boja follows the same core relational primitive described by Square's Books design: accounts/books, journal entries, and entries/postings. Journal entries are immutable and balanced so every unit moved is accounted for. Boja will implement this product subledger in PostgreSQL; it is not a replacement for the company's external general ledger. Finance exports map Boja accounts and entries to the accounting system of record.

18.2 Account taxonomy

Each ledger_account has one currency, owner/scope, account type, normal side, and purpose. At minimum the chart contains:

Counsel review required

Boja is the default merchant of record. Finance and counsel must confirm the contract, liability allocation, account ownership and seller/affiliate payout obligations before production money movement.

18.3 Example entries

Assume a $100.00 captured member sale, $5.00 tax, $15.00 platform fee, and $80.00 seller share:

Sale/capture JE
  Dr processor_clearing:NMI_MID_A          100.00
  Cr tax_payable:jurisdiction                5.00
  Cr server_owner_payable:server_123        80.00
  Cr platform_revenue                       15.00
                                             -----
  Debits = credits                          100.00

When NMI settles $97.10 after a $2.90 processor fee:

Settlement JE
  Dr cash:settlement_bank                   97.10
  Dr processor_fee_expense                   2.90
  Cr processor_clearing:NMI_MID_A          100.00

A $20 partial refund:

Refund JE
  Dr server_owner_payable:server_123        16.00
  Dr platform_revenue                        3.00
  Dr tax_payable:jurisdiction                1.00
  Cr processor_clearing:NMI_MID_A           20.00

For a chargeback:

Chargeback opened; seller bears loss
  Dr chargeback_receivable:server_123       100.00
  Cr processor_clearing:NMI_MID_A          100.00

Chargeback won
  Dr processor_clearing:NMI_MID_A          100.00
  Cr chargeback_receivable:server_123       100.00

18.4 Immutability and balance enforcement

Application roles receive no direct UPDATE or DELETE permission on posted entries/postings. All posting goes through one database function or tightly scoped repository transaction. The write locks/idempotency row, inserts the journal header and all postings, validates same-currency accounts, validates at least two postings, and checks equal debit/credit totals before commit. A deferred constraint trigger rejects unbalanced entries at transaction end.

19. Billing State Machines Around the Ledger

19.1 Payment intent

requires_payment_method
  -> requires_confirmation
  -> processing
  -> requires_action (3DS challenge)
  -> processing
  -> succeeded

Any pre-success state -> canceled
processing -> failed (terminal) or requires_payment_method (recoverable)

Every client create/confirm command supplies a stable nonce/idempotency key. The server stores a canonical request hash. On timeout, the attempt becomes unknown, and reconciliation queries Value.io/NMI before any new economic command is sent.

19.2 Provider transaction types

Provider transactions are typed as: tokenize or vault-reference attach; sale; authorize; capture; void; refund; rebill (merchant-initiated sale); chargeback/reversal notification; settlement/fee.

19.3 Subscription state machine

StateMeaningEntitlement default
activeCurrent period is paid or otherwise validGrant.
past_dueRenewal due and first customer-payment failure recordedGrant only through explicit grace.
dunningA versioned retry plan is activeGrant only through explicit grace.
canceledNo future renewal; current prepaid/grace access may remainGrant until access_through.
expiredAccess period/grace ended or terminal revocation policy appliedRevoke subscription-source grants.
active   -> past_due | canceled | expired
past_due -> active | dunning | canceled | expired
dunning  -> active | canceled | expired
canceled -> active (explicit resume before expiry) | expired
expired  -> active only through a new paid/reactivation command

cancel_at_period_end is an instruction, not a competing status. grace_ends_at is explicit and separate from retry horizon. The system may continue permitted recovery attempts after access has expired, but it cannot silently extend access merely because another attempt is scheduled.

19.4 Refund lifecycle

requested -> submitted -> pending -> succeeded
                    \-> failed
requested -> canceled (before provider submission only)

A refund has amount, currency, reason, actor, original capture, refundable remainder, provider ID, state, and journal entry. Partial refunds are first-class. Concurrent requests lock the original capture/refund aggregate so successful plus in-flight refund amounts cannot exceed captured amount.

19.5 Dispute lifecycle

warning -> opened -> needs_response -> submitted -> won | lost | withdrawn

The dispute records provider dispute ID, original transaction, amount/currency, reason, received/deadline timestamps, evidence metadata, liability policy, and related journal entries. Opening, fee assessment, provisional credit, win, and loss each produce their own idempotent economic entry when applicable.

20. In-House Dunning and Rebilling

20.1 Policy model

A dunning_policy is immutable once referenced and includes: policy version; decline class; attempt offsets/business-day rules; maximum customer-charge attempts; quick technical retry allowance; 27-day account-updater eligibility interval; customer reminder template/timing; access grace duration; terminal subscription action; time zone and deterministic jitter rule. Every renewal cycle snapshots the selected policy ID/version. Support can answer exactly why and when the next attempt will occur.

20.2 V1 baseline policy

The v1 policy encodes HU2's proven calendar and grace behavior as immutable typed data:

20.3 Attempt execution

The scheduler selects due rows with FOR UPDATE SKIP LOCKED. A unique (subscription_id, renewal_cycle_id, attempt_no, attempt_kind) constraint prevents duplicate jobs. Before a charge, the worker:

  1. locks and rechecks subscription/cycle state;
  2. confirms access/cancellation does not prohibit renewal;
  3. selects an active vaulted payment method;
  4. applies recent account-updater facts;
  5. evaluates data-driven route candidates;
  6. persists route decision and provider attempt;
  7. commits;
  8. calls the provider;
  9. records synchronous evidence, or leaves unknown for reconciliation;
  10. lets the billing-event projector commit money/state/outbox effects.

The initial customer-initiated transaction ID, MID, network indicators, and consent evidence are retained for merchant-initiated rebills. MID failover must comply with stored-credential/acquirer rules; it is not a generic retry trick.