import { ColumnDefinition } from '@voltro/database'; import { Context } from 'effect'; import { DataStore } from '@voltro/database'; import { default as default_2 } from 'stripe'; import { Effect } from 'effect'; import { IncomingRequest } from '@voltro/plugin-webhooks'; import { IncomingResponse } from '@voltro/plugin-webhooks'; import { IncomingWebhookDescriptor } from '@voltro/plugin-webhooks'; import { Layer } from 'effect'; import { mountIncomingWebhook } from '@voltro/plugin-webhooks'; import { RpcInterceptor } from '@voltro/protocol'; import { RpcKind } from '@voltro/protocol'; import { Schema } from 'effect'; import { Subject } from '@voltro/protocol'; import { TableIndex } from '@voltro/database'; import { TableLike } from '@voltro/database'; import { VoltroPlugin } from '@voltro/protocol'; export declare const BILLING_CUSTOMERS_TABLE = "_voltro_billing_customers"; export declare const BILLING_DUNNING_NOTICES_TABLE = "_voltro_billing_dunning_notices"; export declare const BILLING_FLUSH_CLAIMS_TABLE = "_voltro_billing_flush_claims"; export declare const BILLING_INVOICES_TABLE = "_voltro_billing_invoices"; export declare const BILLING_SUBSCRIPTIONS_TABLE = "_voltro_billing_subscriptions"; export declare const BILLING_USAGE_TABLE = "_voltro_billing_usage"; /** The webhook id + the path the doc + plugin mount at. */ export declare const BILLING_WEBHOOK_ID = "billing"; export declare const BILLING_WEBHOOK_PATH = "/billing/webhook"; /** * The shape every handler executor receives carries the resolved subject * under `ctx.request.subject` (mirrors `@voltro/plugin-rbac`'s * `RbacContext`). Kept structural so callers need no @voltro/runtime dep. */ export declare interface BillingContext { readonly request: { readonly subject: Subject; }; } /** tenant ↔ provider customer link. */ export declare const billingCustomersTable: BillingTable; /** * The sent-notice ledger. One row per `(tenant, episode, step)` — and the * UNIQUE over exactly those three is the send gate, not a report: a notice is * CLAIMED by `insertIgnore` before it is sent, so a duplicated webhook * delivery loses the race and sends nothing. */ export declare const billingDunningNoticesTable: TableLike & { readonly fields: Record>; readonly appliedIndexes: ReadonlyArray; }; /** * A billing failure. `transient: true` marks failures the service retries * (network blip talking to the provider); non-transient failures (a 4xx * from the provider, a misconfigured plan) are surfaced immediately. The * `billingPlugin` registers this via `errorSchemas` so it decodes TYPED on * the client rather than crossing as an untyped defect. */ export declare class BillingError extends BillingError_base { } declare const BillingError_base: Schema.TaggedErrorClass; } & { /** Where the failure originated — provider name or an internal stage. */ source: typeof Schema.String; message: typeof Schema.String; /** Retryable? The service retries `transient` failures on a Schedule. */ transient: typeof Schema.Boolean; }>; /** * The normalized event union a `BillingProvider.normalizeEvent` produces * from a verified, decoded provider webhook payload. `BillingService` * applies these to the DB rows — switching provider swaps the adapter; * the service + rows + entitlement engine are unchanged. */ export declare type BillingEvent = { readonly _tag: 'subscriptionUpserted'; readonly tenantId: string; readonly providerSubscriptionId: string; readonly plan: PlanId; readonly status: SubscriptionStatus; /** Seat quantity from the provider event; defaults to 1 when absent. */ readonly quantity?: number; readonly currentPeriodStart?: Date | null; readonly currentPeriodEnd: Date | null; readonly cancelAt: Date | null; /** When the PROVIDER emitted this — the out-of-order guard. See * `occurredAt` on the union below. */ readonly occurredAt: Date | null; } | { readonly _tag: 'subscriptionCanceled'; readonly tenantId: string; readonly providerSubscriptionId: string; readonly occurredAt: Date | null; } | { readonly _tag: 'invoicePaid'; readonly tenantId: string; readonly providerInvoiceId: string; readonly amountMinor: number; readonly currency: string; readonly occurredAt: Date | null; } | { readonly _tag: 'invoicePaymentFailed'; readonly tenantId: string; readonly providerInvoiceId: string; readonly amountMinor: number; readonly currency: string; readonly occurredAt: Date | null; } | { readonly _tag: 'customerLinked'; readonly tenantId: string; readonly providerCustomerId: string; readonly occurredAt: Date | null; }; /** The event tag union — handy for `onEvent` keys. */ export declare type BillingEventTag = BillingEvent['_tag']; /** One row per flush window — `windowKey` UNIQUE makes the INSERT the gate. */ export declare const billingFlushClaimsTable: TableLike; /** Invoice history (money as integer minor units). */ export declare const billingInvoicesTable: BillingTable; export declare const billingPlugin: (options?: BillingPluginOptions) => VoltroPlugin; export declare interface BillingPluginOptions { /** `'stripe' | 'mock'` (built from env) or a `BillingProvider`. Default: * `'stripe'` when `STRIPE_SECRET_KEY` is set, else `'mock'`. */ readonly provider?: BillingProviderName | BillingProvider; /** Provider api key — server-only. Default `STRIPE_SECRET_KEY` env. */ readonly apiKey?: string; /** Provider webhook signing secret (`whsec_…`). Default `STRIPE_WEBHOOK_SECRET` env. */ readonly webhookSecret?: string; /** The single source of tier→limit truth. Keyed by plan id. */ readonly plans?: Readonly>; /** Per-rpc-tag entitlement enforcement (the doc's fictional `guards:` * replacement) — installs the interceptor when present. */ readonly enforce?: EnforceMap; /** Usage-based billing autopilot (round-2/02): derive per-tenant usage from * the graph's own telemetry (rpc calls / row writes / AI tokens) and flush * to the provider on a schedule — zero hand-wired counters. Keyed by the * meter (entitlement/usage) key reported to the provider. */ readonly metering?: MeteringConfig; /** Autopilot flush cadence in ms (default 60_000). `0` disables the * self-scheduled flush — call `billing.flushUsage()` from your own * `*.cron.tsx` instead (e.g. for cluster-coordinated flushing). */ readonly flushIntervalMs?: number; /** * Dunning: the past-due notification sequence, the grace clock, and the * lockout. Composed on the PROVIDER'S outcomes — nothing here retries a * payment or schedules one; Stripe owns the retry cadence and each attempt * it makes is the tick that evaluates whichever steps have come due. * * Defaults: enabled, a 7-day grace, a 3-step sequence (0h / 72h / 144h), a * hard lockout — and NO transport, so until `notify` is wired the sequence * only claims + logs. Every number is overridable here and by env * (`VOLTRO_BILLING_GRACE_HOURS`, `VOLTRO_BILLING_DUNNING_STEP_HOURS`, * `VOLTRO_BILLING_LOCKOUT`, `VOLTRO_BILLING_DUNNING`). */ readonly dunning?: DunningConfig; /** Typed per-event side effects, run AFTER the row is updated. */ readonly onEvent?: OnEventMap; /** Transient-failure retries on provider calls. Default 3. */ readonly attempts?: number; /** * Checkout behaviour, handed straight to the provider. * * `adjustableQuantity` renders STRIPE's seat stepper on STRIPE's checkout * page — set it for a per-seat plan so a customer can buy the number of * seats they want in one go. `trialDays` makes Stripe run the trial and * report `trialing`; we neither count trial days nor expire them. */ readonly checkout?: { readonly adjustableQuantity?: { readonly min: number; readonly max: number; }; readonly trialDays?: number; }; /** * Per-tenant entitlement-limit override — the seam for a cloud-issued license * snapshot. Pass `@voltro/plugin-licensing`'s `entitlementResolver` here to let * a signed license decide limits per-tenant; a `null` result falls back to the * static `plans` registry. Absent → static registry only. */ readonly resolveEntitlementLimit?: (tenantId: string, key: string) => Effect.Effect; /** * Namespace for this plugin's rpc tags + inspect endpoints. Default `billing`. * * Set it when your app already publishes under that name — an exact tag * collision is fatal at codegen, and this is the way out. Orthogonal to * `name` below: `alias` REPLACES the namespace, `name` distinguishes two * installations within it. * * The dashboard panel follows: the plugin's inspect endpoints keep a mount * under its CANONICAL name alongside the aliased one, so aliasing does not * take the panel away. The one case it cannot cover is two installs of this * plugin — one canonical name, two panels — which get no shared mount at * all, on purpose. Read `inspectSlug` from `/_voltro/inspect/plugins` to * reach a specific install. */ readonly alias?: string; /** * Discriminator for a SECOND installation of this plugin, when one app runs * two (`@voltro/plugin-billing#analytics`). Not a rename — for that use * `alias`. */ readonly name?: string; } /** * Dumb adapter over a billing provider. Holds NO policy — checkout/portal * URL minting, usage push, and pure payload→event mapping only. The * `BillingService` wraps it with the entitlement engine, the DB rows, and * transient retry. * * `normalizeEvent` receives a payload whose signature was ALREADY verified * by @voltro/plugin-webhooks' mounter — this is pure mapping, never a * second signature check. */ export declare interface BillingProvider { readonly name: string; /** Whether the provider supports server-pushed metered usage. Stripe * does (subscription-item usage records); a flat-plan provider may not. * When false, `BillingService.reportUsage` records locally but never * calls `provider.reportUsage`. */ readonly supportsMeteredUsage: boolean; readonly createCheckoutSession: (input: CheckoutInput) => Effect.Effect<{ url: string; }, BillingError>; readonly createPortalSession: (input: PortalInput) => Effect.Effect<{ url: string; }, BillingError>; readonly reportUsage: (input: UsagePush) => Effect.Effect; /** * Apply a plan / seat change to the LIVE subscription. * * This is the call whose absence made `changeSeats` a lie: without it the * service patched its own table and the provider went on billing the old * quantity, so a customer could be granted seats nobody was charging for. * The provider — not us — computes and bills the proration. */ readonly updateSubscription: (input: SubscriptionUpdateInput) => Effect.Effect; /** * What WOULD a change cost, without applying it? Backed by the provider's * own invoice preview, so the number quoted to the customer is the number * they are charged — a locally-computed estimate can differ from the * provider's rounding, its tax, and its credit balance, and any difference * is a support ticket. */ readonly previewSubscriptionChange: (input: SubscriptionUpdateInput) => Effect.Effect; /** * The provider's CURRENT view of a subscription — a direct read, not an * event body. * * This is the method dunning refuses to lock a customer out without. An * `invoice.payment_failed` can arrive after the retry that succeeded, a * redelivery can arrive days late, and neither carries the state that * matters — the subscription's status right now does. Returns null when the * provider does not know the id. */ readonly fetchSubscription: (providerSubscriptionId: string) => Effect.Effect; /** * The billing contact the PROVIDER has on file, when it has one. Used as the * dunning recipient of last resort — `dunning.resolveRecipient` wins when * the app keeps its own. Optional: a provider may model no customer email. */ readonly customerEmail?: (providerCustomerId: string) => Effect.Effect; /** The provider's invoice history. We mirror it for listing but never * render an invoice ourselves — `hostedUrl` / `pdfUrl` are theirs. */ readonly listInvoices: (input: { readonly providerCustomerId: string; readonly limit?: number; }) => Effect.Effect, BillingError>; /** * Verify a webhook signature and decode the payload, using the provider's * OWN verification. Returns `null` when this provider does not do its own * verification (the mount's generic check then stands alone). */ readonly verifyWebhook?: (input: { readonly rawBody: Uint8Array; readonly signatureHeader: string; readonly secret: string; }) => Effect.Effect; /** Map a verified, decoded webhook payload to a `BillingEvent`, or * `null` for an event type this provider doesn't model (handler * no-ops, still 200). */ readonly normalizeEvent: (raw: unknown) => Effect.Effect; } export declare type BillingProviderName = 'stripe' | 'mock'; /** The billing service Tag — `const billing = yield* BillingService`. */ export declare class BillingService extends BillingService_base { } declare const BillingService_base: Context.TagClass; /** Wrap a built service in a `Layer` for the plugin's `services` field. */ export declare const billingServiceLayer: (service: BillingServiceShape) => Layer.Layer; export declare interface BillingServiceOptions { readonly provider: BillingProvider; readonly plans: PlanRegistry; /** Stores — memory by default; swapped to DataStore-backed via `bindDataStore`. */ readonly stores?: BillingStores; /** Transient-failure retries on provider calls. Default 3. */ readonly attempts?: number; /** Checkout behaviour handed straight to the provider — we render none of * it ourselves. */ readonly checkout?: { /** Seat stepper bounds on the provider's checkout page. */ readonly adjustableQuantity?: { readonly min: number; readonly max: number; }; /** Free-trial length. The provider runs the trial and reports `trialing`. */ readonly trialDays?: number; }; /** Clock injection for deterministic period + grace windowing in tests. */ readonly now?: () => Date; /** * Dunning policy, already resolved against env by `billingPlugin`. Omitted * → the documented defaults (7-day grace, 3-step sequence, hard lockout, no * transport, so the sequence logs and sends nothing). */ readonly dunning?: ResolvedDunningConfig; /** * Optional per-tenant entitlement-limit override — the seam for a * cloud-issued license snapshot (e.g. `@voltro/plugin-licensing`'s * `entitlementResolver`). Called before the static plan registry: a non-null * result is used as the limit for `(tenantId, key)`; `null` falls back to the * tenant's plan tier. Absent → behavior is exactly the static registry. */ readonly resolveEntitlementLimit?: (tenantId: string, key: string) => Effect.Effect; } export declare interface BillingServiceShape { /** Read the tenant's subscription row; null when none exists. */ readonly subscription: (tenantId: string) => Effect.Effect; /** Resolve the tenant's plan id; defaults to `'free'` when no row. */ readonly plan: (tenantId: string) => Effect.Effect; /** * "Is this tenant entitled right now?" — the one truthful answer, dunning * state included. A PURE read of the local row (no provider call, no write), * so it is safe on a hot path: the grace clock is a column and the lockout * is derived from it, never a flag some job had to remember to set. */ readonly entitlementStatus: (tenantId: string) => Effect.Effect; /** * Re-derive dunning state for a tenant FROM THE PROVIDER, then fire whatever * notification steps are now due (each at most once per episode). * * Runs automatically after every subscription/invoice event, which is what * makes the provider's own retry cadence the schedule. Call it directly only * to force a re-check (e.g. from a support tool). It is idempotent, it never * charges anything, and it never asks the provider to retry. */ readonly reconcileDunning: (tenantId: string) => Effect.Effect; /** * Reconcile every past-due tenant. Optional — the event path already covers * the normal case; a sweep exists so a step configured for a moment the * provider happens not to emit an event (and a reconcile missed during a * provider outage) still lands. Wire it from your own `*.cron.tsx` if you * want that; nothing schedules it for you. Returns the number reconciled. */ readonly dunningSweep: () => Effect.Effect; /** Pure read — would `cost` units of `key` be allowed? No mutation. */ readonly checkEntitlement: (tenantId: string, key: string, cost: number) => Effect.Effect; /** Check + decrement, atomically. Fails `EntitlementExceeded` over-limit. */ readonly consumeEntitlement: (tenantId: string, key: string, cost: number) => Effect.Effect; /** Record metered usage locally; flushed to the provider in batches. */ readonly reportUsage: (tenantId: string, key: string, qty: number) => Effect.Effect; /** Flush all pending local usage counters to the provider. */ readonly flushUsage: () => Effect.Effect; /** What a plan/seat change WOULD cost, from the provider's own invoice * preview. Quote this, not a local estimate — the two differ by the * provider's rounding, tax and credit balance. */ readonly previewChange: (tenantId: string, next: { readonly plan?: PlanId; readonly quantity?: number; }) => Effect.Effect; /** The provider's invoice history for this tenant. */ readonly invoices: (tenantId: string) => Effect.Effect, BillingError>; /** Mint a provider-hosted checkout URL for `plan`. */ readonly startCheckout: (input: { readonly tenantId: string; readonly plan: PlanId; readonly successUrl: string; readonly cancelUrl: string; }) => Effect.Effect<{ url: string; }, BillingError>; /** Mint a provider-hosted billing-portal URL. */ readonly portalUrl: (tenantId: string, returnUrl: string) => Effect.Effect<{ url: string; }, BillingError>; /** Apply a normalized provider event to the DB rows (idempotent upsert). */ readonly applyEvent: (event: BillingEvent) => Effect.Effect; /** * Change the tenant's plan mid-cycle. Computes a TIME-BASED prorated * settlement on the amount difference for the unused remainder of the * current period (positive = charge on upgrade, negative = credit on * downgrade), persists the new plan, and returns the settlement. A same-plan * call is a no-op with a zero delta. Fails `BillingError` when the tenant has * no subscription or the target plan is unknown. */ readonly changePlan: (tenantId: string, newPlan: PlanId, changeAt?: Date) => Effect.Effect; /** * Set the tenant's seat quantity mid-cycle. Prorates the amount delta * (`unitAmountMinor × Δquantity`) over the unused remainder of the period, * persists the new quantity, and returns the settlement. `quantity` must be a * positive integer. Fails `BillingError` on no subscription / bad quantity. */ readonly changeSeats: (tenantId: string, quantity: number, changeAt?: Date) => Effect.Effect; } export declare interface BillingStores { readonly customers: CustomerStore; readonly subscriptions: SubscriptionStore; readonly invoices: InvoiceStore; readonly usage: UsageStore; /** The dunning sent-notice ledger — the send gate, not a report. */ readonly dunningNotices: DunningNoticeStore; } /** One active subscription per tenant. */ export declare const billingSubscriptionsTable: BillingTable; /** * The slice of a built `Table` this package exposes. Annotated explicitly * because the full inferred `Table` type leaks @voltro/database's private * column-builder class across the package boundary (TS4094) — the public * surface only needs the table name, its columns, and its indexes. */ export declare interface BillingTable extends TableLike { readonly fields: Record>; readonly appliedIndexes: ReadonlyArray; } export declare const billingTables: () => ReadonlyArray; /** Per-tenant metered counters driving the entitlement engine. */ export declare const billingUsageTable: BillingTable; /** * The incoming-webhook descriptor for billing. `payload` is `Unknown` — * each provider's payload shape differs and `normalizeEvent` does the * narrowing; the signature + idempotency layers don't need a typed body. */ export declare const billingWebhookDescriptor: (options: BillingWebhookOptions) => IncomingWebhookDescriptor; declare interface BillingWebhookOptions { readonly provider: BillingProvider; readonly service: BillingServiceShape; readonly onEvent?: OnEventMap; /** The signing secret. It is BOTH the secret the mount's generic HMAC check * verifies with AND the one the provider's own verifier (Stripe's * `constructEvent`) uses. Null → the mount answers 503: an unverifiable * billing webhook must not reach subscription state. */ readonly webhookSecret?: string | null; } /** * Build the BillingService implementation. The interceptor + the plugin's * rpc routes share this single instance (so `enforce` and the rpc routes * see the same swapped-in stores). */ export declare const buildBillingService: (options: BillingServiceOptions) => { readonly service: BillingServiceShape; readonly holder: StoreHolder; }; /** * Build the interceptor for the `enforce` map. Before the executor runs, * for a matched rpc tag it consumes the rule's cost from the subject's * tenant — short-circuiting with `EntitlementExceeded` BEFORE the * executor when the quota is exhausted (the ratelimit precedent). * * The `service` is captured once at plugin-build time (the same memory/DB * service the `services` layer exposes), so the interceptor doesn't need * to resolve the Tag from a layer it isn't wired into. */ export declare const buildEnforceInterceptor: (enforce: EnforceMap, service: BillingServiceShape) => RpcInterceptor; /** Build the registry from `billingPlugin({ plans })`. */ export declare const buildPlanRegistry: (plans: Readonly>) => PlanRegistry; export declare interface CheckoutInput { readonly tenantId: string; /** Provider price id to subscribe to. */ readonly priceId: string; readonly successUrl: string; readonly cancelUrl: string; /** Optional existing provider customer id to attach the checkout to. */ readonly providerCustomerId?: string; /** Seats to buy up front. Defaults to 1. */ readonly quantity?: number; /** Let the buyer change the seat count on the checkout page itself. * Stripe renders the stepper; we neither build nor validate it. */ readonly adjustableQuantity?: { readonly min: number; readonly max: number; }; /** Days of free trial. Stripe runs the trial and emits `trialing`. */ readonly trialDays?: number; } /** * Try to claim the flush window. INSERT-wins: the first replica's row is the * winner; a racing replica's `insertIgnore` returns that same row, so it sees a * `claimedBy` that isn't its own and stands down. Returns true iff THIS replica * owns the window (→ it runs the flush). Fails OPEN on a store error (better a * possible double-flush — which markReported makes idempotent — than no flush). */ export declare const claimFlushWindow: (store: DataStore, windowKey: string, replicaId: string, nowMs: number) => Promise; declare interface CustomerRecord { readonly tenantId: string; readonly provider: string; readonly providerCustomerId: string; } export declare interface CustomerStore { readonly upsert: (record: CustomerRecord) => Effect.Effect; readonly getByTenant: (tenantId: string) => Effect.Effect; } export declare const dataStoreDunningNoticeStore: (store: DataStore) => DunningNoticeStore; export declare const dataStoreStores: (store: DataStore) => BillingStores; /** * The default sequence: on the first failure, again after 3 days, and a final * warning 24h before the default grace runs out. Every number here is a * default the framework picked on the app's behalf, so every one of them is * overridable in `app.config.ts` AND by env. */ export declare const DEFAULT_DUNNING_STEPS: ReadonlyArray; /** Default grace: 7 days from the first confirmed past-due. */ export declare const DEFAULT_GRACE_HOURS = 168; /** Default lockout once grace expires. */ export declare const DEFAULT_LOCKOUT: LockoutMode; /** The free/fallback plan id every app implicitly has. */ export declare const DEFAULT_PLAN: PlanId; /** * The default notice copy. Deliberately plain, unbranded, and link-light — * it exists so the sequence is usable the moment `notify` is wired, not so * apps ship it unchanged. Override by rendering from the structured fields in * a custom `notify`. */ export declare const defaultDunningEmail: (notice: { readonly stepId: string; readonly locked: boolean; readonly graceEndsAt: Date; readonly portalUrl: string | null; }) => { readonly subject: string; readonly html: string; readonly text: string; }; /** The steps whose `afterHours` have elapsed. Ledger claims — not this * function — decide which of them actually SEND. */ export declare const dueSteps: (steps: ReadonlyArray, pastDueSince: Date, now: Date) => ReadonlyArray; /** The synthetic step fired the first time an episode is observed LOCKED. */ export declare const DUNNING_LOCK_STEP_ID = "locked"; export declare interface DunningConfig { /** Master switch. Default true — but with no `notify` the sequence only * logs, so nothing is sent until the app wires a transport. */ readonly enabled?: boolean; /** Hours of grace from the first PROVIDER-CONFIRMED past-due. Default 168. */ readonly graceHours?: number; /** The sequence. Default `DEFAULT_DUNNING_STEPS`. */ readonly steps?: ReadonlyArray; /** Hard (limits fall to free) or soft (report only). Default `'hard'`. */ readonly lockout?: LockoutMode; /** * Where a notice goes. Absent → the sequence is inert (it logs, claims the * ledger, and sends nothing), which is the correct default for an * irreversible action the framework cannot address on the app's behalf. * `dunningMailNotifier(mail)` bridges to `@voltro/plugin-mail`. */ readonly notify?: (notice: DunningNotice) => Effect.Effect; /** * The billing contact for a tenant. Takes precedence over the provider's * customer email — an app that keeps its own billing contact should say so * rather than have Stripe's copy of an address win. */ readonly resolveRecipient?: (tenantId: string) => Effect.Effect; /** When set, each notice carries a freshly minted provider portal URL that * returns here — the one link a dunning email actually needs. */ readonly portalReturnUrl?: string; } /** The slice of `@voltro/plugin-mail`'s `MailService` a notice needs. Typed * STRUCTURALLY so this package carries no dependency on the mail plugin (the * `@voltro/plugin-auth` `mailSender` precedent). */ export declare interface DunningMailLike { readonly send: (message: { readonly to: string; readonly subject: string; readonly html: string; readonly text?: string; }) => Effect.Effect; } /** * Bridge `notify` to `@voltro/plugin-mail`: * * ```ts * const mail = yield* MailService * billingPlugin({ dunning: { notify: dunningMailNotifier(mail) } }) * ``` * * A notice with no resolvable recipient is skipped rather than failing — the * ledger already recorded the step, and a hard failure here would only mean * the webhook that triggered it gets retried into the same dead end. */ export declare const dunningMailNotifier: (mail: DunningMailLike) => ((notice: DunningNotice) => Effect.Effect); /** The rendered notice handed to `notify`. */ export declare interface DunningNotice { /** The step's id, or `'locked'` for the lockout notice. */ readonly stepId: string; readonly tenantId: string; /** Resolved billing contact, or null when neither `resolveRecipient` nor the * provider could name one. A null `to` is still delivered to `notify` — an * app may route it in-product rather than by email. */ readonly to: string | null; /** The plan the tenant is SUBSCRIBED to (not the locked-down effective one). */ readonly plan: PlanId; readonly status: SubscriptionStatus; readonly pastDueSince: Date; readonly graceEndsAt: Date; /** True once grace has run out — i.e. this is (or follows) the lockout. */ readonly locked: boolean; readonly lockout: LockoutMode; /** Provider-hosted billing portal, when `portalReturnUrl` is configured. */ readonly portalUrl: string | null; /** Ready-to-send default copy. A custom `notify` may ignore all three. */ readonly subject: string; readonly html: string; readonly text: string; } export declare interface DunningNoticeRow { readonly tenantId: string; readonly episode: string; readonly stepId: string; readonly sentAt: Date | null; } export declare interface DunningNoticeStore { /** * Claim `(tenant, episode, step)`. `true` means THIS caller won and must * send; `false` means someone already claimed it — a duplicate delivery, a * second replica, or a re-reconcile. * * The claim happens BEFORE the send, deliberately. The failure mode of * claim-then-send is a notice that is claimed but never delivered (one * missing email); the failure mode of send-then-claim is a customer * receiving the same dunning email twice. Only one of those is a real-world * harm, so the ordering is not arbitrary. */ readonly claim: (tenantId: string, episode: string, stepId: string, at: Date) => Effect.Effect; /** Steps already claimed for an episode — assertion + inspect surface. */ readonly sentSteps: (tenantId: string, episode: string) => Effect.Effect, BillingError>; } /** * One step of the sequence. `afterHours` is measured from `pastDueSince` — * NOT from the previous step and NOT from a retry attempt, so a step cannot be * pulled forward or pushed back by how often the provider happens to retry. */ export declare interface DunningStep { /** Stable ledger key. Changing it re-opens the step for every live episode. */ readonly id: string; /** Hours after the tenant went past-due at which this step becomes due. */ readonly afterHours: number; } /** A registry with a single zero-quota `free` plan — the zero-config default. */ export declare const emptyPlanRegistry: () => PlanRegistry; export declare type EnforceMap = Readonly>; /** One per-tag enforcement rule from `billingPlugin({ enforce })`. */ export declare interface EnforceRule { /** The entitlement key the tag consumes. */ readonly entitlement: string; /** Units charged per call. Default 1. */ readonly cost?: number; } /** The result of a pure (non-mutating) entitlement check. */ export declare interface EntitlementDecision { /** Whether the call would be allowed. */ readonly allowed: boolean; /** The entitlement key checked. */ readonly entitlement: string; /** The static limit for the caller's plan (`Infinity` for `'unlimited'`). */ readonly limit: number; /** How much is already used in the current window. */ readonly used: number; /** The cost the call would charge. */ readonly cost: number; } /** * Raised when a caller has exhausted an entitlement (quota). Distinct from * the scope/permission `Forbidden` / `ScopeError` — those gate "may you * call this proc"; this gates "do you have quota left". Both axes compose. * `billingPlugin` registers it via `errorSchemas` so callers can * pattern-match on `_tag === 'EntitlementExceeded'`. */ export declare class EntitlementExceeded extends EntitlementExceeded_base { } declare const EntitlementExceeded_base: Schema.TaggedErrorClass; } & { /** The entitlement key that was exhausted (`'aiCalls'`). */ entitlement: typeof Schema.String; /** The static limit for the caller's plan. */ limit: typeof Schema.Number; /** How much was already used in the current window. */ used: typeof Schema.Number; /** The cost the rejected call would have charged. */ cost: typeof Schema.Number; }>; /** An entitlement limit: a finite quota or the unbounded `'unlimited'`. */ export declare type EntitlementLimit = number | 'unlimited'; /** The truthful "is this tenant entitled right now" answer. */ export declare interface EntitlementStatus { readonly tenantId: string; /** The plan whose LIMITS apply right now — `'free'` under a hard lockout. */ readonly plan: PlanId; /** The plan the tenant is subscribed to, lockout or not. */ readonly billedPlan: PlanId; readonly status: SubscriptionStatus | 'none'; /** * False ONLY when dunning has locked this tenant out. A canceled * subscription is not a lockout — it is simply the free tier, and reporting * it as "not entitled" would conflate "never paid" with "stopped paying". */ readonly entitled: boolean; /** Past due, but still inside the grace window. */ readonly inGrace: boolean; /** When grace runs out; null when the tenant is not past due — or when the * provider has not confirmed the past-due yet, in which case the tenant is * in grace with no expiry rather than locked. */ readonly graceEndsAt: Date | null; /** Non-null iff locked out — the instant grace expired. */ readonly lockedSince: Date | null; readonly lockout: LockoutMode; } /** The episode a `pastDueSince` identifies. Recovery clears the column, so the * next failure necessarily produces a different key. */ export declare const episodeKeyOf: (pastDueSince: Date) => string; /** * Derive the entitlement state from the subscription row. PURE — no I/O, no * writes, so every read path can call it on the hot path. * * The one subtlety worth stating out loud: a `pastDue` row whose * `pastDueSince` is null reports as IN GRACE, never locked. `pastDueSince` is * written only after the provider itself confirmed the past-due (see * `service.ts`'s `reconcileDunning`), so a `pastDue` status that arrived by * webhook alone — or one whose reconcile failed against an unreachable * provider — can never lock a customer out. Locking on unverified event data * is the one failure mode here with a real-world cost. */ export declare const evaluateBillingEntitlement: (sub: Subscription | null, config: Pick, now: Date, tenantId?: string) => EntitlementStatus; /** * Pure entitlement decision. `limit` is `Infinity` for an `'unlimited'` * plan. `'unlimited'` and any non-positive cost always allow without * touching the counter; otherwise allow iff `used + cost <= limit`. */ export declare const evaluateEntitlement: (entitlement: string, limit: number, used: number, cost: number) => EntitlementDecision; /** The window a `now` falls in, bucketed by the flush interval. Two replicas * flushing within the same interval land on the same key → one wins. */ export declare const flushWindowKey: (nowMs: number, intervalMs: number) => string; /** When the grace for an episode runs out. */ export declare const graceEndOf: (pastDueSince: Date, graceHours: number) => Date; declare interface InvoiceRecord { readonly providerInvoiceId: string; readonly amountMinor: number; readonly currency: string; readonly status: string; readonly createdAt: Date | null; /** Provider-hosted invoice page — we never render one ourselves. */ readonly hostedUrl: string | null; readonly pdfUrl: string | null; } declare interface InvoiceRecord_2 { readonly tenantId: string; readonly provider: string; readonly providerInvoiceId: string; readonly amountMinor: number; readonly currency: string; /** 'paid' | 'open' | 'uncollectible' | 'void'. */ readonly status: string; /** Provider-event timestamp this `status` came from; null when undated. */ readonly statusEventAt: Date | null; } export declare interface InvoiceStore { /** * Upsert by `providerInvoiceId` (idempotent under webhook replay), with the * same stale-event guard the subscription row carries: a late-delivered * `payment_failed` must not flip an invoice that has since been paid back to * `'open'`, which is what a naive last-write-wins mirror does under Stripe's * unordered delivery. */ readonly upsert: (invoice: InvoiceRecord_2) => Effect.Effect; readonly listByTenant: (tenantId: string) => Effect.Effect, BillingError>; } /** * Is `incoming` older than what the row already reflects? Undated events never * displace a dated one, and a dated event always wins over an undated row — * that ordering is what makes a hand-built (undated) payload usable in tests * without letting it stomp production ordering. */ export declare const isStaleStatusEvent: (existing: Date | null, incoming: Date | null) => boolean; /** * What "locked" means once the grace period is over. * * - `'hard'` — entitlement limits fall to the `'free'` plan's. The app needs * no new code: every existing `requireEntitlement` / `enforce` check * starts answering with the free tier's numbers. * - `'soft'` — limits stay on the paid plan; only `entitlementStatus()` * reports the lockout, so the app decides what to withhold. */ export declare type LockoutMode = 'hard' | 'soft'; export declare const memoryDunningNoticeStore: () => DunningNoticeStore; export declare const memoryStores: () => BillingStores; /** `meterKey → source`. The meterKey is the entitlement/usage key reported to * the provider (`billing.reportUsage(tenant, meterKey, qty)`). */ declare type MeteringConfig = Readonly>; /** One meter's source — where its usage is derived from. */ declare type MeterSource = /** Count rpc calls whose tag matches (string = exact, RegExp = test). Optional * `kind` restricts to mutation/query/action. */ { readonly from: 'rpc'; readonly match: string | RegExp; readonly kind?: RpcKind; } /** Count row writes to `table` (default op `insert`). */ | { readonly from: 'cdc'; readonly table: string; readonly op?: 'insert' | 'update' | 'delete'; } /** Sum the AI usage ledger — `tokens` (input+output, default) or `costMicroUsd`. */ | { readonly from: 'ai'; readonly metric?: 'tokens' | 'costMicroUsd'; }; /** * In-memory provider with deterministic outputs. Checkout/portal URLs are * stable functions of the input so tests can assert on them; usage pushes * are recorded on `pushed` for assertion. `normalizeEvent` accepts the * already-normalized `BillingEvent` shape directly (the test feeds events * straight through) OR a `{ type, data }` envelope mirroring the Stripe * mapping, so the same webhook path exercises the mock end-to-end. */ export declare interface MockProvider extends BillingProvider { /** Recorded usage pushes — assertion surface for tests. */ readonly pushed: ReadonlyArray; /** Recorded subscription updates. The point of asserting on these is that * a change which never reaches the provider is a change the customer is * not billed for — the exact defect this contract was widened to fix. */ readonly updates: ReadonlyArray; /** * Set what the provider will report for `fetchSubscription`. This is the * mock's most load-bearing seam: dunning refuses to act on an event body and * reads the provider instead, so a test that wants "the retry actually * succeeded" says so HERE, not by crafting a webhook. */ readonly setSubscription: (providerSubscriptionId: string, state: ProviderSubscriptionState | null) => void; /** Set the customer email `customerEmail` will report. */ readonly setCustomerEmail: (providerCustomerId: string, email: string | null) => void; /** Every `fetchSubscription` id asked for — proves the reconcile happened. */ readonly fetched: ReadonlyArray; } export declare const mockProvider: (options?: { readonly baseUrl?: string; readonly supportsMeteredUsage?: boolean; }) => MockProvider; /** A monetary amount: integer minor units + ISO-4217 currency. */ export declare interface Money { /** Integer minor units (cents). Never a float. */ readonly amountMinor: number; /** ISO-4217 currency code, lowercased (`'usd'`, `'eur'`). */ readonly currency: string; } /** * Build the request handler for the billing webhook. The returned function * takes the transport-agnostic `{ method, headers, rawBody }` triple and * returns the typed `IncomingResponse`. */ export declare const mountBillingWebhook: (options: MountBillingWebhookOptions) => (request: IncomingRequest) => Promise; declare interface MountBillingWebhookOptions extends BillingWebhookOptions { /** The provider webhook signing secret (`whsec_…`). Null does NOT skip * verification — every delivery answers 503 until it is configured. */ readonly webhookSecret: string | null; /** Optional idempotency-cache override (tests pass a fresh cache). */ readonly idempotencyCache?: Parameters[1]['idempotencyCache']; } /** * Map a verified Stripe event to a `BillingEvent`, or null for a type we do * not model (the handler no-ops and still answers 200, which is what stops * Stripe retrying an event we will never care about). */ export declare const normalizeStripeEvent: (event: default_2.Event, resolvePrice?: (priceId: string) => string | null) => BillingEvent | null; /** Per-event side effect, run AFTER the row is updated. */ export declare type OnEventMap = Readonly Effect.Effect>>; /** Calendar-month window key (`'YYYY-MM'`) for metered counters. */ export declare const periodKey: (now?: Date) => string; /** One plan/tier: its entitlement limits + (for paid plans) a provider price id. */ export declare interface PlanConfig { /** Provider price id (Stripe `price_…`). Absent for free/zero-cost plans. */ readonly priceId?: string; /** Per-entitlement-key limits. Values are `number | 'unlimited'`. */ readonly entitlements: Readonly>; /** The plan's per-period price in INTEGER minor units (cents) — the unit * amount for ONE seat. Multiplied by the subscription's seat `quantity` to * get the billed amount, and drives mid-cycle proration. Absent for free * plans (treated as 0). Never a float. */ readonly unitAmountMinor?: number; /** ISO-4217 currency for `unitAmountMinor`, lowercased (`'usd'`). Defaults * to `'usd'` when a `unitAmountMinor` is set without one. */ readonly currency?: string; } /** Plan id (the key in `billingPlugin({ plans })`). */ export declare type PlanId = string; export declare interface PlanRegistry { /** Whether `plan` is a known plan id. */ readonly has: (plan: PlanId) => boolean; /** * The static limit for `(plan, key)`. `Infinity` for `'unlimited'`, `0` * for an undeclared key. When `plan` is unknown the registry FAILS * CLOSED — returns `0` — never silently granting an unknown plan's quota. * So a redeploy that drops a live plan id DENIES that tenant's quota * rather than falling back to the free-tier limit. */ readonly entitlementLimit: (plan: PlanId, key: string) => number; /** The plan id whose `priceId` matches, or null. */ readonly planForPriceId: (priceId: string) => PlanId | null; /** The provider price id for a plan, or null (free plans have none). */ readonly priceIdFor: (plan: PlanId) => string | null; /** The per-seat unit amount in integer minor units for a plan; `0` for a * free/undeclared plan. Multiplied by seat quantity for the billed amount. */ readonly unitAmountFor: (plan: PlanId) => number; /** The ISO-4217 currency for a plan's price, lowercased; `'usd'` default. */ readonly currencyFor: (plan: PlanId) => string; /** All declared plan ids. */ readonly planIds: () => ReadonlyArray; } export declare interface PortalInput { readonly tenantId: string; /** Provider customer id whose billing portal to open. */ readonly providerCustomerId: string; readonly returnUrl: string; } /** The provider's CURRENT view of a subscription — read directly, not from an * event body. `reconcileDunning` asks for this before it will lock anyone * out. */ export declare interface ProviderSubscriptionState { readonly status: SubscriptionStatus; /** Null when the provider cannot map the price to one of our plans. */ readonly plan: PlanId | null; readonly quantity: number; readonly currentPeriodStart: Date | null; readonly currentPeriodEnd: Date | null; readonly cancelAt: Date | null; } /** * In-handler DUNNING guard: refuse when the tenant's grace period has run out. * * Orthogonal to `requireEntitlement`, which answers "do you have quota left". * This answers "did you pay". A tenant with plenty of quota can still be locked * out, and a tenant in grace is NOT locked out — that is the whole point of the * grace window. * * Under `lockout: 'hard'` the quota guards already degrade to the free plan's * limits on their own, so reach for this when a feature has no numeric quota to * degrade (an export, a webhook target, an admin action). * * ```ts * export default (input, ctx) => Effect.gen(function* () { * yield* requireEntitled(ctx) * // … * }) * ``` */ export declare const requireEntitled: (ctx: BillingContext) => Effect.Effect; /** * In-handler entitlement guard. Resolves `ctx.request.subject.tenantId`, * consumes `cost` units of `key`, and fails with the typed * `EntitlementExceeded` when the quota is exhausted. Effect-native. * * ```ts * export default (input, ctx) => Effect.gen(function* () { * yield* requireEntitlement(ctx, 'aiCalls', Math.ceil(input.tokens / 1000)) * // … * }) * ``` * * Reads `BillingService` from the per-request layer — declare it in the * effect's R channel (it's provided by `billingPlugin`'s `services`). */ export declare const requireEntitlement: (ctx: BillingContext, key: string, cost: number) => Effect.Effect; /** A `DunningConfig` with every default resolved and every number concrete. */ export declare interface ResolvedDunningConfig { readonly enabled: boolean; readonly graceHours: number; readonly steps: ReadonlyArray; readonly lockout: LockoutMode; readonly notify?: (notice: DunningNotice) => Effect.Effect; readonly resolveRecipient?: (tenantId: string) => Effect.Effect; readonly portalReturnUrl?: string; } /** * Resolve the config against `app.config.ts` first and env second. * * Env overrides exist because these numbers gate money-adjacent behaviour and * an operator must be able to widen the grace on a live incident without a * redeploy. They FAIL LOUD rather than falling back to a default: a typo'd * `VOLTRO_BILLING_GRACE_HOURS` that silently means "7 days" is precisely the * kind of quiet wrong number this repo has been bitten by. */ export declare const resolveDunningConfig: (config: DunningConfig | undefined, env: NodeJS.ProcessEnv) => ResolvedDunningConfig; /** * Resolve `options.provider` to a concrete `BillingProvider`. A passed * object is used verbatim. `'stripe'` needs an api key (option or * `STRIPE_SECRET_KEY`). `'mock'` (and the absent default) is the * zero-config in-memory provider. */ export declare const resolveProvider: (input: ResolveProviderInput) => BillingProvider; declare interface ResolveProviderInput { readonly provider?: BillingProviderName | BillingProvider; readonly apiKey?: string; readonly env: NodeJS.ProcessEnv; /** Price id → plan id, from the plugin's plan registry. */ readonly planForPriceId?: (priceId: string) => string | null; /** Where the mock-by-absence warning goes. Optional: a library caller * resolving a provider outside a boot has nowhere to put it, and a missing * logger must not be a reason to fail. */ readonly log?: { readonly warn: (message: string) => void; }; } /** * A mutable holder so `bindDataStore` can swap the stores AFTER the service * layer is built. The service reads `holder.stores` on every call rather * than capturing a snapshot — identical to how storage rebinds its ref * store once the DataStore exists. */ declare interface StoreHolder { stores: BillingStores; } export declare const stripeProvider: (options: StripeProviderOptions) => BillingProvider; export declare interface StripeProviderOptions { readonly apiKey: string; /** Override metered-usage support (Stripe supports it by default). */ readonly supportsMeteredUsage?: boolean; /** * Map a Stripe price id to one of our plan ids. * * Without it, a subscription is only recognised when it carries * `metadata.plan` — which our own checkout sets, but a subscription created * in the Stripe Dashboard, by a sales-led flow, or by a migration does NOT. * Those events were previously dropped on the floor and the customer stayed * on `free` while paying. The plugin passes its plan registry here. */ readonly planForPriceId?: (priceId: string) => string | null; /** Injected in tests. */ readonly client?: default_2; } /** A tenant's subscription row, normalized across providers. */ export declare interface Subscription { readonly tenantId: string; readonly provider: string; readonly providerSubscriptionId: string; readonly plan: PlanId; readonly status: SubscriptionStatus; /** Per-seat quantity — the billed amount is `plan.unitAmountMinor × quantity`. * Defaults to 1 for a single-seat subscription. */ readonly quantity: number; /** Start of the current paid period; null for plans without a period. * Anchors mid-cycle proration (the "used" side of the period). */ readonly currentPeriodStart: Date | null; /** When the current paid period ends; null for plans without a period. */ readonly currentPeriodEnd: Date | null; /** When the subscription is scheduled to cancel; null if not scheduled. */ readonly cancelAt: Date | null; /** * When this tenant went past due — the dunning grace clock, and the identity * of the current dunning episode. * * Written ONLY by a provider-confirmed reconcile, never straight off a * webhook: it is the value the lockout is derived from, and locking a * customer out on an event body alone is the one mistake here with a * real-world cost. Cleared the moment the provider reports the * subscription healthy again, which is also what cancels the rest of the * notification sequence. */ readonly pastDueSince: Date | null; /** * The provider-event timestamp the current `status` came from — the * out-of-order guard. Provider webhooks are at-least-once AND unordered, so * an event that is older than what the row already reflects is DROPPED * rather than applied (a stale `active` must not un-do a `pastDue`, and a * stale `pastDue` must not resurrect a resolved one). Null on rows written * before any dated event. */ readonly statusEventAt: Date | null; } /** The outcome of a mid-cycle plan/seat change — the prorated settlement. */ export declare interface SubscriptionChange { readonly tenantId: string; readonly plan: PlanId; readonly quantity: number; /** Integer minor-unit settlement: > 0 charge, < 0 credit, 0 at boundary. */ readonly prorationMinor: number; readonly currency: string; } /** What a provider reports back after (or before) a subscription change. */ declare interface SubscriptionChangeResult { readonly plan: PlanId; readonly quantity: number; /** The proration the PROVIDER computed — and, unless `proration: 'none'`, * will actually bill. Positive = owed, negative = credited. */ readonly prorationMinor: number; readonly currency: string; readonly currentPeriodStart: Date | null; readonly currentPeriodEnd: Date | null; } /** * Raised when dunning has locked a tenant out — the grace period after a * provider-confirmed past-due has expired. Distinct from * `EntitlementExceeded`: that one means "you used your quota", this one means * "you did not pay". Registered via `errorSchemas`, so a client can branch on * `_tag === 'SubscriptionLocked'` and route the user to the billing portal * rather than showing a generic failure. * * Dates cross the wire as ISO strings — a `Schema.DateFromSelf` would not * survive the JSON boundary the error union is encoded through. */ export declare class SubscriptionLocked extends SubscriptionLocked_base { } declare const SubscriptionLocked_base: Schema.TaggedErrorClass; } & { tenantId: typeof Schema.String; /** The subscription status at the time of the refusal (`'pastDue'`). */ status: typeof Schema.String; /** ISO timestamp at which the grace period expired. */ lockedSince: typeof Schema.String; /** `'hard'` (limits fell to the free plan) or `'soft'` (report only). */ lockout: typeof Schema.String; }>; /** Fields a mid-cycle / reconcile patch may set on the tenant's row. */ export declare interface SubscriptionPatch { readonly plan?: PlanId; readonly quantity?: number; readonly status?: SubscriptionStatus; readonly currentPeriodStart?: Date | null; readonly currentPeriodEnd?: Date | null; /** The dunning grace clock. `null` clears it (recovery). */ readonly pastDueSince?: Date | null; /** Provider-event (or direct-read) timestamp the `status` came from. */ readonly statusEventAt?: Date | null; } export declare type SubscriptionStatus = 'active' | 'trialing' | 'pastDue' | 'canceled' | 'incomplete'; export declare interface SubscriptionStore { /** * Upsert by `providerSubscriptionId` — the webhook's idempotency anchor. * * STALE-EVENT GUARD: when the incoming `statusEventAt` is OLDER than the one * already on the row, the whole upsert is skipped. Provider webhooks are * at-least-once and unordered, so without this a redelivered `active` from * before a decline silently un-does the past-due (and with it the dunning * clock the lockout is derived from). `pastDueSince` is never written here — * only a provider-confirmed reconcile sets it. */ readonly upsert: (sub: Subscription) => Effect.Effect; readonly getByTenant: (tenantId: string) => Effect.Effect; /** Every subscription in a given status — the dunning sweep's only query. */ readonly listByStatus: (status: SubscriptionStatus) => Effect.Effect, BillingError>; /** Mark a subscription canceled by provider id (no-op if absent). */ readonly markCanceled: (providerSubscriptionId: string) => Effect.Effect; /** Apply a mid-cycle plan/seat/status change to the tenant's row (no-op if * absent). Only the provided fields are patched. */ readonly patchByTenant: (tenantId: string, patch: SubscriptionPatch) => Effect.Effect; /** Set a subscription's status by provider id (no-op if absent). */ readonly setStatus: (providerSubscriptionId: string, status: SubscriptionStatus) => Effect.Effect; } /** Apply a plan / seat change to the LIVE subscription at the provider. */ declare interface SubscriptionUpdateInput { readonly providerSubscriptionId: string; /** New price to move to. Omit to keep the current one. */ readonly priceId?: string; /** New seat quantity. Omit to keep the current one. */ readonly quantity?: number; /** * How the provider should handle the mid-period money. * * - `'prorate'` — bill or credit the difference for the remaining period * (Stripe's `create_prorations`). The default, and the only option that * charges what the customer actually used. * - `'none'` — change takes effect with no adjustment. * - `'always_invoice'` — prorate AND invoice immediately rather than at * the next cycle. */ readonly proration?: 'prorate' | 'none' | 'always_invoice'; } export declare interface UsagePush { readonly tenantId: string; readonly entitlementKey: string; /** Aggregate quantity to report for the current period. */ readonly quantity: number; /** End of the period the usage falls in (provider timestamp anchor). */ readonly periodEnd?: Date; } declare interface UsageRow { readonly tenantId: string; readonly entitlementKey: string; readonly period: string; readonly used: number; readonly reportedToProvider: number; } export declare interface UsageStore { /** Current used count for `(tenant, key, period)`; 0 when no row. */ readonly used: (tenantId: string, key: string, period: string) => Effect.Effect; /** * Atomic check-and-increment — the entitlement quota gate. In ONE * critical section: read the current `used`, and increment by `cost` * IFF `used + cost <= limit`. Returns the decision; `allowed:false` * means NOTHING was incremented. This is the fix for the read-then-write * race the service layer had — the check and the increment can no longer * interleave with a concurrent caller (memory: one sync tick; DataStore: * a compare-and-set loop so it's safe across instances, not just one * process). `limit` is finite and `cost > 0` by the time this is called * (the service short-circuits unlimited / non-positive cost). */ readonly consume: (tenantId: string, key: string, period: string, cost: number, limit: number) => Effect.Effect; /** Add `delta` to the used count, returning the new total. */ readonly increment: (tenantId: string, key: string, period: string, delta: number) => Effect.Effect; /** All rows with un-flushed usage (`used > reportedToProvider`). */ readonly pending: () => Effect.Effect, BillingError>; /** Mark `(tenant, key, period)` flushed up to `used`. */ readonly markReported: (tenantId: string, key: string, period: string, reported: number) => Effect.Effect; } export { VoltroPlugin } export { }