@fantastic.dev/better-stripe 0.4.0 → 0.6.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.
Files changed (162) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/MIGRATION.md +135 -0
  3. package/README.md +23 -2
  4. package/dist/client/billing/checkout.d.ts.map +1 -1
  5. package/dist/client/billing/checkout.js +22 -8
  6. package/dist/client/billing/checkout.js.map +1 -1
  7. package/dist/client/billing/subscriptionRouting.d.ts +24 -0
  8. package/dist/client/billing/subscriptionRouting.d.ts.map +1 -0
  9. package/dist/client/billing/subscriptionRouting.js +19 -0
  10. package/dist/client/billing/subscriptionRouting.js.map +1 -0
  11. package/dist/client/billing/subscriptions.d.ts +6 -6
  12. package/dist/client/billing/subscriptions.d.ts.map +1 -1
  13. package/dist/client/billing/subscriptions.js +31 -12
  14. package/dist/client/billing/subscriptions.js.map +1 -1
  15. package/dist/client/connect/disputes.js.map +1 -1
  16. package/dist/client/constants.js +1 -1
  17. package/dist/client/core/accountLinks.d.ts +1 -0
  18. package/dist/client/core/accountLinks.d.ts.map +1 -1
  19. package/dist/client/core/accountLinks.js +0 -6
  20. package/dist/client/core/accountLinks.js.map +1 -1
  21. package/dist/client/core/accountStatus.d.ts.map +1 -1
  22. package/dist/client/core/accountStatus.js +23 -8
  23. package/dist/client/core/accountStatus.js.map +1 -1
  24. package/dist/client/index.d.ts +5 -2
  25. package/dist/client/index.d.ts.map +1 -1
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/types/documents.d.ts +1 -0
  28. package/dist/client/types/documents.d.ts.map +1 -1
  29. package/dist/client/types/events.d.ts +2 -1
  30. package/dist/client/types/events.d.ts.map +1 -1
  31. package/dist/client/types/options.d.ts +7 -0
  32. package/dist/client/types/options.d.ts.map +1 -1
  33. package/dist/client/utils/stripeDashboardUrl.d.ts.map +1 -1
  34. package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
  35. package/dist/client/webhooks/handler.d.ts.map +1 -1
  36. package/dist/client/webhooks/handler.js +35 -1
  37. package/dist/client/webhooks/handler.js.map +1 -1
  38. package/dist/client/webhooks/hooks.d.ts +1 -1
  39. package/dist/client/webhooks/hooks.d.ts.map +1 -1
  40. package/dist/client/webhooks/hooks.js +5 -1
  41. package/dist/client/webhooks/hooks.js.map +1 -1
  42. package/dist/client/webhooks/processors.d.ts.map +1 -1
  43. package/dist/client/webhooks/processors.js +12 -2
  44. package/dist/client/webhooks/processors.js.map +1 -1
  45. package/dist/client/webhooks/v2.d.ts.map +1 -1
  46. package/dist/client/webhooks/v2.js +11 -3
  47. package/dist/client/webhooks/v2.js.map +1 -1
  48. package/dist/component/_generated/component.d.ts +8 -0
  49. package/dist/component/_generated/component.d.ts.map +1 -1
  50. package/dist/component/_generated/server.d.ts +7 -1
  51. package/dist/component/_generated/server.d.ts.map +1 -1
  52. package/dist/component/_generated/server.js +2 -1
  53. package/dist/component/_generated/server.js.map +1 -1
  54. package/dist/component/billing/mutations.d.ts +29 -28
  55. package/dist/component/billing/mutations.d.ts.map +1 -1
  56. package/dist/component/billing/mutations.js +1 -0
  57. package/dist/component/billing/mutations.js.map +1 -1
  58. package/dist/component/billing/queries.d.ts +138 -131
  59. package/dist/component/billing/queries.d.ts.map +1 -1
  60. package/dist/component/billing/schema.d.ts +67 -65
  61. package/dist/component/billing/schema.d.ts.map +1 -1
  62. package/dist/component/billing/validators.d.ts +94 -91
  63. package/dist/component/billing/validators.d.ts.map +1 -1
  64. package/dist/component/billing/validators.js +1 -0
  65. package/dist/component/billing/validators.js.map +1 -1
  66. package/dist/component/connect/mutations.d.ts +37 -37
  67. package/dist/component/connect/queries.d.ts +115 -115
  68. package/dist/component/connect/queries.d.ts.map +1 -1
  69. package/dist/component/connect/queries.js +34 -71
  70. package/dist/component/connect/queries.js.map +1 -1
  71. package/dist/component/connect/schema.d.ts +73 -73
  72. package/dist/component/connect/validators.d.ts +119 -119
  73. package/dist/component/connect/validators.d.ts.map +1 -1
  74. package/dist/component/convex.config.d.ts +1 -0
  75. package/dist/component/convex.config.d.ts.map +1 -1
  76. package/dist/component/convex.config.js +2 -1
  77. package/dist/component/convex.config.js.map +1 -1
  78. package/dist/component/core/mutations.d.ts +12 -12
  79. package/dist/component/core/queries.d.ts +34 -34
  80. package/dist/component/core/schema.d.ts +11 -11
  81. package/dist/component/core/validators.d.ts +14 -14
  82. package/dist/component/core/validators.d.ts.map +1 -1
  83. package/dist/component/lib/fees.d.ts +12 -12
  84. package/dist/component/lib/fees.d.ts.map +1 -1
  85. package/dist/component/lib/fees.js +1 -1
  86. package/dist/component/lib/reversals.js.map +1 -1
  87. package/dist/component/products/mutations.d.ts +7 -7
  88. package/dist/component/products/queries.d.ts +33 -33
  89. package/dist/component/products/schema.d.ts +15 -15
  90. package/dist/component/products/validators.d.ts +16 -16
  91. package/dist/component/products/validators.d.ts.map +1 -1
  92. package/dist/component/schema.d.ts +173 -171
  93. package/dist/component/schema.d.ts.map +1 -1
  94. package/dist/component/webhooks/mutations.d.ts +3 -3
  95. package/dist/component/webhooks/queries.d.ts +12 -12
  96. package/dist/component/webhooks/schema.d.ts +7 -7
  97. package/dist/component/webhooks/validators.d.ts +10 -10
  98. package/dist/component/webhooks/validators.d.ts.map +1 -1
  99. package/dist/react/components/AccountCreateCard.js.map +1 -1
  100. package/dist/react/components/BuyerBillingView.js +1 -1
  101. package/dist/react/components/CheckoutSessionProviderWithKey.d.ts +1 -1
  102. package/dist/react/components/DisputeDetail.js +1 -1
  103. package/dist/react/components/DisputesList.js +1 -1
  104. package/dist/react/components/EarningsSummary.js +1 -1
  105. package/dist/react/components/EmbeddedCheckout.d.ts.map +1 -1
  106. package/dist/react/components/EmbeddedCheckout.js +1 -1
  107. package/dist/react/components/EmbeddedCheckout.js.map +1 -1
  108. package/dist/react/components/PayoutSchedule.js +1 -1
  109. package/dist/react/components/SplitBreakdown.js +1 -1
  110. package/dist/react/components/StripeProvider.d.ts +1 -1
  111. package/dist/react/components/StripeProviderWithKey.d.ts +1 -1
  112. package/dist/react/hooks/useAccount.d.ts.map +1 -1
  113. package/dist/react/hooks/useAccountOnboarding.d.ts.map +1 -1
  114. package/dist/react/hooks/useCheckout.d.ts.map +1 -1
  115. package/dist/react/hooks/useDisputeWithCountdown.d.ts.map +1 -1
  116. package/dist/react/hooks/useDisputes.d.ts.map +1 -1
  117. package/dist/react/hooks/useEarnings.d.ts.map +1 -1
  118. package/dist/react/hooks/useInvoices.d.ts.map +1 -1
  119. package/dist/react/hooks/usePaymentMethodActions.js.map +1 -1
  120. package/dist/react/hooks/usePaymentMethods.d.ts.map +1 -1
  121. package/dist/react/hooks/usePrices.d.ts.map +1 -1
  122. package/dist/react/hooks/useProducts.d.ts.map +1 -1
  123. package/dist/react/hooks/useSplitBreakdown.d.ts.map +1 -1
  124. package/dist/react/hooks/useSubscription.d.ts.map +1 -1
  125. package/dist/react/hooks/useSubscriptions.d.ts.map +1 -1
  126. package/dist/react/lib/stripe-element-styles.d.ts.map +1 -1
  127. package/dist/react/lib/stripe-element-styles.js.map +1 -1
  128. package/package.json +25 -23
  129. package/src/client/billing/checkout.test.ts +62 -14
  130. package/src/client/billing/checkout.ts +26 -10
  131. package/src/client/billing/subscriptionRouting.ts +25 -0
  132. package/src/client/billing/subscriptions.test.ts +90 -28
  133. package/src/client/billing/subscriptions.ts +53 -18
  134. package/src/client/constants.ts +1 -1
  135. package/src/client/core/accountLinks.test.ts +20 -13
  136. package/src/client/core/accountLinks.ts +2 -10
  137. package/src/client/core/accountStatus.test.ts +67 -0
  138. package/src/client/core/accountStatus.ts +30 -13
  139. package/src/client/helpers.test.ts +2 -2
  140. package/src/client/index.ts +49 -44
  141. package/src/client/stripe-version.test.ts +47 -0
  142. package/src/client/types/documents.ts +1 -0
  143. package/src/client/types/events.ts +2 -1
  144. package/src/client/types/options.ts +7 -0
  145. package/src/client/webhooks/handler.ts +38 -1
  146. package/src/client/webhooks/hooks.ts +3 -0
  147. package/src/client/webhooks/processors.test.ts +201 -40
  148. package/src/client/webhooks/processors.ts +21 -17
  149. package/src/client/webhooks/retry.integration.test.ts +196 -0
  150. package/src/client/webhooks/v2.test.ts +36 -0
  151. package/src/client/webhooks/v2.ts +14 -4
  152. package/src/component/_generated/component.ts +15 -28
  153. package/src/component/_generated/server.ts +9 -2
  154. package/src/component/billing/mutations.ts +1 -0
  155. package/src/component/billing/validators.ts +1 -0
  156. package/src/component/config-queries.test.ts +8 -4
  157. package/src/component/connect/queries.ts +31 -66
  158. package/src/component/convex.config.ts +2 -1
  159. package/src/component/earnings-totals.test.ts +27 -1
  160. package/src/component/lib/fees.ts +1 -1
  161. package/src/react/components/EmbeddedCheckout.render.test.tsx +57 -0
  162. package/src/react/components/EmbeddedCheckout.tsx +1 -0
