@devly-cl/billing-flow
v0.1.0
Published
Reusable native Flow billing adapter with durable host ports.
Readme
@devly-cl/billing-flow
Native Flow billing adapter for Node 24+. Depends only on the public APIs of @devly-cl/billing-core and @devly-cl/flow-client; emits CommonJS and TypeScript declarations and supports ESM and NodeNext consumers. Published releases are installed from npm; local npm pack only creates a candidate tarball.
Install
npm install @devly-cl/billing-core @devly-cl/flow-client @devly-cl/billing-flowIntegration
Construct FlowBillingClient with a wire FlowHttpClient and an optional beforeRequest(method, path) policy. It translates only FlowClientError into the shared core SubscriptionGatewayError. mutation(request, validate) marks validation failures after a successful request as uncertain. Preflight rejection remains definitive.
Construct FlowBillingProvider with FlowBillingProviderOptions: immutable environment and client; journal and authorized resource context; payment-method evidence port; reference factory; time-zone getter and optional clock; readiness/capability callbacks; callback and return-URL functions. Getters and policies are evaluated when used. The host must gate charge-producing POSTs immediately before dispatch while permitting reads, reconciliation and cancellation during a sales pause.
FlowOperationJournal owns atomic begin, get, complete, fail, and find. begin must distinguish environment/key/kind/tenant and a stable fingerprint of the full request. It returns false only for an identical existing intent; mismatches must throw. Persist the intent before the external call. The runner never repeats a POST for an existing intent: it returns the durable result or recovers from canonical provider reads. Store implementations own locks, request canonicalization, leases and timestamps. FlowResourceContext separately checks authorized checkout/subscription ownership and checkout eligibility. FlowOperationRunner receives only the journal.
Some Flow mutations have no safe automatic lookup after a lost response. A host may call resolveUncertainCustomer({tenantId,customerId}) only for the matching durable customer:{tenantId} intent; it confirms the supplied ID through /customer/get and requires the original external ID and email. A host may call resolveUncertainRegistration({tenantId,localOperationId,token,url}) only for the matching durable registration intent; it validates the Flow-hosted URL and /customer/getRegisterStatus, then uses the existing canonical card verification. Neither method issues a POST or creates a new key. The host must authorize the tenant and immutable Flow binding, show unresolved operation metadata without raw token/result data, and retain an operator audit trail.
FlowReferenceFactory supplies customer/plan IDs (including optional legacy plan IDs), fallback offer ID, registration token lookup hash and snapshot fingerprint. Preserve each historical algorithm when integrating existing data. Registration token lookup and callback receipt hashing are independent contracts. Snapshot fingerprinting must omit opaque.fingerprint itself and protect the remaining snapshot. No project name, environment variables or frontend routes are built in.
Plan economics are immutable at the adapter boundary. When a durable or recovered historical plan has matching economics but a different payment callback, the provider leaves that plan and its subscriptions intact, then creates or reuses a deterministic callback-scoped plan derived from the base ID and current callback URL. The journal makes this safe across retries and restarts. A changed amount, currency, interval, trial, or status is still a BILLING_FLOW_PLAN_MISMATCH; it is never hidden by a scoped plan. Existing subscribers retain their original callback, so hosts need a stable public callback ingress or periodic reconciliation for historical plans.
FlowPaymentMethodEvidencePort stores and reads verified card evidence without exposing persistence to the provider. A partial receipt must retain its tenant metadata even before card evidence is complete. The host owns storage JSON, authorization and the transaction used to display {brand,last4}.
Capabilities and callbacks
The provider implements checkout/registration, canonical invoice reconciliation, cancellation, recurring-term reads, and managed native plan changes. It advertises subscriptionCheckout and paymentMethodSetup, plus managedPlanChange when enabled by host policy. Generic recurring-price/interval mutation, one-off invoice, payment-method replacement and signed-body webhook capabilities are not advertised. FlowNativePlanChanges can also be composed directly with explicit journal runner, resource context, clock, timezone and snapshot fingerprint. Calendar helpers preserve Santiago DST ambiguity and leap-year handling; mapper helpers preserve recurring charges, credits and outside-payment evidence.
For recurring reconciliation and a paid plan adjustment, a Flow invoice is paid only when its invoice status, payment status, ownership and immutable dimensions all match. Flow documents error as a failed charge attempt, so error=1 does not overturn a later canonical payment.status=2; a missing, external, mismatched or unconfirmed payment remains pending or review according to the operation.
FlowCallbacks receives a durable queue, an environment-to-verifier resolver and idempotent invoice/wake effects. Receipt enqueue does no provider lookup. Each worker claim verifies canonical state before effects and completion; uncertain acknowledgement retries the same receipt. The queue owns raw-token digest, leases, retry scheduling and retention. HTTP routing, browser returns, commerce wake-up behavior and navigation belong to the host.
Public errors are the original core constructors; import them from @devly-cl/billing-core. Wire types remain available from @devly-cl/flow-client. Only the package root is exported. Legacy host paths remain compatibility facades during migration.
Integration example
The declared host ports below must be implemented by the integrating application. This is wiring, not an in-memory production journal or a charge example. Supply stable reference/fingerprint algorithms and durable storage before invoking operations.
import { FlowHttpClient, type FlowCredentials } from '@devly-cl/flow-client';
import { FlowBillingClient, FlowBillingProvider, type FlowBillingProviderOptions } from '@devly-cl/billing-flow';
declare const host: Omit<FlowBillingProviderOptions, 'environment' | 'client'>;
declare function getCredentials(): FlowCredentials;
declare function beforeRequest(method: 'GET' | 'POST', path: string): void;
const wire = new FlowHttpClient({ environment: 'test', getCredentials });
const client = new FlowBillingClient(wire, { beforeRequest });
const provider = new FlowBillingProvider({
environment: 'test',
client,
journal: host.journal,
context: host.context,
references: host.references,
paymentMethods: host.paymentMethods,
getTimeZone: () => host.getTimeZone(),
assertReady: () => host.assertReady(),
assertPlanChangesReady: () => host.assertPlanChangesReady(),
planChangesEnabled: () => host.planChangesEnabled(),
callbackUrl: (kind) => host.callbackUrl(kind),
returnUrl: () => host.returnUrl(),
now: host.now ? () => host.now!() : undefined,
});
void provider;beforeRequest must recheck the host's sales policy before charge-producing POSTs; account separately for preview, canonical reads and maintenance cancellation. callbackUrl supplies the integrating application's public callback endpoints, and returnUrl supplies its browser continuation. Do not embed customer tokens or secrets in those URLs. A pinned test provider must receive only test credentials.
Validation
Run npm run build:packages, npm run test:packages, npm run test:package-boundaries and npm run test:package-artifacts from this repository root. The isolated artifact consumer installs these three packages offline, intercepts signed requests, persists an uncertain operation to disk, restarts the process and recovers without another POST. It has no Nest, TypeORM or .env. Local contract tests do not demonstrate a real PSP transaction or production readiness of credentials.
After package changes, rebuild and restart Nest. Automatic package hot reload has not been validated.
