@voltro/plugin-billing 0.81.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ import { IncomingResponse } from '@voltro/plugin-webhooks';
8
8
  import { IncomingWebhookDescriptor } from '@voltro/plugin-webhooks';
9
9
  import { Layer } from 'effect';
10
10
  import { mountIncomingWebhook } from '@voltro/plugin-webhooks';
11
+ import { PluginEgressDeclaration } from '@voltro/protocol';
11
12
  import { RpcInterceptor } from '@voltro/protocol';
12
13
  import { RpcKind } from '@voltro/protocol';
13
14
  import { Schema } from 'effect';
@@ -16,6 +17,14 @@ import { TableIndex } from '@voltro/database';
16
17
  import { TableLike } from '@voltro/database';
17
18
  import { VoltroPlugin } from '@voltro/protocol';
18
19
 
20
+ export declare const assertBillingProductionReady: (input: {
21
+ readonly env: NodeJS.ProcessEnv;
22
+ readonly providerOption: BillingProviderName | BillingProvider | undefined;
23
+ readonly resolvedName: string;
24
+ readonly webhookSecret: string | null;
25
+ readonly redirectOrigins: ReadonlyArray<string> | undefined;
26
+ }) => void;
27
+
19
28
  export declare const BILLING_CUSTOMERS_TABLE = "_voltro_billing_customers";
20
29
 
21
30
  export declare const BILLING_DUNNING_NOTICES_TABLE = "_voltro_billing_dunning_notices";
@@ -33,6 +42,14 @@ export declare const BILLING_WEBHOOK_ID = "billing";
33
42
 
34
43
  export declare const BILLING_WEBHOOK_PATH = "/billing/webhook";
35
44
 
45
+ /**
46
+ * What a billing call would do to what the tenant pays — the unit an app's
47
+ * `admit` hook decides on. `checkout` starts paying, `change` moves a plan or
48
+ * seat count, `resume` takes a cancellation back, `portal` opens the provider's
49
+ * self-service page (which can do all of those), `cancel` stops paying.
50
+ */
51
+ export declare type BillingAction = 'checkout' | 'change' | 'resume' | 'portal' | 'cancel';
52
+
36
53
  /**
37
54
  * The shape every handler executor receives carries the resolved subject
38
55
  * under `ctx.request.subject` (mirrors `@voltro/plugin-rbac`'s
@@ -58,6 +75,14 @@ export declare const billingDunningNoticesTable: TableLike & {
58
75
  readonly appliedIndexes: ReadonlyArray<TableIndex>;
59
76
  };
60
77
 
78
+ /**
79
+ * Where billing data leaves the installation, for `voltro compliance generate`
80
+ * (egress.json): Stripe's API for the stripe provider (customers, their email
81
+ * and the subscription items), nothing for the mock, and the provider's own
82
+ * name for a custom one.
83
+ */
84
+ export declare const billingEgressOf: (provider: Pick<BillingProvider, "name">) => ReadonlyArray<PluginEgressDeclaration>;
85
+
61
86
  /**
62
87
  * A billing failure. `transient: true` marks failures the service retries
63
88
  * (network blip talking to the provider); non-transient failures (a 4xx
@@ -122,6 +147,14 @@ export declare type BillingEvent = {
122
147
  readonly tenantId: string;
123
148
  readonly providerCustomerId: string;
124
149
  readonly occurredAt: Date | null;
150
+ } | {
151
+ /** The provider customer was deleted (e.g. in the provider's dashboard).
152
+ * `tenantId` is null when the customer carried no tenant metadata — the
153
+ * link is then resolved from our own customer table. */
154
+ readonly _tag: 'customerUnlinked';
155
+ readonly tenantId: string | null;
156
+ readonly providerCustomerId: string;
157
+ readonly occurredAt: Date | null;
125
158
  };
126
159
 
127
160
  /** The event tag union — handy for `onEvent` keys. */