@@ -79,7 +79,10 @@ function makeCtx(opts?: { query?: unknown; queryThrows?: boolean }) {
79
79
  legs.map((t) => ({
80
80
  stripeTransferId: t.stripeTransferId,
81
81
  amount: t.amount,
82
- frontier: Math.max(t.reversedAmount ?? 0, t.reversalClaimedAmount ?? 0),
82
+ frontier: Math.max(
83
+ t.reversedAmount ?? 0,
84
+ t.reversalClaimedAmount ?? 0,
85
+ ),
83
86
  })),
84
87
  args.mode as Parameters<typeof computeReversalSlices>[1],
85
88
  );
@@ -121,25 +124,36 @@ function makeStripe() {
121
124
  retrieve: vi.fn(),
122
125
  },
123
126
  transfers: {
124
- create: vi.fn(async (_params: unknown) => ({ id: `tr_${++trCreateSeq}` })),
127
+ create: vi.fn(async (_params: unknown) => ({
128
+ id: `tr_${++trCreateSeq}`,
129
+ })),
125
130
  createReversal: vi.fn(
126
131
  async (
127
132
  _id: unknown,
128
133
  _params?: unknown,
129
134
  _opts?: unknown,
130
- ): Promise<MockReversal> => ({ id: "trr_x", amount: 0, metadata: null }),
135
+ ): Promise<MockReversal> => ({
136
+ id: "trr_x",
137
+ amount: 0,
138
+ metadata: null,
139
+ }),
131
140
  ),
