@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.
- package/CHANGELOG.md +61 -0
- package/MIGRATION.md +135 -0
- package/README.md +23 -2
- package/dist/client/billing/checkout.d.ts.map +1 -1
- package/dist/client/billing/checkout.js +22 -8
- package/dist/client/billing/checkout.js.map +1 -1
- package/dist/client/billing/subscriptionRouting.d.ts +24 -0
- package/dist/client/billing/subscriptionRouting.d.ts.map +1 -0
- package/dist/client/billing/subscriptionRouting.js +19 -0
- package/dist/client/billing/subscriptionRouting.js.map +1 -0
- package/dist/client/billing/subscriptions.d.ts +6 -6
- package/dist/client/billing/subscriptions.d.ts.map +1 -1
- package/dist/client/billing/subscriptions.js +31 -12
- package/dist/client/billing/subscriptions.js.map +1 -1
- package/dist/client/connect/disputes.js.map +1 -1
- package/dist/client/constants.js +1 -1
- package/dist/client/core/accountLinks.d.ts +1 -0
- package/dist/client/core/accountLinks.d.ts.map +1 -1
- package/dist/client/core/accountLinks.js +0 -6
- package/dist/client/core/accountLinks.js.map +1 -1
- package/dist/client/core/accountStatus.d.ts.map +1 -1
- package/dist/client/core/accountStatus.js +23 -8
- package/dist/client/core/accountStatus.js.map +1 -1
- package/dist/client/index.d.ts +5 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/types/documents.d.ts +1 -0
- package/dist/client/types/documents.d.ts.map +1 -1
- package/dist/client/types/events.d.ts +2 -1
- package/dist/client/types/events.d.ts.map +1 -1
- package/dist/client/types/options.d.ts +7 -0
- package/dist/client/types/options.d.ts.map +1 -1
- package/dist/client/utils/stripeDashboardUrl.d.ts.map +1 -1
- package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
- package/dist/client/webhooks/handler.d.ts.map +1 -1
- package/dist/client/webhooks/handler.js +35 -1
- package/dist/client/webhooks/handler.js.map +1 -1
- package/dist/client/webhooks/hooks.d.ts +1 -1
- package/dist/client/webhooks/hooks.d.ts.map +1 -1
- package/dist/client/webhooks/hooks.js +5 -1
- package/dist/client/webhooks/hooks.js.map +1 -1
- package/dist/client/webhooks/processors.d.ts.map +1 -1
- package/dist/client/webhooks/processors.js +12 -2
- package/dist/client/webhooks/processors.js.map +1 -1
- package/dist/client/webhooks/v2.d.ts.map +1 -1
- package/dist/client/webhooks/v2.js +11 -3
- package/dist/client/webhooks/v2.js.map +1 -1
- package/dist/component/_generated/component.d.ts +8 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +7 -1
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +2 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/billing/mutations.d.ts +29 -28
- package/dist/component/billing/mutations.d.ts.map +1 -1
- package/dist/component/billing/mutations.js +1 -0
- package/dist/component/billing/mutations.js.map +1 -1
- package/dist/component/billing/queries.d.ts +138 -131
- package/dist/component/billing/queries.d.ts.map +1 -1
- package/dist/component/billing/schema.d.ts +67 -65
- package/dist/component/billing/schema.d.ts.map +1 -1
- package/dist/component/billing/validators.d.ts +94 -91
- package/dist/component/billing/validators.d.ts.map +1 -1
- package/dist/component/billing/validators.js +1 -0
- package/dist/component/billing/validators.js.map +1 -1
- package/dist/component/connect/mutations.d.ts +37 -37
- package/dist/component/connect/queries.d.ts +115 -115
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +34 -71
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/connect/schema.d.ts +73 -73
- package/dist/component/connect/validators.d.ts +119 -119
- package/dist/component/connect/validators.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts +1 -0
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +2 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/core/mutations.d.ts +12 -12
- package/dist/component/core/queries.d.ts +34 -34
- package/dist/component/core/schema.d.ts +11 -11
- package/dist/component/core/validators.d.ts +14 -14
- package/dist/component/core/validators.d.ts.map +1 -1
- package/dist/component/lib/fees.d.ts +12 -12
- package/dist/component/lib/fees.d.ts.map +1 -1
- package/dist/component/lib/fees.js +1 -1
- package/dist/component/lib/reversals.js.map +1 -1
- package/dist/component/products/mutations.d.ts +7 -7
- package/dist/component/products/queries.d.ts +33 -33
- package/dist/component/products/schema.d.ts +15 -15
- package/dist/component/products/validators.d.ts +16 -16
- package/dist/component/products/validators.d.ts.map +1 -1
- package/dist/component/schema.d.ts +173 -171
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/webhooks/mutations.d.ts +3 -3
- package/dist/component/webhooks/queries.d.ts +12 -12
- package/dist/component/webhooks/schema.d.ts +7 -7
- package/dist/component/webhooks/validators.d.ts +10 -10
- package/dist/component/webhooks/validators.d.ts.map +1 -1
- package/dist/react/components/AccountCreateCard.js.map +1 -1
- package/dist/react/components/BuyerBillingView.js +1 -1
- package/dist/react/components/CheckoutSessionProviderWithKey.d.ts +1 -1
- package/dist/react/components/DisputeDetail.js +1 -1
- package/dist/react/components/DisputesList.js +1 -1
- package/dist/react/components/EarningsSummary.js +1 -1
- package/dist/react/components/EmbeddedCheckout.d.ts.map +1 -1
- package/dist/react/components/EmbeddedCheckout.js +1 -1
- package/dist/react/components/EmbeddedCheckout.js.map +1 -1
- package/dist/react/components/PayoutSchedule.js +1 -1
- package/dist/react/components/SplitBreakdown.js +1 -1
- package/dist/react/components/StripeProvider.d.ts +1 -1
- package/dist/react/components/StripeProviderWithKey.d.ts +1 -1
- package/dist/react/hooks/useAccount.d.ts.map +1 -1
- package/dist/react/hooks/useAccountOnboarding.d.ts.map +1 -1
- package/dist/react/hooks/useCheckout.d.ts.map +1 -1
- package/dist/react/hooks/useDisputeWithCountdown.d.ts.map +1 -1
- package/dist/react/hooks/useDisputes.d.ts.map +1 -1
- package/dist/react/hooks/useEarnings.d.ts.map +1 -1
- package/dist/react/hooks/useInvoices.d.ts.map +1 -1
- package/dist/react/hooks/usePaymentMethodActions.js.map +1 -1
- package/dist/react/hooks/usePaymentMethods.d.ts.map +1 -1
- package/dist/react/hooks/usePrices.d.ts.map +1 -1
- package/dist/react/hooks/useProducts.d.ts.map +1 -1
- package/dist/react/hooks/useSplitBreakdown.d.ts.map +1 -1
- package/dist/react/hooks/useSubscription.d.ts.map +1 -1
- package/dist/react/hooks/useSubscriptions.d.ts.map +1 -1
- package/dist/react/lib/stripe-element-styles.d.ts.map +1 -1
- package/dist/react/lib/stripe-element-styles.js.map +1 -1
- package/package.json +25 -23
- package/src/client/billing/checkout.test.ts +62 -14
- package/src/client/billing/checkout.ts +26 -10
- package/src/client/billing/subscriptionRouting.ts +25 -0
- package/src/client/billing/subscriptions.test.ts +90 -28
- package/src/client/billing/subscriptions.ts +53 -18
- package/src/client/constants.ts +1 -1
- package/src/client/core/accountLinks.test.ts +20 -13
- package/src/client/core/accountLinks.ts +2 -10
- package/src/client/core/accountStatus.test.ts +67 -0
- package/src/client/core/accountStatus.ts +30 -13
- package/src/client/helpers.test.ts +2 -2
- package/src/client/index.ts +49 -44
- package/src/client/stripe-version.test.ts +47 -0
- package/src/client/types/documents.ts +1 -0
- package/src/client/types/events.ts +2 -1
- package/src/client/types/options.ts +7 -0
- package/src/client/webhooks/handler.ts +38 -1
- package/src/client/webhooks/hooks.ts +3 -0
- package/src/client/webhooks/processors.test.ts +201 -40
- package/src/client/webhooks/processors.ts +21 -17
- package/src/client/webhooks/retry.integration.test.ts +196 -0
- package/src/client/webhooks/v2.test.ts +36 -0
- package/src/client/webhooks/v2.ts +14 -4
- package/src/component/_generated/component.ts +15 -28
- package/src/component/_generated/server.ts +9 -2
- package/src/component/billing/mutations.ts +1 -0
- package/src/component/billing/validators.ts +1 -0
- package/src/component/config-queries.test.ts +8 -4
- package/src/component/connect/queries.ts +31 -66
- package/src/component/convex.config.ts +2 -1
- package/src/component/earnings-totals.test.ts +27 -1
- package/src/component/lib/fees.ts +1 -1
- package/src/react/components/EmbeddedCheckout.render.test.tsx +57 -0
- 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(
|
|
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) => ({
|
|
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> => ({
|
|
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 (
|
|
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 (
|
|
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 {
|
|
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(
|
|
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("
|
|
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.
|
|
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
|
-
])(
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
whCtx
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
{
|
|
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({
|
|
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
|
-
{
|
|
1028
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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(
|
|
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 {
|
|
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(
|
|
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: {
|
|
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(
|
|
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(
|
|
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] ===
|
|
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] ===
|
|
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({
|
|
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
|
-
|
|
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(
|
|
579
|
-
|
|
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
|
-
|
|
712
|
-
|
|
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
|
-
| "
|
|
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
|
-
| "
|
|
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,
|