@@ -195,6 +228,29 @@ export declare interface BillingPluginOptions {
195
228
  * a signed license decide limits per-tenant; a `null` result falls back to the
196
229
  * static `plans` registry. Absent → static registry only.
197
230
  */
231
+ /**
232
+ * The app's say on whether a tenant may change what it pays right now —
233
+ * refuse a checkout, a plan change or the portal for an organization that is
234
+ * deleted or suspended. Fail with a `BillingError` to refuse; the provider
235
+ * is not called. The roles check (`billing:manage`) answers WHO may act; this
236
+ * answers whether the TENANT may, which a scope cannot see.
237
+ */
238
+ readonly admit?: (tenantId: string, action: BillingAction) => Effect.Effect<void, BillingError>;
239
+ /**
240
+ * Origins the provider may send the customer back to (`successUrl`,
241
+ * `cancelUrl`, the portal's `returnUrl`) — e.g. `['https://app.example.com']`.
242
+ * Without it any URL is accepted, which makes checkout a redirect to
243
+ * anywhere; the plugin says so at boot when the Stripe provider is active.
244
+ */
245
+ readonly redirectOrigins?: ReadonlyArray<string>;
246
+ /**
247
+ * Whether the app has closed (deleted) a tenant — positive evidence only.
248
+ * The next sync of a closed tenant that is still paying (any event, the
249
+ * catch-up, a checkpoint) ends its subscriptions: the safety net for a
250
+ * deletion path that did not call `closeAccount`. An unknown tenant is NOT closed (a subscription pointed at
251
+ * the wrong database must never be ended by it).
252
+ */
253
+ readonly isTenantClosed?: (tenantId: string) => Effect.Effect<boolean, BillingError>;
198
254
  readonly resolveEntitlementLimit?: (tenantId: string, key: string) => Effect.Effect<number | null, BillingError>;
199
255
  /**
200
256
  * Namespace for this plugin's rpc tags + inspect endpoints. Default `billing`.
@@ -237,6 +293,10 @@ export declare interface BillingProvider {
237
293
  * When false, `BillingService.reportUsage` records locally but never
238
294
  * calls `provider.reportUsage`. */
239
295
  readonly supportsMeteredUsage: boolean;
296
+ /** Whether a usage push must name a linked provider customer. Stripe
297
+ * meters map events to a customer by id, so an unlinked tenant's usage
298
+ * stays pending instead of being sent unattributable. */
299
+ readonly usageRequiresCustomer?: boolean;
240
300
  readonly createCheckoutSession: (input: CheckoutInput) => Effect.Effect<{
241
301
  url: string;
242
302
  }, BillingError>;
@@ -272,6 +332,43 @@ export declare interface BillingProvider {
272
332
  * provider does not know the id.
273
333
  */
274
334
  readonly fetchSubscription: (providerSubscriptionId: string) => Effect.Effect<ProviderSubscriptionState | null, BillingError>;
335
+ /**
336
+ * Every live (not yet ended) subscription at the provider, optionally for
337
+ * one customer. The provider-side half of reconciliation: what it lists is
338
+ * the truth our rows are corrected to.
339
+ */
340
+ readonly listSubscriptions: (input: {
341
+ readonly providerCustomerId?: string;
342
+ }) => Effect.Effect<ReadonlyArray<ProviderSubscriptionSnapshot>, BillingError>;
343
+ /** End a subscription — at period end (reversible) or immediately with a
344
+ * final invoice. Returns the provider's state afterwards. */
345
+ readonly cancelSubscription: (input: {
346
+ readonly providerSubscriptionId: string;
347
+ readonly mode: CancelMode;
348
+ }) => Effect.Effect<ProviderSubscriptionState, BillingError>;
349
+ /** Undo a period-end cancellation that has not taken effect yet. */
350
+ readonly resumeSubscription: (providerSubscriptionId: string) => Effect.Effect<ProviderSubscriptionState, BillingError>;
351
+ /**
352
+ * The provider customer for a tenant, created when it has none — BEFORE the
353
+ * first checkout, so every checkout names its customer. That is what lets
354
+ * the provider refuse a second subscription for the same customer, and
355
+ * lets open checkouts be found and expired. Must be idempotent per tenant:
356
+ * two concurrent first checkouts get the same customer.
357
+ */
358
+ readonly ensureCustomer: (input: {
359
+ readonly tenantId: string;
360
+ }) => Effect.Effect<string, BillingError>;
361
+ /** Expire every checkout still open for a customer, so only the newest one
362
+ * can be paid. Returns how many were expired. */
363
+ readonly expireOpenCheckouts: (input: {
364
+ readonly providerCustomerId: string;
365
+ }) => Effect.Effect<number, BillingError>;
366
+ /** Add prices the subscription is missing (metered items), without
367
+ * proration. Prices already present are left alone. */
368
+ readonly addSubscriptionPrices: (input: {
369
+ readonly providerSubscriptionId: string;
370
+ readonly priceIds: ReadonlyArray<string>;
371
+ }) => Effect.Effect<void, BillingError>;
275
372
  /**
276
373
  * The billing contact the PROVIDER has on file, when it has one. Used as the
277
374
  * dunning recipient of last resort — `dunning.resolveRecipient` wins when
@@ -298,6 +395,23 @@ export declare interface BillingProvider {
298
395
  * `null` for an event type this provider doesn't model (handler
299
396
  * no-ops, still 200). */
300
397
  readonly normalizeEvent: (raw: unknown) => Effect.Effect<BillingEvent | null, BillingError>;
398
+ /**
399
+ * Who and when a verified payload is about: the provider customer it names
400
+ * and when the provider created it. The webhook syncs that customer from a
401
+ * fresh read instead of trusting the payload, and an event created long
402
+ * before it arrives means deliveries were failing — the cue to catch up.
403
+ * Optional: without it the webhook applies the normalized event as is.
404
+ */
405
+ readonly eventContext?: (raw: unknown) => {
406
+ readonly providerCustomerId: string | null;
407
+ readonly createdAt: Date | null;
408
+ };
409
+ /**
410
+ * Events the provider could not deliver to the app's webhook, as far back as
411
+ * it keeps them (Stripe: 30 days) — what an outage of the endpoint left
412
+ * behind. Optional: a provider without it has no catch-up.
413
+ */
414
+ readonly listUndeliveredEvents?: () => Effect.Effect<ReadonlyArray<UndeliveredEvent>, BillingError>;
301
415
  }
302
416
 
303
417
  export declare type BillingProviderName = 'stripe' | 'mock';
@@ -329,6 +443,31 @@ export declare interface BillingServiceOptions {
329
443
  /** Free-trial length. The provider runs the trial and reports `trialing`. */
330
444
  readonly trialDays?: number;
331
445
  };
446
+ /**
447
+ * The app's say on whether a tenant may change what it pays right now —
448
+ * e.g. refuse a checkout for a deleted or suspended organization. Called
449
+ * before the provider is; a failure refuses the call. Absent → every
450
+ * authorized caller is admitted.
451
+ */
452
+ readonly admit?: (tenantId: string, action: BillingAction) => Effect.Effect<void, BillingError>;
453
+ /**
454
+ * Origins a checkout's `successUrl` / `cancelUrl` and a portal's `returnUrl`
455
+ * may point at. The provider redirects the customer there, so an arbitrary
456
+ * URL would make our checkout a redirect to anywhere. Absent → any URL.
457
+ */
458
+ readonly redirectOrigins?: ReadonlyArray<string>;
459
+ /**
460
+ * Whether the app has closed (deleted) a tenant. The reconcile ends every
461
+ * subscription of a closed tenant still paying — the safety net for a
462
+ * deletion path that did not call `closeAccount`. Answer `true` only on
463
+ * positive evidence (a row marked deleted): an unknown tenant is not proof,
464
+ * and a failing check is treated as "not closed".
465
+ */
466
+ readonly isTenantClosed?: (tenantId: string) => Effect.Effect<boolean, BillingError>;
467
+ /** The app's per-event side effects, run for every correction a sync or
468
+ * reconcile makes (the webhook runs them itself, and fails the delivery on
469
+ * an error). */
470
+ readonly onEvent?: OnEventMap;
332
471
  /** Clock injection for deterministic period + grace windowing in tests. */
333
472
  readonly now?: () => Date;
334
473
  /**
@@ -400,6 +539,8 @@ export declare interface BillingServiceShape {
400
539
  readonly plan: PlanId;
401
540
  readonly successUrl: string;
402
541
  readonly cancelUrl: string;
542
+ /** Seats to buy up front. Omit for 1. */
543
+ readonly quantity?: number;
403
544
  }) => Effect.Effect<{
404
545
  url: string;
405
546
  }, BillingError>;