132
141
  // No prior reversals by default; the first-invoice-fee adoption guard
133
142
  // (BTS-69) lists these before reversing.
134
143
  listReversals: vi.fn(
135
- async (_id: unknown, _opts?: unknown): Promise<{ data: MockReversal[] }> => ({
144
+ async (
145
+ _id: unknown,
146
+ _opts?: unknown,
147
+ ): Promise<{ data: MockReversal[] }> => ({
136
148
  data: [],
137
149
  }),
138
150
  ),
139
151
  },
140
152
  invoicePayments: {
141
153
  list: vi.fn(
142
- async (_params: unknown): Promise<{ data: Record<string, unknown>[] }> => ({
154
+ async (
155
+ _params: unknown,
156
+ ): Promise<{ data: Record<string, unknown>[] }> => ({
143
157
  data: [{ payment: { charge: "ch_inv" } }],
144
158
  }),
145
159
  ),
@@ -183,7 +197,10 @@ function event(type: string, object: unknown): Stripe.Event {
183
197
  /** The payload + resolved ref path from the Nth runMutation call. */
184
198
  function dispatchedPayload(ctx: ReturnType<typeof makeCtx>, n = 0) {
185
199
  const call = ctx.runMutation.mock.calls[n];
186
- return { path: call[0][TO_REF] as string, data: call[1] as Record<string, unknown> };
200
+ return {
201
+ path: call[0][TO_REF] as string,
202
+ data: call[1] as Record<string, unknown>,
203
+ };
187
204
  }
188
205
 
189
206
  beforeEach(() => {
@@ -255,7 +272,9 @@ describe("processEvent — prices", () => {
255
272
  // last runMutation is the price upsert (no auto-create happened)
256
273
  const calls = ctx.runMutation.mock.calls;
257
274
  const upsert = calls[calls.length - 1];
258
- expect(upsert[0][TO_REF]).toBe("betterStripe/products/mutations/upsertPrice");
275
+ expect(upsert[0][TO_REF]).toBe(
276
+ "betterStripe/products/mutations/upsertPrice",
277
+ );
259
278
  expect(upsert[1]).toMatchObject({
260
279
  stripePriceId: "price_1",
261
280
  productId: "internal_prod_1",
@@ -352,10 +371,7 @@ describe("processEvent — subscriptions", () => {
352
371
 
353
372
  it("normalizes a created subscription with identifiers, trial, period, and price", async () => {
354
373
  const whCtx = makeWhCtx();
355
- await processEvent(
356
- whCtx,
357
- event("customer.subscription.created", baseSub),
358
- );
374
+ await processEvent(whCtx, event("customer.subscription.created", baseSub));
359
375
 
360
376
  const { path, data } = dispatchedPayload(whCtx.ctx);
361
377
  expect(path).toBe("betterStripe/billing/mutations/upsertSubscription");
@@ -375,12 +391,36 @@ describe("processEvent — subscriptions", () => {
375
391
  );
376
392
  });
377
393
 
378
- it("routes a deleted subscription through the subscriptionDeleted dispatcher", async () => {
394
+ it("tracks collection pause separately from subscription status", async () => {
379
395
  const whCtx = makeWhCtx();
380
396
  await processEvent(
381
397
  whCtx,
382
- event("customer.subscription.deleted", baseSub),
398
+ event("customer.subscription.updated", {
399
+ ...baseSub,
400
+ pause_collection: { behavior: "keep_as_draft" },
401
+ }),
402
+ );
403
+ expect(dispatchedPayload(whCtx.ctx).data).toMatchObject({
404
+ status: "active",
405
+ isCollectionPaused: true,
406
+ });
407
+ const resumed = makeWhCtx();
408
+ await processEvent(
409
+ resumed,
410
+ event("customer.subscription.updated", {
411
+ ...baseSub,
412
+ pause_collection: null,
413
+ }),
383
414
  );
415
+ expect(dispatchedPayload(resumed.ctx).data).toMatchObject({
416
+ status: "active",
417
+ isCollectionPaused: false,
418
+ });
419
+ });
420
+
421
+ it("routes a deleted subscription through the subscriptionDeleted dispatcher", async () => {
422
+ const whCtx = makeWhCtx();
423
+ await processEvent(whCtx, event("customer.subscription.deleted", baseSub));
384
424
  // both dispatchers map to the same upsertSubscription path, so assert the
385
425
  // event still produces exactly one subscription upsert
386
426
  expect(whCtx.ctx.runMutation).toHaveBeenCalledTimes(1);
@@ -414,6 +454,28 @@ describe("processEvent — subscriptions", () => {
414
454
  expect(dispatchedPayload(whCtx.ctx).data.accountId).toBe("acct_v2");
415
455
  });
416
456
 
457
+ it.each(["customer.subscription.created", "customer.subscription.updated"])(
458
+ "preserves checkout destination and platform fee on %s",
459
+ async (type) => {
460
+ const whCtx = makeWhCtx();
461
+ await processEvent(
462
+ whCtx,
463
+ event(type, {
464
+ ...baseSub,
465
+ customer_account: "acct_buyer",
466
+ transfer_data: { destination: "acct_store" },
467
+ application_fee_percent: 10,
468
+ }),
469
+ );
470
+ expect(dispatchedPayload(whCtx.ctx).data).toMatchObject({
471
+ accountId: "acct_buyer",
472
+ chargeType: "destination",
473
+ destinationAccountId: "acct_store",
474
+ applicationFeePercent: 10,
475
+ });
476
+ },
477
+ );
478
+
417
479
  it("falls back to the empty-string userId sentinel when metadata is absent", async () => {
418
480
  const whCtx = makeWhCtx();
419
481
  await processEvent(
@@ -538,6 +600,35 @@ describe("processEvent — checkout", () => {
538
600
  });
539
601
 
540
602
  describe("processEvent — invoices", () => {
603
+ it.each([false, true])(
604
+ "inherits subscription identifiers when invoice metadata is empty (override=%s)",
605
+ async (override) => {
606
+ const whCtx = makeWhCtx();
607
+ await processEvent(
608
+ whCtx,
609
+ event("invoice.paid", {
610
+ id: "in_inherited",
611
+ currency: "usd",
612
+ amount_due: 2900,
613
+ amount_paid: 2900,
614
+ status: "paid",
615
+ metadata: override
616
+ ? { userId: "invoice_user", orgId: "invoice_org" }
617
+ : {},
618
+ parent: {
619
+ subscription_details: {
620
+ subscription: "sub_parent",
621
+ metadata: { userId: "buyer_1", orgId: "store_org" },
622
+ },
623
+ },
624
+ }),
625
+ );
626
+ const { data } = dispatchedPayload(whCtx.ctx);
627
+ expect(data.userId).toBe(override ? "invoice_user" : "buyer_1");
628
+ expect(data.orgId).toBe(override ? "invoice_org" : "store_org");
629
+ },
630
+ );
631
+
541
632
  it("normalizes an invoice and resolves the parent subscription id", async () => {
542
633
  const whCtx = makeWhCtx();
543
634
  await processEvent(
@@ -667,14 +758,17 @@ describe("processEvent — payments (status collapse)", () => {
667
758
  ["requires_confirmation", "requires_action"],
668
759
  ["requires_payment_method", "requires_action"],
669
760
  ["requires_capture", "requires_action"],
670
- ])("maps Stripe status %s to app status %s", async (stripeStatus, expected) => {
671
- const whCtx = makeWhCtx();
672
- await processEvent(
673
- whCtx,
674
- event("payment_intent.succeeded", { ...base, status: stripeStatus }),
675
- );
676
- expect(dispatchedPayload(whCtx.ctx).data.status).toBe(expected);
677
- });
761
+ ])(
762
+ "maps Stripe status %s to app status %s",
763
+ async (stripeStatus, expected) => {
764
+ const whCtx = makeWhCtx();
765
+ await processEvent(
766
+ whCtx,
767
+ event("payment_intent.succeeded", { ...base, status: stripeStatus }),
768
+ );
769
+ expect(dispatchedPayload(whCtx.ctx).data.status).toBe(expected);
770
+ },
771
+ );
678
772
 
679
773
  it("maps any unrecognized status to failed", async () => {
680
774
  const whCtx = makeWhCtx();
@@ -901,7 +995,11 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
901
995
  metadata: {
902
996
  bsChargeType: "separate",
903
997
  bsSplit: JSON.stringify([
904
- { destinationAccountId: "acct_store", role: "store", percent: 90 },
998
+ {
999
+ destinationAccountId: "acct_store",
1000
+ role: "store",
1001
+ percent: 90,
1002
+ },
905
1003
  ]),
906
1004
  },
907
1005
  }),
@@ -1001,7 +1099,10 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
1001
1099
 
1002
1100
  it("does not split a normal (non-separate) subscription invoice", async () => {
1003
1101
  const stripe = makeStripe();
1004
- stripe.subscriptions.retrieve.mockResolvedValue({ id: "sub_1", metadata: {} });
1102
+ stripe.subscriptions.retrieve.mockResolvedValue({
1103
+ id: "sub_1",
1104
+ metadata: {},
1105
+ });
1005
1106
  const whCtx = makeWhCtx({ stripe });
1006
1107
 
1007
1108
  await processEvent(
@@ -1024,8 +1125,18 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
1024
1125
  // The charge funded a store + affiliate transfer.
1025
1126
  const ctx = makeCtx({
1026
1127
  query: [
1027
- { stripeTransferId: "tr_store", destinationAccountId: "acct_store", role: "store", amount: 8000 },
1028
- { stripeTransferId: "tr_aff", destinationAccountId: "acct_aff", role: "affiliate", amount: 1000 },
1128
+ {
1129
+ stripeTransferId: "tr_store",
1130
+ destinationAccountId: "acct_store",
1131
+ role: "store",
1132
+ amount: 8000,
1133
+ },
1134
+ {
1135
+ stripeTransferId: "tr_aff",
1136
+ destinationAccountId: "acct_aff",
1137
+ role: "affiliate",
1138
+ amount: 1000,
1139
+ },
1029
1140
  ],
1030
1141
  });
1031
1142
  const whCtx = makeWhCtx({ stripe, ctx });
@@ -1062,7 +1173,12 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
1062
1173
  const stripe = makeStripe();
1063
1174
  const ctx = makeCtx({
1064
1175
  query: [
1065
- { stripeTransferId: "tr_store", destinationAccountId: "acct_store", role: "store", amount: 8000 },
1176
+ {
1177
+ stripeTransferId: "tr_store",
1178
+ destinationAccountId: "acct_store",
1179
+ role: "store",
1180
+ amount: 8000,
1181
+ },
1066
1182
  ],
1067
1183
  });
1068
1184
  const whCtx = makeWhCtx({ stripe, ctx });
@@ -1088,7 +1204,14 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
1088
1204
  const stripe = makeStripe();
1089
1205
  const ctx = makeCtx({
1090
1206
  query: [
1091
- { stripeTransferId: "tr_store", destinationAccountId: "acct_store", role: "store", amount: 8000, currency: "usd", reversedAmount: 8000 },
1207
+ {
1208
+ stripeTransferId: "tr_store",
1209
+ destinationAccountId: "acct_store",
1210
+ role: "store",
1211
+ amount: 8000,
1212
+ currency: "usd",
1213
+ reversedAmount: 8000,
1214
+ },
1092
1215
  ],
1093
1216
  });
1094
1217
  const whCtx = makeWhCtx({ stripe, ctx });
@@ -1118,7 +1241,14 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
1118
1241
  const stripe = makeStripe();
1119
1242
  const ctx = makeCtx({
1120
1243
  query: [
1121
- { stripeTransferId: "tr_store", destinationAccountId: "acct_store", role: "store", amount: 8000, currency: "usd", reversedAmount: 8000 },
1244
+ {
1245
+ stripeTransferId: "tr_store",
1246
+ destinationAccountId: "acct_store",
1247
+ role: "store",
1248
+ amount: 8000,
1249
+ currency: "usd",
1250
+ reversedAmount: 8000,
1251
+ },
1122
1252
  ],
1123
1253
  });
1124
1254
  const whCtx = makeWhCtx({ stripe, ctx });
@@ -1271,7 +1401,12 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
1271
1401
  stripe.charges.retrieve.mockRejectedValue(new Error("stripe down"));
1272
1402
  const ctx = makeCtx({
1273
1403
  query: [
1274
- { stripeTransferId: "tr_store", destinationAccountId: "acct_store", role: "store", amount: 9000 },
1404
+ {
1405
+ stripeTransferId: "tr_store",
1406
+ destinationAccountId: "acct_store",
1407
+ role: "store",
1408
+ amount: 9000,
1409
+ },
1275
1410
  ],
1276
1411
  });
1277
1412
  const whCtx = makeWhCtx({ stripe, ctx });
@@ -1302,7 +1437,12 @@ describe("processEvent — split transfer engine (BTS-22)", () => {
1302
1437
  const stripe = makeStripe();
1303
1438
  const ctx = makeCtx({
1304
1439
  query: [
1305
- { stripeTransferId: "tr_only", destinationAccountId: "acct_store", role: "store", amount: 9000 },
1440
+ {
1441
+ stripeTransferId: "tr_only",
1442
+ destinationAccountId: "acct_store",
1443
+ role: "store",
1444
+ amount: 9000,
1445
+ },
1306
1446
  ],
1307
1447
  });
1308
1448
  const whCtx = makeWhCtx({ stripe, ctx });
@@ -1482,7 +1622,8 @@ describe("processEvent — per-invoice statement descriptor (BTS-32)", () => {
1482
1622
 
1483
1623
  expect(errSpy).toHaveBeenCalled();
1484
1624
  // The invoice row still lands in the component DB.
1485
- const paths = (whCtx.ctx.runMutation as ReturnType<typeof vi.fn>).mock.calls;
1625
+ const paths = (whCtx.ctx.runMutation as ReturnType<typeof vi.fn>).mock
1626
+ .calls;
1486
1627
  expect(paths.length).toBeGreaterThan(0);
1487
1628
  });
1488
1629
  });
@@ -1544,7 +1685,10 @@ describe("processEvent — per-invoice fixed/tier fee (BTS-51)", () => {
1544
1685
 
1545
1686
  await processEvent(
1546
1687
  whCtx,
1547
- event("invoice.created", flaggedInvoice({ metadata: { bsFeeApplied: "1" } })),
1688
+ event(
1689
+ "invoice.created",
1690
+ flaggedInvoice({ metadata: { bsFeeApplied: "1" } }),
1691
+ ),
1548
1692
  );
1549
1693
  expect(stripe.invoices.update).not.toHaveBeenCalled();
1550
1694
  });
@@ -1830,7 +1974,10 @@ describe("processEvent — first-invoice fee correction (BTS-68)", () => {
1830
1974
  const reversals: MockReversal[] = [];
1831
1975
  stripe.transfers.createReversal.mockImplementation(
1832
1976
  async (id: unknown, params: unknown) => {
1833
- const p = params as { amount: number; metadata?: Record<string, string> };
1977
+ const p = params as {
1978
+ amount: number;
1979
+ metadata?: Record<string, string>;
1980
+ };
1834
1981
  const rev: MockReversal = {
1835
1982
  id: `trr_${reversals.length + 1}`,
1836
1983
  amount: p.amount,
@@ -1850,7 +1997,9 @@ describe("processEvent — first-invoice fee correction (BTS-68)", () => {
1850
1997
  // ── Delivery 1: the reversal SUCCEEDS, then the bsFeeCollected marker write
1851
1998
  // FAILS → the error propagates → the event is marked `failed`. So the money
1852
1999
  // moved (F1=320 reversed) but the invoice was never marked collected.
1853
- stripe.invoices.update.mockRejectedValueOnce(new Error("marker write failed"));
2000
+ stripe.invoices.update.mockRejectedValueOnce(
2001
+ new Error("marker write failed"),
2002
+ );
1854
2003
  await expect(
1855
2004
  processEvent(whCtx, event("invoice.paid", paidFirstInvoice())),
1856
2005
  ).rejects.toThrow("marker write failed");
@@ -1909,7 +2058,10 @@ describe("processEvent — per-charge fixed/tier fee (BTS-60)", () => {
1909
2058
  function wireHappyPath(
1910
2059
  stripe: ReturnType<typeof makeStripe>,
1911
2060
  pi: Record<string, unknown>,
1912
- opts: { amountReceived?: number; freshMetadata?: Record<string, string> } = {},
2061
+ opts: {
2062
+ amountReceived?: number;
2063
+ freshMetadata?: Record<string, string>;
2064
+ } = {},
1913
2065
  ) {
1914
2066
  stripe.paymentIntents.retrieve.mockResolvedValue({
1915
2067
  ...pi,
@@ -2029,7 +2181,9 @@ describe("processEvent — per-charge fixed/tier fee (BTS-60)", () => {
2029
2181
 
2030
2182
  expect(stripe.paymentIntents.retrieve).not.toHaveBeenCalled();
2031
2183
  expect(stripe.transfers.createReversal).not.toHaveBeenCalled();
2032
- expect(dispatchedPayload(whCtx.ctx).data.feeCollectedAmount).toBeUndefined();
2184
+ expect(
2185
+ dispatchedPayload(whCtx.ctx).data.feeCollectedAmount,
2186
+ ).toBeUndefined();
2033
2187
  });
2034
2188
 
2035
2189
  it("swallows malformed bsFeeConfig without reversing or crashing", async () => {
@@ -2072,7 +2226,9 @@ describe("processEvent — per-charge fixed/tier fee (BTS-60)", () => {
2072
2226
  const stripe = makeStripe();
2073
2227
  const pi = flaggedPi();
2074
2228
  wireHappyPath(stripe, pi);
2075
- stripe.paymentIntents.update.mockRejectedValueOnce(new Error("mark failed"));
2229
+ stripe.paymentIntents.update.mockRejectedValueOnce(
2230
+ new Error("mark failed"),
2231
+ );
2076
2232
  const whCtx = makeWhCtx({ stripe });
2077
2233
 
2078
2234
  // Delivery 1: reversal succeeds, marking the PI fails → must throw (the
@@ -2350,7 +2506,8 @@ describe("processEvent — refunds: split clawback + fee-refund denormalization
2350
2506
  const calls = whCtx.ctx.runMutation.mock.calls;
2351
2507
  const feeCall = calls.find(
2352
2508
  (c) =>
2353
- c[0][TO_REF] === "betterStripe/connect/mutations/recordPaymentFeeRefund",
2509
+ c[0][TO_REF] ===
2510
+ "betterStripe/connect/mutations/recordPaymentFeeRefund",
2354
2511
  );
2355
2512
  expect(feeCall).toBeDefined();
2356
2513
  // Absolute cumulative values straight from Stripe — never incremented, so
@@ -2365,7 +2522,8 @@ describe("processEvent — refunds: split clawback + fee-refund denormalization
2365
2522
  await processEvent(whCtx, feeEvent);
2366
2523
  const feeCalls = whCtx.ctx.runMutation.mock.calls.filter(
2367
2524
  (c) =>
2368
- c[0][TO_REF] === "betterStripe/connect/mutations/recordPaymentFeeRefund",
2525
+ c[0][TO_REF] ===
2526
+ "betterStripe/connect/mutations/recordPaymentFeeRefund",
2369
2527
  );
2370
2528
  expect(feeCalls).toHaveLength(2);
2371
2529
  expect(feeCalls[1][1]).toEqual(feeCalls[0][1]);
@@ -2373,7 +2531,10 @@ describe("processEvent — refunds: split clawback + fee-refund denormalization
2373
2531
 
2374
2532
  it("application_fee.refunded skips quietly when the charge has no payment intent", async () => {
2375
2533
  const stripe = makeStripe();
2376
- stripe.charges.retrieve.mockResolvedValue({ id: "ch_1", payment_intent: null });
2534
+ stripe.charges.retrieve.mockResolvedValue({
2535
+ id: "ch_1",
2536
+ payment_intent: null,
2537
+ });
2377
2538
  const whCtx = makeWhCtx({ stripe });
2378
2539
 
2379
2540
  await expect(
@@ -1,3 +1,4 @@
1
+ import { subscriptionRouting } from "../billing/subscriptionRouting.js";
1
2
  import type Stripe from "stripe";
2
3
 
3
4
  import type { SplitRecipient } from "../../component/lib/fees.js";
@@ -234,6 +235,7 @@ async function upsertSubscriptionFromStripe(
234
235
 
235
236
  await dispatchUpsert(whCtx, dispatcherName, {
236
237
  stripeSubscriptionId: subscription.id,
238
+ ...subscriptionRouting(subscription),
237
239
  accountId,
238
240
  userId,
239
241
  orgId,
@@ -243,6 +245,7 @@ async function upsertSubscriptionFromStripe(
243
245
  currentPeriodStart: epochToIso(periodStart),
244
246
  currentPeriodEnd: epochToIso(periodEnd),
245
247
  cancelAtPeriodEnd: subscription.cancel_at_period_end,
248
+ isCollectionPaused: !!subscription.pause_collection,
246
249
  canceledAt: epochToIso(subscription.canceled_at),
247
250
  isTrialing: trial.isTrialing,
248
251
  trialStart: trial.trialStart,
@@ -499,8 +502,8 @@ async function applyFirstInvoiceFee(
499
502
  // so either signal means "already collected". Checking Stripe's own field
500
503
  // (not just our marker) is the authoritative double-collection guard; the SDK
501
504
  // doesn't type it on the Invoice object, hence the narrow read.
502
- const alreadyOnInvoice =
503
- (fresh as { application_fee_amount?: number | null }).application_fee_amount;
505
+ const alreadyOnInvoice = (fresh as { application_fee_amount?: number | null })
506
+ .application_fee_amount;
504
507
  if (freshMeta.bsFeeApplied || alreadyOnInvoice != null) {
505
508
  return undefined;
506
509
  }
@@ -575,9 +578,12 @@ async function applyFirstInvoiceFee(
575
578
  // survives both the marker-write failure and the key TTL, so finding it means
576
579
  // "already collected": re-record the marker with the actually-reversed amount
577
580
  // and return without reversing again.
578
- const priorReversals = await whCtx.stripe.transfers.listReversals(transferId, {
579
- limit: 100,
580
- });
581
+ const priorReversals = await whCtx.stripe.transfers.listReversals(
582
+ transferId,
583
+ {
584
+ limit: 100,
585
+ },
586
+ );
581
587
  const adopted = priorReversals.data.find(
582
588
  (r) => r.metadata?.bsFeeFor === invoice.id,
583
589
  );
@@ -708,9 +714,15 @@ async function upsertInvoiceFromStripe(
708
714
  whCtx: WebhookContext,
709
715
  invoice: Stripe.Invoice,
710
716
  ): Promise<void> {
711
- const { userId, orgId } = extractIdentifiers(
712
- invoice.metadata as Record<string, string> | null,
717
+ // Subscription-generated invoices carry the subscription metadata snapshot
718
+ // on parent.subscription_details, not in invoice.metadata. Prefer explicit
719
+ // invoice identifiers, including their snake_case aliases, when provided.
720
+ const invoiceIdentifiers = extractIdentifiers(invoice.metadata);
721
+ const subscriptionIdentifiers = extractIdentifiers(
722
+ invoice.parent?.subscription_details?.metadata,
713
723
  );
724
+ const userId = invoiceIdentifiers.userId || subscriptionIdentifiers.userId;
725
+ const orgId = invoiceIdentifiers.orgId ?? subscriptionIdentifiers.orgId;
714
726
 
715
727
  const parentSub = invoice.parent?.subscription_details?.subscription;
716
728
  const subscriptionId =
@@ -885,11 +897,7 @@ async function upsertPaymentFromStripe(
885
897
  requires_capture: "requires_action",
886
898
  };
887
899
  const status = (statusMap[paymentIntent.status] ?? "failed") as
888
- | "succeeded"
889
- | "failed"
890
- | "canceled"
891
- | "processing"
892
- | "requires_action";
900
+ "succeeded" | "failed" | "canceled" | "processing" | "requires_action";
893
901
 
894
902
  await dispatchUpsert(whCtx, "paymentUpserted", {
895
903
  stripePaymentIntentId: paymentIntent.id,
@@ -925,11 +933,7 @@ async function handlePayoutEvent(
925
933
  amount: payout.amount,
926
934
  currency: payout.currency,
927
935
  status: payout.status as
928
- | "pending"
929
- | "paid"
930
- | "failed"
931
- | "canceled"
932
- | "in_transit",
936
+ "pending" | "paid" | "failed" | "canceled" | "in_transit",
933
937
  arrivalDate: epochToIso(payout.arrival_date),
934
938
  method: payout.method ?? undefined,
935
939
  metadata: payout.metadata ?? undefined,