@@ -407,6 +548,48 @@ export declare interface BillingServiceShape {
407
548
  readonly portalUrl: (tenantId: string, returnUrl: string) => Effect.Effect<{
408
549
  url: string;
409
550
  }, BillingError>;
551
+ /**
552
+ * The customer's own cancellation: the subscription stays active until the
553
+ * paid period ends, then ends. The provider applies it first; the row
554
+ * mirrors its answer (`cancelAt`). Reversible with `resumeSubscription`.
555
+ */
556
+ readonly cancelSubscription: (tenantId: string) => Effect.Effect<Subscription, BillingError>;
557
+ /** Take back a period-end cancellation before it takes effect. */
558
+ readonly resumeSubscription: (tenantId: string) => Effect.Effect<Subscription, BillingError>;
559
+ /**
560
+ * End every provider subscription of a tenant — for deleting an
561
+ * organization or account. Pending usage is flushed first; each subscription
562
+ * then ends once that usage has settled on the meter (minutes, see
563
+ * `CancelMode` 'close') with a final invoice for it and a proration credit.
564
+ * The rows carry the end as `cancelAt` until the provider's deletion event
565
+ * marks them canceled. The provider customer and its invoices are KEPT: they
566
+ * are the accounting record. Fails without ending anything when the flush
567
+ * fails, so no usage is lost; the caller retries.
568
+ */
569
+ readonly closeAccount: (tenantId: string) => Effect.Effect<CloseAccountResult, BillingError>;
570
+ /**
571
+ * Correct every local row from the provider: what the provider lists wins,
572
+ * a local live row the provider no longer lists is re-read directly, missing
573
+ * metered items are added back. Run it on a schedule — it heals whatever a
574
+ * missed or failed webhook left behind.
575
+ */
576
+ readonly reconcileFromProvider: () => Effect.Effect<ProviderReconcileResult, BillingError>;
577
+ /**
578
+ * Bring one provider customer's rows to what the provider says now, and
579
+ * return its current state as events (all of it, not just changes). The
580
+ * webhook uses it for the customer an event names. Does not run `onEvent`.
581
+ */
582
+ readonly syncCustomer: (providerCustomerId: string, hintTenantId?: string) => Effect.Effect<{
583
+ readonly tenantId: string | null;
584
+ readonly events: ReadonlyArray<BillingEvent>;
585
+ }, BillingError>;
586
+ /**
587
+ * The checkpoint: sync a tenant from the provider, run `onEvent` for what it
588
+ * found, and answer with its current subscription. Call it where a stale row
589
+ * would matter — the return from checkout or the portal, a billing page
590
+ * load. Every call that changes what a tenant pays already runs it first.
591
+ */
592
+ readonly syncTenant: (tenantId: string) => Effect.Effect<Subscription | null, BillingError>;
410
593
  /** Apply a normalized provider event to the DB rows (idempotent upsert). */
411
594
  readonly applyEvent: (event: BillingEvent) => Effect.Effect<void, BillingError>;
412
595
  /**
@@ -471,13 +654,11 @@ declare interface BillingWebhookOptions {
471
654
  * `constructEvent`) uses. Null → the mount answers 503: an unverifiable
472
655
  * billing webhook must not reach subscription state. */
473
656
  readonly webhookSecret?: string | null;
657
+ /** Called when an event arrives long after it was created — deliveries were
658
+ * failing; the plugin catches up on what the endpoint missed. */
659
+ readonly onLateDelivery?: () => void;
474
660
  }
475
661
 
476
- /**
477
- * Build the BillingService implementation. The interceptor + the plugin's
478
- * rpc routes share this single instance (so `enforce` and the rpc routes
479
- * see the same swapped-in stores).
480
- */
481
662
  export declare const buildBillingService: (options: BillingServiceOptions) => {
482
663
  readonly service: BillingServiceShape;
483
664
  readonly holder: StoreHolder;
@@ -498,10 +679,49 @@ export declare const buildEnforceInterceptor: (enforce: EnforceMap, service: Bil
498
679
  /** Build the registry from `billingPlugin({ plans })`. */
499
680
  export declare const buildPlanRegistry: (plans: Readonly<Record<PlanId, PlanConfig>>) => PlanRegistry;
500
681
 
682
+ /** How a subscription ends. */
683
+ export declare type CancelMode =
684
+ /** Stays active until the paid period ends, then ends — the customer's own
685
+ * cancellation. Reversible with `resumeSubscription` until then. */
686
+ 'periodEnd'
687
+ /**
688
+ * Closing an account: ends as soon as the last usage has settled on the
689
+ * meter — a few minutes, not a period — with a final invoice for that usage
690
+ * and a proration credit for the unused time.
691
+ *
692
+ * Not "now", on purpose: meter events aggregate asynchronously, and a
693
+ * subscription ended the moment its last usage was sent is invoiced for none
694
+ * of it (measured: the meter showed 250, the final invoice 0, and a finalized
695
+ * invoice never picks the usage up later).
696
+ */
697
+ | 'close';
698
+
699
+ /** One finding. `apply` is the request that resolves it; absent → a human. */
700
+ export declare type CatalogFinding = {
701
+ readonly kind: 'ok';
702
+ readonly object: string;
703
+ } | {
704
+ readonly kind: 'create' | 'update' | 'adopt';
705
+ readonly object: string;
706
+ readonly detail: string;
707
+ readonly apply: StripeRequest;
708
+ } | {
709
+ readonly kind: 'drift';
710
+ readonly object: string;
711
+ readonly detail: string;
712
+ readonly apply?: StripeRequest;
713
+ } | {
714
+ readonly kind: 'manual';
715
+ readonly object: string;
716
+ readonly detail: string;
717
+ };
718
+
501
719
  export declare interface CheckoutInput {
502
720
  readonly tenantId: string;
503
721
  /** Provider price id to subscribe to. */
504
722
  readonly priceId: string;
723
+ /** Usage-based prices added next to `priceId` (no quantity). */
724
+ readonly meteredPriceIds?: ReadonlyArray<string>;
505
725
  readonly successUrl: string;
506
726
  readonly cancelUrl: string;
507
727
  /** Optional existing provider customer id to attach the checkout to. */
@@ -527,6 +747,13 @@ export declare interface CheckoutInput {
527
747
  */
528
748
  export declare const claimFlushWindow: (store: DataStore, windowKey: string, replicaId: string, nowMs: number) => Promise<boolean>;
529
749
 
750
+ /** What `closeAccount` did at the provider. */
751
+ export declare interface CloseAccountResult {
752
+ readonly tenantId: string;
753
+ /** Provider subscriptions ended (immediately, with a final invoice). */
754
+ readonly canceled: ReadonlyArray<string>;
755
+ }
756
+
530
757
  declare interface CustomerRecord {
531
758
  readonly tenantId: string;
532
759
  readonly provider: string;
@@ -536,6 +763,12 @@ declare interface CustomerRecord {
536
763
  export declare interface CustomerStore {
537
764
  readonly upsert: (record: CustomerRecord) => Effect.Effect<void, BillingError>;
538
765
  readonly getByTenant: (tenantId: string) => Effect.Effect<CustomerRecord | null, BillingError>;
766
+ readonly getByProviderCustomerId: (providerCustomerId: string) => Effect.Effect<CustomerRecord | null, BillingError>;
767
+ /** Every linked customer — the provider reconcile's per-customer pass. */
768
+ readonly list: () => Effect.Effect<ReadonlyArray<CustomerRecord>, BillingError>;
769
+ /** Drop the link to a provider customer that no longer exists. Returns the
770
+ * tenant it belonged to, or null when it was not linked. */
771
+ readonly removeByProviderCustomerId: (providerCustomerId: string) => Effect.Effect<string | null, BillingError>;
539
772
  }
540
773
 
541
774
  export declare const dataStoreDunningNoticeStore: (store: DataStore) => DunningNoticeStore;
@@ -576,6 +809,50 @@ export declare const defaultDunningEmail: (notice: {
576
809
  readonly text: string;
577
810
  };
578
811
 
812
+ /** A meter: found by event name (Stripe keeps it unique per account). Only the
813
+ * display name is editable; anything else is a human's call. */
814
+ export declare const diffMeter: (spec: StripeCatalogMeter, meters: ReadonlyArray<StripeMeter>) => {
815
+ finding: CatalogFinding;
816
+ id: string | null;
817
+ };
818
+
819
+ /** The portal configuration: found by `metadata.voltro_key`, else adopted by
820
+ * its name; must be the account default, since sessions name none. */
821
+ export declare const diffPortal: (portal: StripeCatalog["portal"], env: StripeCatalogEnvironment, plans: ReadonlyArray<{
822
+ readonly productId: string;
823
+ readonly priceId: string;
824
+ }> | null, configs: ReadonlyArray<StripePortalConfiguration>) => ReadonlyArray<CatalogFinding>;
825
+
826
+ /**
827
+ * A price: found by `lookup_key`. A price's amount, currency, interval, meter
828
+ * and tax behaviour cannot be edited at Stripe — a mismatch means a NEW price
829
+ * that takes the key over, which is a pricing change and runs only when
830
+ * `allowPriceChange` says so. Existing subscriptions stay on their old price
831
+ * (Stripe does not move them); that is the customer-facing half of the
832
+ * decision, and why it is never automatic.
833
+ */
834
+ export declare const diffPrice: (spec: StripeCatalogPrice, productId: string | null, meterId: string | null, byLookup: StripePrice | null, productPrices: ReadonlyArray<StripePrice>, allowPriceChange: boolean) => {
835
+ finding: CatalogFinding;
836
+ id: string | null;
837
+ };
838
+
839
+ /**
840
+ * A product: found by `metadata.voltro_key`; failing that, ADOPTED from an
841
+ * active product carrying the same identifying metadata (one created before
842
+ * the catalog existed) — only when exactly one qualifies, never by guessing.
843
+ */
844
+ export declare const diffProduct: (spec: StripeCatalogProduct, products: ReadonlyArray<StripeProduct>) => {
845
+ finding: CatalogFinding;
846
+ id: string | null;
847
+ };
848
+
849
+ export declare const diffTaxSettings: (tax: StripeCatalog["tax"], settings: StripeTaxSettings) => CatalogFinding;
850
+
851
+ /** The webhook endpoint, where the environment has one: every event the plugin
852
+ * maps, on the SDK's API version. Its signing secret exists only in the
853
+ * response to its creation. */
854
+ export declare const diffWebhook: (env: StripeCatalogEnvironment, endpoints: ReadonlyArray<StripeWebhookEndpoint>) => CatalogFinding;
855
+
579
856
  /** The steps whose `afterHours` have elapsed. Ledger claims — not this
580
857
  * function — decide which of them actually SEND. */
581
858
  export declare const dueSteps: (steps: ReadonlyArray<DunningStep>, pastDueSince: Date, now: Date) => ReadonlyArray<DunningStep>;
@@ -776,6 +1053,13 @@ export declare interface EntitlementStatus {
776
1053
  readonly lockout: LockoutMode;
777
1054
  }
778
1055
 
1056
+ /**
1057
+ * Why `path` must not receive secrets, or null when it may: a file inside a git
1058
+ * work tree that git would track (not ignored) is one `git add .` from being
1059
+ * committed with a live account's price ids and webhook secret in it.
1060
+ */
1061
+ export declare const envOutRefusal: (path: string, git?: (args: ReadonlyArray<string>, cwd: string) => boolean, realpath?: (p: string) => string) => string | null;
1062
+
779
1063
  /** The episode a `pastDueSince` identifies. Recovery clears the column, so the
780
1064
  * next failure necessarily produces a different key. */
781
1065
  export declare const episodeKeyOf: (pastDueSince: Date) => string;
@@ -805,6 +1089,9 @@ export declare const evaluateEntitlement: (entitlement: string, limit: number, u
805
1089
  * flushing within the same interval land on the same key → one wins. */
806
1090
  export declare const flushWindowKey: (nowMs: number, intervalMs: number) => string;
807
1091
 
1092
+ /** Stripe's form encoding: `a[b][0][c]=v`. `null` unsets a field (empty value). */
1093
+ export declare const formEncode: (params: Readonly<Record<string, unknown>>) => string;
1094
+
808
1095
  /** When the grace for an episode runs out. */
809
1096
  export declare const graceEndOf: (pastDueSince: Date, graceHours: number) => Date;
810
1097
 
@@ -843,12 +1130,9 @@ export declare interface InvoiceStore {
843
1130
  readonly listByTenant: (tenantId: string) => Effect.Effect<ReadonlyArray<InvoiceRecord_2>, BillingError>;
844
1131
  }
845
1132
 
846
- /**
847
- * Is `incoming` older than what the row already reflects? Undated events never
848
- * displace a dated one, and a dated event always wins over an undated row —
849
- * that ordering is what makes a hand-built (undated) payload usable in tests
850
- * without letting it stomp production ordering.
851
- */
1133
+ /** Is this a real deployment boot? Only an explicitly dev-like NODE_ENV is not. */
1134
+ export declare const isProductionBoot: (nodeEnv: string | undefined) => boolean;
1135
+
852
1136
  export declare const isStaleStatusEvent: (existing: Date | null, incoming: Date | null) => boolean;
853
1137
 
854
1138
  /**
@@ -866,6 +1150,9 @@ export declare const memoryDunningNoticeStore: () => DunningNoticeStore;
866
1150
 
867
1151
  export declare const memoryStores: () => BillingStores;
868
1152
 
1153
+ /** Replace (or add) `NAME=value` lines; every other line is kept as it was. */
1154
+ export declare const mergeEnvFile: (text: string, values: Readonly<Record<string, string>>) => string;
1155
+
869
1156
  /** `meterKey → source`. The meterKey is the entitlement/usage key reported to
870
1157
  * the provider (`billing.reportUsage(tenant, meterKey, qty)`). */
871
1158
  declare type MeteringConfig = Readonly<Record<string, MeterSource>>;
@@ -917,6 +1204,32 @@ export declare interface MockProvider extends BillingProvider {
917
1204
  readonly setCustomerEmail: (providerCustomerId: string, email: string | null) => void;
918
1205
  /** Every `fetchSubscription` id asked for — proves the reconcile happened. */
919
1206
  readonly fetched: ReadonlyArray<string>;
1207
+ /** Put a subscription on the provider's LIST (and its direct read) — what
1208
+ * `listSubscriptions` returns and a reconcile corrects local rows to. */
1209
+ readonly listSubscription: (snapshot: ProviderSubscriptionSnapshot) => void;
1210
+ /** Recorded cancellations, in order. */
1211
+ readonly cancels: ReadonlyArray<{
1212
+ readonly providerSubscriptionId: string;
1213
+ readonly mode: CancelMode;
1214
+ }>;
1215
+ /** Recorded resumptions. */
1216
+ readonly resumes: ReadonlyArray<string>;
1217
+ /** Recorded `addSubscriptionPrices` calls. */
1218
+ readonly priceAdds: ReadonlyArray<{
1219
+ readonly providerSubscriptionId: string;
1220
+ readonly priceIds: ReadonlyArray<string>;
1221
+ }>;
1222
+ /** Recorded checkout sessions. */
1223
+ readonly checkouts: ReadonlyArray<CheckoutInput>;
1224
+ /** Customers created by `ensureCustomer`, by tenant. */
1225
+ readonly customersCreated: ReadonlyArray<string>;
1226
+ /** Every `expireOpenCheckouts` call, by customer. */
1227
+ readonly expiredFor: ReadonlyArray<string>;
1228
+ /** Set what `listUndeliveredEvents` reports — events the provider could
1229
+ * not deliver to the app's webhook. */
1230
+ readonly setUndelivered: (events: ReadonlyArray<UndeliveredEvent>) => void;
1231
+ /** How often `listUndeliveredEvents` was asked — proves a catch-up ran. */
1232
+ readonly undeliveredReads: () => number;
920
1233
  }
921
1234
 
922
1235
  export declare const mockProvider: (options?: {
@@ -957,6 +1270,19 @@ export declare const normalizeStripeEvent: (event: default_2.Event, resolvePrice
957
1270
  /** Per-event side effect, run AFTER the row is updated. */
958
1271
  export declare type OnEventMap = Readonly<Record<string, (event: BillingEvent) => Effect.Effect<void, unknown>>>;
959
1272
 
1273
+ /** Findings the account still differs by (after `apply`: what it could not fix). */
1274
+ export declare const openFindings: (findings: ProvisionResult["findings"]) => (CatalogFinding & {
1275
+ readonly applied?: boolean;
1276
+ })[];
1277
+
1278
+ export declare interface ParsedProvisionArgs extends ProvisionOptions {
1279
+ readonly confirmLive: boolean;
1280
+ readonly strict: boolean;
1281
+ }
1282
+
1283
+ /** Parse the command line; a string is the refusal. */
1284
+ export declare const parseProvisionArgs: (argv: ReadonlyArray<string>) => ParsedProvisionArgs | string;
1285
+
960
1286
  /** Calendar-month window key (`'YYYY-MM'`) for metered counters. */
961
1287
  export declare const periodKey: (now?: Date) => string;
962
1288
 
@@ -964,6 +1290,13 @@ export declare const periodKey: (now?: Date) => string;
964
1290
  export declare interface PlanConfig {
965
1291
  /** Provider price id (Stripe `price_…`). Absent for free/zero-cost plans. */
966
1292
  readonly priceId?: string;
1293
+ /**
1294
+ * Usage-based provider prices subscribed ALONGSIDE `priceId` (Stripe prices
1295
+ * with `recurring.meter`). Checkout adds each as a quantity-less line item,
1296
+ * so the subscription carries the metered item from its first invoice —
1297
+ * without it, meter events are recorded at Stripe but never billed.
1298
+ */
1299
+ readonly meteredPriceIds?: ReadonlyArray<string>;
967
1300
  /** Per-entitlement-key limits. Values are `number | 'unlimited'`. */
968
1301
  readonly entitlements: Readonly<Record<string, EntitlementLimit>>;
969
1302
  /** The plan's per-period price in INTEGER minor units (cents) — the unit
@@ -994,6 +1327,8 @@ export declare interface PlanRegistry {
994
1327
  readonly planForPriceId: (priceId: string) => PlanId | null;
995
1328
  /** The provider price id for a plan, or null (free plans have none). */
996
1329
  readonly priceIdFor: (plan: PlanId) => string | null;
1330
+ /** The usage-based price ids subscribed next to the plan price. */
1331
+ readonly meteredPriceIdsFor: (plan: PlanId) => ReadonlyArray<string>;
997
1332
  /** The per-seat unit amount in integer minor units for a plan; `0` for a
998
1333
  * free/undeclared plan. Multiplied by seat quantity for the billed amount. */
999
1334
  readonly unitAmountFor: (plan: PlanId) => number;
@@ -1010,6 +1345,51 @@ export declare interface PortalInput {
1010
1345
  readonly returnUrl: string;
1011
1346
  }
1012
1347
 
1348
+ /** The portal configuration the plugin's sessions use (the account default). */
1349
+ export declare const portalParams: (portal: StripeCatalog["portal"], env: StripeCatalogEnvironment, plans: ReadonlyArray<{
1350
+ readonly productId: string;
1351
+ readonly priceId: string;
1352
+ }>) => Record<string, unknown>;
1353
+
1354
+ /** What `reconcileFromProvider` found and corrected. */
1355
+ export declare interface ProviderReconcileResult {
1356
+ /** Subscriptions compared (provider-listed plus local rows it did not list). */
1357
+ readonly checked: number;
1358
+ /** Tenants whose local rows changed because the provider said otherwise. */
1359
+ readonly corrected: ReadonlyArray<string>;
1360
+ /** Tenants with more than one live subscription at the provider — billed
1361
+ * twice. Never resolved automatically: refunding is a human decision. */
1362
+ readonly duplicates: ReadonlyArray<string>;
1363
+ /** Provider subscriptions naming no tenant; nothing to attribute them to. */
1364
+ readonly unattributed: number;
1365
+ /** Subscriptions that had a plan's metered price added back. */
1366
+ readonly meteredItemsRestored: number;
1367
+ /** Tenants the app reports closed whose subscriptions were still paying —
1368
+ * ended now (see `isTenantClosed`). */
1369
+ readonly closedTenants: ReadonlyArray<string>;
1370
+ /** Live rows the provider has never heard of that were NOT canceled, because
1371
+ * so many at once means the wrong account or provider (0 normally). */
1372
+ readonly refusedUnknown: number;
1373
+ /** Every correction as the event it stands for, for the app's `onEvent`
1374
+ * projections; the plugin's scheduled reconcile dispatches them. */
1375
+ readonly events: ReadonlyArray<BillingEvent>;
1376
+ }
1377
+
1378
+ /**
1379
+ * One subscription as the provider lists it — the input of the provider-side
1380
+ * reconcile. Carries the ids a local row is matched by and the price ids a
1381
+ * plan's metered items are checked against.
1382
+ */
1383
+ export declare interface ProviderSubscriptionSnapshot extends ProviderSubscriptionState {
1384
+ readonly providerSubscriptionId: string;
1385
+ readonly providerCustomerId: string | null;
1386
+ /** From the subscription's own metadata; null when the provider object was
1387
+ * not created through our checkout and names no tenant. */
1388
+ readonly tenantId: string | null;
1389
+ /** Every price the subscription carries, licensed and metered. */
1390
+ readonly priceIds: ReadonlyArray<string>;
1391
+ }
1392
+
1013
1393
  /** The provider's CURRENT view of a subscription — read directly, not from an
1014
1394
  * event body. `reconcileDunning` asks for this before it will lock anyone
1015
1395
  * out. */
@@ -1023,6 +1403,34 @@ export declare interface ProviderSubscriptionState {
1023
1403
  readonly cancelAt: Date | null;
1024
1404
  }
1025
1405
 
1406
+ export declare interface ProvisionOptions {
1407
+ readonly env: StripeEnvironment;
1408
+ readonly apply: boolean;
1409
+ readonly allowPriceChange: boolean;
1410
+ /** Where `STRIPE_PRICE_*` ids and a created webhook's secret go; null →
1411
+ * nowhere (and a webhook endpoint is then not created). */
1412
+ readonly envOut: string | null;
1413
+ }
1414
+
1415
+ export declare interface ProvisionResult {
1416
+ readonly livemode: boolean;
1417
+ readonly findings: ReadonlyArray<CatalogFinding & {
1418
+ readonly applied?: boolean;
1419
+ }>;
1420
+ /** Price ids by env name, and a created webhook's `STRIPE_WEBHOOK_SECRET`. */
1421
+ readonly envOut: Readonly<Record<string, string>>;
1422
+ }
1423
+
1424
+ /**
1425
+ * Read the account, diff it against the catalog, and (with `apply`) fix what
1426
+ * can be fixed — in dependency order: meters, products, prices, tax, portal,
1427
+ * webhook.
1428
+ */
1429
+ export declare const provisionStripeCatalog: (rest: StripeRest, catalog: StripeCatalog, options: ProvisionOptions) => Promise<ProvisionResult>;
1430
+
1431
+ /** Stripe echoes a masked key in auth errors, and the mask ends in real characters. */
1432
+ export declare const redactStripeKeys: (text: string) => string;
1433
+
1026
1434
  /**
1027
1435
  * In-handler DUNNING guard: refuse when the tenant's grace period has run out.
1028
1436
  *
@@ -1097,14 +1505,36 @@ declare interface ResolveProviderInput {
1097
1505
  readonly env: NodeJS.ProcessEnv;
1098
1506
  /** Price id → plan id, from the plugin's plan registry. */
1099
1507
  readonly planForPriceId?: (priceId: string) => string | null;
1100
- /** Where the mock-by-absence warning goes. Optional: a library caller
1101
- * resolving a provider outside a boot has nowhere to put it, and a missing
1102
- * logger must not be a reason to fail. */
1103
- readonly log?: {
1104
- readonly warn: (message: string) => void;
1105
- };
1106
1508
  }
1107
1509
 
1510
+ /** The key for `env`, checked against its mode. */
1511
+ export declare const resolveProvisionKey: (env: StripeEnvironment, vars: NodeJS.ProcessEnv, dotenvKey: string | undefined) => {
1512
+ key: string;
1513
+ } | {
1514
+ refusal: string;
1515
+ };
1516
+
1517
+ /**
1518
+ * The provisioning command. Exit code: 0 when the account matches the catalog
1519
+ * (after `--apply`: when everything it could fix is fixed); 1 when something
1520
+ * still differs; 2 on a refused invocation. `manual` items fail the run only
1521
+ * with `--strict` — the launch-readiness check.
1522
+ */
1523
+ export declare const runStripeProvision: (catalog: StripeCatalog, argv: ReadonlyArray<string>, options?: {
1524
+ readonly dotenv?: string;
1525
+ readonly rest?: (key: string) => StripeRest;
1526
+ readonly vars?: NodeJS.ProcessEnv;
1527
+ }) => Promise<number>;
1528
+
1529
+ /** What only a human can settle before an environment sells. */
1530
+ export declare const sellingReadiness: (input: {
1531
+ readonly catalog: StripeCatalog;
1532
+ readonly env: StripeCatalogEnvironment;
1533
+ readonly chargesEnabled: boolean | null;
1534
+ readonly accountTaxIds: number | null;
1535
+ readonly activeRegistrations: ReadonlyArray<string>;
1536
+ }) => ReadonlyArray<CatalogFinding>;
1537
+
1108
1538
  /**
1109
1539
  * A mutable holder so `bindDataStore` can swap the stores AFTER the service
1110
1540
  * layer is built. The service reads `holder.stores` on every call rather
@@ -1115,6 +1545,157 @@ declare interface StoreHolder {
1115
1545
  stores: BillingStores;
1116
1546
  }
1117
1547
 
1548
+ /** The API version every provisioning request and webhook endpoint uses — the
1549
+ * one the SDK the plugin bills with pins. */
1550
+ export declare const STRIPE_API_VERSION: string;
1551
+
1552
+ /** The Dashboard steps no API can check, which every Stripe billing app needs. */
1553
+ export declare const STRIPE_DASHBOARD_REMINDERS: ReadonlyArray<string>;
1554
+
1555
+ /**
1556
+ * Every Stripe event type `normalizeStripeEvent` maps — the list a webhook
1557
+ * endpoint subscribes to. Subscribing to fewer loses state changes; a test
1558
+ * holds this list to the switch below.
1559
+ */
1560
+ export declare const STRIPE_WEBHOOK_EVENTS: ReadonlyArray<string>;
1561
+
1562
+ export declare interface StripeCatalog {
1563
+ readonly products: ReadonlyArray<StripeCatalogProduct>;
1564
+ readonly meters: ReadonlyArray<StripeCatalogMeter>;
1565
+ readonly tax: {
1566
+ /** The business location Stripe Tax computes from. */
1567
+ readonly headOffice: StripeCatalogAddress;
1568
+ readonly defaultTaxCode: string;
1569
+ readonly defaultTaxBehavior: 'exclusive' | 'inclusive';
1570
+ /** Countries an active registration must exist for before tax is collected. */
1571
+ readonly registrationCountries: ReadonlyArray<string>;
1572
+ };
1573
+ readonly portal: {
1574
+ readonly key: string;
1575
+ readonly name: string;
1576
+ readonly headline: string;
1577
+ readonly privacyPolicyUrl: string;
1578
+ readonly termsOfServiceUrl: string;
1579
+ };
1580
+ readonly environments: Readonly<Record<StripeEnvironment, StripeCatalogEnvironment>>;
1581
+ /** Dashboard-only steps, listed as reminders where selling readiness is required. */
1582
+ readonly dashboardReminders?: ReadonlyArray<string>;
1583
+ }
1584
+
1585
+ export declare interface StripeCatalogAddress {
1586
+ readonly line1: string;
1587
+ readonly line2?: string;
1588
+ readonly postal_code: string;
1589
+ readonly city: string;
1590
+ readonly state?: string;
1591
+ readonly country: string;
1592
+ }
1593
+
1594
+ export declare interface StripeCatalogEnvironment {
1595
+ /** Where the customer portal returns to. */
1596
+ readonly portalReturnUrl: string;
1597
+ /** The app's `POST /billing/webhook` URL, or null where `stripe listen`
1598
+ * forwards instead (local development against a sandbox). */
1599
+ readonly webhookUrl: string | null;
1600
+ /** Whether account-level steps (activation, tax ids, Dashboard-only
1601
+ * settings) must be done before this environment sells. */
1602
+ readonly requireSellingReadiness: boolean;
1603
+ }
1604
+
1605
+ export declare interface StripeCatalogMeter {
1606
+ readonly eventName: string;
1607
+ readonly displayName: string;
1608
+ readonly formula: 'sum' | 'count' | 'last';
1609
+ /** Payload key naming the customer — `stripe_customer_id` for this plugin. */
1610
+ readonly customerPayloadKey: string;
1611
+ readonly valuePayloadKey: string;
1612
+ }
1613
+
1614
+ export declare interface StripeCatalogPrice {
1615
+ readonly lookupKey: string;
1616
+ /** The env var the app reads this price's id from (`--env-out` writes it). */
1617
+ readonly envName: string;
1618
+ readonly currency: string;
1619
+ readonly unitAmount: number;
1620
+ readonly interval: 'day' | 'week' | 'month' | 'year';
1621
+ readonly usageType: 'licensed' | 'metered';
1622
+ /** For a metered price: the event name of the catalog meter it bills. */
1623
+ readonly meter?: string;
1624
+ readonly taxBehavior: 'exclusive' | 'inclusive';
1625
+ }
1626
+
1627
+ export declare interface StripeCatalogProduct {
1628
+ /** Stable identity, stored as `metadata.voltro_key`. */
1629
+ readonly key: string;
1630
+ readonly name: string;
1631
+ readonly description: string;
1632
+ readonly unitLabel: string;
1633
+ readonly taxCode: string;
1634
+ /** Extra metadata beside `voltro_key` — also what a product created before
1635
+ * the catalog is ADOPTED by (exactly one active product must carry it). */
1636
+ readonly metadata: Readonly<Record<string, string>>;
1637
+ readonly price: StripeCatalogPrice;
1638
+ /** Whether customers may switch to this product in the portal. */
1639
+ readonly inPortal: boolean;
1640
+ }
1641
+
1642
+ export declare type StripeEnvironment = 'sandbox' | 'live';
1643
+
1644
+ declare interface StripeMeter {
1645
+ readonly id: string;
1646
+ readonly status: string;
1647
+ readonly event_name: string;
1648
+ readonly display_name: string;
1649
+ readonly default_aggregation: {
1650
+ readonly formula: string;
1651
+ };
1652
+ readonly customer_mapping: {
1653
+ readonly type: string;
1654
+ readonly event_payload_key: string;
1655
+ };
1656
+ readonly value_settings: {
1657
+ readonly event_payload_key: string;
1658
+ };
1659
+ }
1660
+
1661
+ declare interface StripePortalConfiguration {
1662
+ readonly id: string;
1663
+ readonly active: boolean;
1664
+ readonly is_default: boolean;
1665
+ readonly name: string | null;
1666
+ readonly metadata: Readonly<Record<string, string>>;
1667
+ readonly default_return_url: string | null;
1668
+ readonly business_profile: Readonly<Record<string, unknown>>;
1669
+ readonly features: Readonly<Record<string, unknown>>;
1670
+ }
1671
+
1672
+ declare interface StripePrice {
1673
+ readonly id: string;
1674
+ readonly active: boolean;
1675
+ readonly product: string;
1676
+ readonly currency: string;
1677
+ readonly unit_amount: number | null;
1678
+ readonly lookup_key: string | null;
1679
+ readonly tax_behavior: string | null;
1680
+ readonly recurring: {
1681
+ readonly interval: string;
1682
+ readonly usage_type: string;
1683
+ readonly meter: string | null;
1684
+ } | null;
1685
+ }
1686
+
1687
+ declare interface StripeProduct {
1688
+ readonly id: string;
1689
+ readonly active: boolean;
1690
+ readonly name: string;
1691
+ readonly description: string | null;
1692
+ readonly unit_label: string | null;
1693
+ readonly tax_code: string | null | {
1694
+ readonly id: string;
1695
+ };
1696
+ readonly metadata: Readonly<Record<string, string>>;
1697
+ }
1698
+
1118
1699
  export declare const stripeProvider: (options: StripeProviderOptions) => BillingProvider;
1119
1700
 
1120
1701
  export declare interface StripeProviderOptions {
@@ -1131,10 +1712,57 @@ export declare interface StripeProviderOptions {
1131
1712
  * on `free` while paying. The plugin passes its plan registry here.
1132
1713
  */
1133
1714
  readonly planForPriceId?: (priceId: string) => string | null;
1715
+ /**
1716
+ * How long a closed account's subscription runs on so its last metered usage
1717
+ * settles before the final invoice (seconds, default 600). Meter events
1718
+ * aggregate asynchronously; ending the subscription sooner invoices usage the
1719
+ * meter has not counted yet as zero.
1720
+ */
1721
+ readonly closeSettleSeconds?: number;
1134
1722
  /** Injected in tests. */
1135
1723
  readonly client?: default_2;
1136
1724
  }
1137
1725
 
1726
+ declare interface StripeRequest {
1727
+ readonly method: 'POST';
1728
+ readonly path: string;
1729
+ readonly params: Readonly<Record<string, unknown>>;
1730
+ }
1731
+
1732
+ export declare interface StripeRest {
1733
+ readonly get: <T>(path: string, query?: Readonly<Record<string, unknown>>) => Promise<T>;
1734
+ readonly post: <T>(path: string, params: Readonly<Record<string, unknown>>) => Promise<T>;
1735
+ /** Every page of a list endpoint. */
1736
+ readonly list: <T>(path: string, query?: Readonly<Record<string, unknown>>) => Promise<ReadonlyArray<T>>;
1737
+ }
1738
+
1739
+ export declare const stripeRest: (apiKey: string, fetchImpl?: typeof fetch) => StripeRest;
1740
+
1741
+ export declare class StripeRestError extends Error {
1742
+ readonly status: number;
1743
+ constructor(status: number, message: string);
1744
+ }
1745
+
1746
+ declare interface StripeTaxSettings {
1747
+ readonly status: string;
1748
+ readonly livemode: boolean;
1749
+ readonly head_office: {
1750
+ readonly address: Readonly<Record<string, string | null>>;
1751
+ } | null;
1752
+ readonly defaults: {
1753
+ readonly tax_code: string | null;
1754
+ readonly tax_behavior: string | null;
1755
+ };
1756
+ }
1757
+
1758
+ declare interface StripeWebhookEndpoint {
1759
+ readonly id: string;
1760
+ readonly url: string;
1761
+ readonly status: string;
1762
+ readonly enabled_events: ReadonlyArray<string>;
1763
+ readonly api_version: string | null;
1764
+ }
1765
+
1138
1766
  /** A tenant's subscription row, normalized across providers. */
1139
1767
  export declare interface Subscription {
1140
1768
  readonly tenantId: string;
@@ -1250,7 +1878,11 @@ export declare interface SubscriptionStore {
1250
1878
  * only a provider-confirmed reconcile sets it.
1251
1879
  */
1252
1880
  readonly upsert: (sub: Subscription) => Effect.Effect<void, BillingError>;
1881
+ /** The tenant's CURRENT subscription: a live one when there is one, else
1882
+ * the most recently changed. An old canceled row never hides a live one. */
1253
1883
  readonly getByTenant: (tenantId: string) => Effect.Effect<Subscription | null, BillingError>;
1884
+ /** Every subscription row of a tenant, newest first. */
1885
+ readonly listByTenant: (tenantId: string) => Effect.Effect<ReadonlyArray<Subscription>, BillingError>;
1254
1886
  /** Every subscription in a given status — the dunning sweep's only query. */
1255
1887
  readonly listByStatus: (status: SubscriptionStatus) => Effect.Effect<ReadonlyArray<Subscription>, BillingError>;
1256
1888
  /** Mark a subscription canceled by provider id (no-op if absent). */
@@ -1282,6 +1914,13 @@ declare interface SubscriptionUpdateInput {
1282
1914
  readonly proration?: 'prorate' | 'none' | 'always_invoice';
1283
1915
  }
1284
1916
 
1917
+ /** An event the provider failed to deliver, reduced to what a catch-up needs. */
1918
+ declare interface UndeliveredEvent {
1919
+ readonly id: string;
1920
+ readonly providerCustomerId: string | null;
1921
+ readonly createdAt: Date | null;
1922
+ }
1923
+
1285
1924
  export declare interface UsagePush {
1286
1925
  readonly tenantId: string;
1287
1926
  readonly entitlementKey: string;
@@ -1289,6 +1928,14 @@ export declare interface UsagePush {
1289
1928
  readonly quantity: number;
1290
1929
  /** End of the period the usage falls in (provider timestamp anchor). */
1291
1930
  readonly periodEnd?: Date;
1931
+ /** The provider customer the usage is billed to, when one is linked. */
1932
+ readonly providerCustomerId?: string;
1933
+ /**
1934
+ * Names THIS delta: the same flush replayed carries the same key (so the
1935
+ * provider can dedupe it), the next delta carries a new one. A key per
1936
+ * (tenant, key, period) alone would make every later delta a "duplicate".
1937
+ */
1938
+ readonly idempotencyKey?: string;
1292
1939
  }
1293
1940
 
1294
1941
  declare interface UsageRow {