@fantastic.dev/better-stripe 0.8.1 → 0.10.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 (175) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MIGRATION.md +12 -0
  3. package/README.md +46 -2
  4. package/dist/client/billing/checkout.d.ts +1 -0
  5. package/dist/client/billing/checkout.d.ts.map +1 -1
  6. package/dist/client/billing/checkout.js +5 -1
  7. package/dist/client/billing/checkout.js.map +1 -1
  8. package/dist/client/billing/providerReads.d.ts +18 -0
  9. package/dist/client/billing/providerReads.d.ts.map +1 -0
  10. package/dist/client/billing/providerReads.js +15 -0
  11. package/dist/client/billing/providerReads.js.map +1 -0
  12. package/dist/client/billing/subscriptions.d.ts +1 -1
  13. package/dist/client/billing/subscriptions.d.ts.map +1 -1
  14. package/dist/client/billing/subscriptions.js.map +1 -1
  15. package/dist/client/connect/payoutSnapshot.d.ts +28 -0
  16. package/dist/client/connect/payoutSnapshot.d.ts.map +1 -0
  17. package/dist/client/connect/payoutSnapshot.js +81 -0
  18. package/dist/client/connect/payoutSnapshot.js.map +1 -0
  19. package/dist/client/connect/transfers.d.ts.map +1 -1
  20. package/dist/client/connect/transfers.js +13 -32
  21. package/dist/client/connect/transfers.js.map +1 -1
  22. package/dist/client/core/accounts.d.ts +12 -23
  23. package/dist/client/core/accounts.d.ts.map +1 -1
  24. package/dist/client/core/accounts.js +5 -1
  25. package/dist/client/core/accounts.js.map +1 -1
  26. package/dist/client/core/idempotency.d.ts +5 -0
  27. package/dist/client/core/idempotency.d.ts.map +1 -0
  28. package/dist/client/core/idempotency.js +10 -0
  29. package/dist/client/core/idempotency.js.map +1 -0
  30. package/dist/client/index.d.ts +31 -25
  31. package/dist/client/index.d.ts.map +1 -1
  32. package/dist/client/index.js +29 -52
  33. package/dist/client/index.js.map +1 -1
  34. package/dist/client/settlement/allocation.d.ts +30 -0
  35. package/dist/client/settlement/allocation.d.ts.map +1 -0
  36. package/dist/client/settlement/allocation.js +71 -0
  37. package/dist/client/settlement/allocation.js.map +1 -0
  38. package/dist/client/settlement/commissions.d.ts +27 -0
  39. package/dist/client/settlement/commissions.d.ts.map +1 -0
  40. package/dist/client/settlement/commissions.js +69 -0
  41. package/dist/client/settlement/commissions.js.map +1 -0
  42. package/dist/client/settlement/execution.d.ts +27 -0
  43. package/dist/client/settlement/execution.d.ts.map +1 -0
  44. package/dist/client/settlement/execution.js +70 -0
  45. package/dist/client/settlement/execution.js.map +1 -0
  46. package/dist/client/settlement/reconciliation.d.ts +10 -0
  47. package/dist/client/settlement/reconciliation.d.ts.map +1 -0
  48. package/dist/client/settlement/reconciliation.js +73 -0
  49. package/dist/client/settlement/reconciliation.js.map +1 -0
  50. package/dist/client/settlement/settleSale.d.ts +21 -0
  51. package/dist/client/settlement/settleSale.d.ts.map +1 -0
  52. package/dist/client/settlement/settleSale.js +27 -0
  53. package/dist/client/settlement/settleSale.js.map +1 -0
  54. package/dist/client/settlement/verifiedSale.d.ts +25 -0
  55. package/dist/client/settlement/verifiedSale.d.ts.map +1 -0
  56. package/dist/client/settlement/verifiedSale.js +100 -0
  57. package/dist/client/settlement/verifiedSale.js.map +1 -0
  58. package/dist/client/webhooks/asyncHooks.d.ts +5 -0
  59. package/dist/client/webhooks/asyncHooks.d.ts.map +1 -0
  60. package/dist/client/webhooks/asyncHooks.js +20 -0
  61. package/dist/client/webhooks/asyncHooks.js.map +1 -0
  62. package/dist/component/_generated/component.d.ts +115 -0
  63. package/dist/component/_generated/component.d.ts.map +1 -1
  64. package/dist/component/connect/queries.d.ts +30 -1
  65. package/dist/component/connect/queries.d.ts.map +1 -1
  66. package/dist/component/connect/queries.js +19 -2
  67. package/dist/component/connect/queries.js.map +1 -1
  68. package/dist/component/schema.d.ts +67 -0
  69. package/dist/component/schema.js +4 -0
  70. package/dist/component/schema.js.map +1 -1
  71. package/dist/component/settlement/mutations.d.ts +47 -0
  72. package/dist/component/settlement/mutations.d.ts.map +1 -0
  73. package/dist/component/settlement/mutations.js +104 -0
  74. package/dist/component/settlement/mutations.js.map +1 -0
  75. package/dist/component/settlement/queries.d.ts +29 -0
  76. package/dist/component/settlement/queries.d.ts.map +1 -0
  77. package/dist/component/settlement/queries.js +35 -0
  78. package/dist/component/settlement/queries.js.map +1 -0
  79. package/dist/component/settlement/schema.d.ts +99 -0
  80. package/dist/component/settlement/schema.d.ts.map +1 -0
  81. package/dist/component/settlement/schema.js +35 -0
  82. package/dist/component/settlement/schema.js.map +1 -0
  83. package/dist/react/workflows/AffiliatePayoutEditor.d.ts +41 -0
  84. package/dist/react/workflows/AffiliatePayoutEditor.d.ts.map +1 -0
  85. package/dist/react/workflows/AffiliatePayoutEditor.js +80 -0
  86. package/dist/react/workflows/AffiliatePayoutEditor.js.map +1 -0
  87. package/dist/react/workflows/AffiliateRateEditor.d.ts +26 -0
  88. package/dist/react/workflows/AffiliateRateEditor.d.ts.map +1 -0
  89. package/dist/react/workflows/AffiliateRateEditor.js +49 -0
  90. package/dist/react/workflows/AffiliateRateEditor.js.map +1 -0
  91. package/dist/react/workflows/Checkout.d.ts +3 -1
  92. package/dist/react/workflows/Checkout.d.ts.map +1 -1
  93. package/dist/react/workflows/Checkout.js +3 -2
  94. package/dist/react/workflows/Checkout.js.map +1 -1
  95. package/dist/react/workflows/CheckoutForms.d.ts +2 -0
  96. package/dist/react/workflows/CheckoutForms.d.ts.map +1 -1
  97. package/dist/react/workflows/CheckoutForms.js +22 -16
  98. package/dist/react/workflows/CheckoutForms.js.map +1 -1
  99. package/dist/react/workflows/IntegrationSwitch.js +2 -2
  100. package/dist/react/workflows/IntegrationSwitch.js.map +1 -1
  101. package/dist/react/workflows/Pagination.d.ts +32 -0
  102. package/dist/react/workflows/Pagination.d.ts.map +1 -0
  103. package/dist/react/workflows/Pagination.js +49 -0
  104. package/dist/react/workflows/Pagination.js.map +1 -0
  105. package/dist/react/workflows/Payouts.d.ts +3 -18
  106. package/dist/react/workflows/Payouts.d.ts.map +1 -1
  107. package/dist/react/workflows/Payouts.js +2 -2
  108. package/dist/react/workflows/Payouts.js.map +1 -1
  109. package/dist/react/workflows/PlanPricingEditor.d.ts +35 -0
  110. package/dist/react/workflows/PlanPricingEditor.d.ts.map +1 -0
  111. package/dist/react/workflows/PlanPricingEditor.js +64 -0
  112. package/dist/react/workflows/PlanPricingEditor.js.map +1 -0
  113. package/dist/react/workflows/index.d.ts +8 -0
  114. package/dist/react/workflows/index.d.ts.map +1 -1
  115. package/dist/react/workflows/index.js +4 -0
  116. package/dist/react/workflows/index.js.map +1 -1
  117. package/docs/react-workflows-audit.md +19 -0
  118. package/docs/react-workflows.md +94 -3
  119. package/docs/settlement.md +158 -0
  120. package/package.json +6 -4
  121. package/src/client/billing/checkout-idempotency.test.ts +82 -0
  122. package/src/client/billing/checkout.ts +6 -1
  123. package/src/client/billing/providerReads.test.ts +60 -0
  124. package/src/client/billing/providerReads.ts +30 -0
  125. package/src/client/billing/subscriptions.ts +1 -8
  126. package/src/client/connect/payoutSnapshot.test.ts +91 -0
  127. package/src/client/connect/payoutSnapshot.ts +130 -0
  128. package/src/client/connect/transfers.test.ts +15 -4
  129. package/src/client/connect/transfers.ts +13 -42
  130. package/src/client/core/account-idempotency.test.ts +78 -0
  131. package/src/client/core/accounts.ts +18 -25
  132. package/src/client/core/idempotency.test.ts +10 -0
  133. package/src/client/core/idempotency.ts +11 -0
  134. package/src/client/index.ts +84 -103
  135. package/src/client/settlement/allocation.test.ts +98 -0
  136. package/src/client/settlement/allocation.ts +110 -0
  137. package/src/client/settlement/commissions.test.ts +103 -0
  138. package/src/client/settlement/commissions.ts +125 -0
  139. package/src/client/settlement/execution.test.ts +186 -0
  140. package/src/client/settlement/execution.ts +134 -0
  141. package/src/client/settlement/reconciliation.ts +119 -0
  142. package/src/client/settlement/settleSale.ts +59 -0
  143. package/src/client/settlement/verifiedSale.test.ts +139 -0
  144. package/src/client/settlement/verifiedSale.ts +159 -0
  145. package/src/client/webhooks/asyncHooks.ts +38 -0
  146. package/src/client/webhooks/processors.test.ts +9 -9
  147. package/src/component/_generated/api.ts +4 -0
  148. package/src/component/_generated/component.ts +141 -0
  149. package/src/component/connect/queries.ts +26 -2
  150. package/src/component/earnings-totals.test.ts +13 -0
  151. package/src/component/schema.ts +8 -0
  152. package/src/component/settlement/mutations.ts +119 -0
  153. package/src/component/settlement/queries.ts +38 -0
  154. package/src/component/settlement/schema.ts +45 -0
  155. package/src/component/settlement-claims.test.ts +77 -0
  156. package/src/component/transfers.test.ts +48 -0
  157. package/src/react/components/EmbeddedDisputes.render.test.tsx +2 -2
  158. package/src/react/index.payment-element.test.ts +22 -37
  159. package/src/react/workflows/AffiliatePayoutEditor.test.tsx +90 -0
  160. package/src/react/workflows/AffiliatePayoutEditor.tsx +201 -0
  161. package/src/react/workflows/AffiliateRateEditor.test.tsx +36 -0
  162. package/src/react/workflows/AffiliateRateEditor.tsx +92 -0
  163. package/src/react/workflows/Checkout.test.tsx +30 -0
  164. package/src/react/workflows/Checkout.tsx +10 -3
  165. package/src/react/workflows/CheckoutForms.test.tsx +40 -0
  166. package/src/react/workflows/CheckoutForms.tsx +37 -16
  167. package/src/react/workflows/IntegrationSwitch.test.tsx +25 -0
  168. package/src/react/workflows/IntegrationSwitch.tsx +2 -2
  169. package/src/react/workflows/Pagination.test.tsx +53 -0
  170. package/src/react/workflows/Pagination.tsx +93 -0
  171. package/src/react/workflows/Payouts.test.tsx +33 -0
  172. package/src/react/workflows/Payouts.tsx +21 -15
  173. package/src/react/workflows/PlanPricingEditor.test.tsx +79 -0
  174. package/src/react/workflows/PlanPricingEditor.tsx +156 -0
  175. package/src/react/workflows/index.ts +22 -0
@@ -0,0 +1,119 @@
1
+ import { v } from "convex/values";
2
+ import { mutation } from "../_generated/server.js";
3
+ import { allocationFields, commissionFields, termValidator } from "./schema.js";
4
+ const doc = v.object({
5
+ ...allocationFields,
6
+ _id: v.id("settlementAllocations"),
7
+ _creationTime: v.number(),
8
+ });
9
+ export const reserveSettlementAllocation = mutation({
10
+ args: allocationFields,
11
+ returns: doc,
12
+ handler: async (ctx, args) => {
13
+ const existing = await ctx.db
14
+ .query("settlementAllocations")
15
+ .withIndex("by_source_charge", (q) =>
16
+ q.eq("sourceChargeId", args.sourceChargeId),
17
+ )
18
+ .first();
19
+ if (existing) {
20
+ if (
21
+ existing.currency !== args.currency ||
22
+ existing.paymentIntentId !== args.paymentIntentId ||
23
+ existing.sourceInvoiceId !== args.sourceInvoiceId
24
+ )
25
+ throw new Error("Settlement source identity cannot change.");
26
+ return existing;
27
+ }
28
+ const adjustment = args.adjustmentAmount ?? 0;
29
+ const amounts = [
30
+ args.gross,
31
+ args.processingFee,
32
+ args.platformNet,
33
+ ...args.transfers.map((t) => t.amount),
34
+ ];
35
+ const keys = args.transfers.map(
36
+ (t) => `${t.destinationAccountId}:${t.role}`,
37
+ );
38
+ if (
39
+ !Number.isSafeInteger(adjustment) ||
40
+ args.processingFee + args.platformNet + adjustment < 0 ||
41
+ amounts.some((n) => !Number.isSafeInteger(n) || n < 0) ||
42
+ args.gross <= 0 ||
43
+ !args.currency ||
44
+ !args.sourceChargeId ||
45
+ args.transfers.some((t) => !t.destinationAccountId) ||
46
+ new Set(keys).size !== keys.length ||
47
+ args.gross !==
48
+ args.processingFee +
49
+ args.platformNet +
50
+ adjustment +
51
+ args.transfers.reduce((sum, t) => sum + t.amount, 0)
52
+ )
53
+ throw new Error(
54
+ "Settlement allocation must reconcile in integer currency units with unique recipient legs.",
55
+ );
56
+ const id = await ctx.db.insert("settlementAllocations", args);
57
+ return (await ctx.db.get("settlementAllocations", id))!;
58
+ },
59
+ });
60
+ export const claimCommissionInvoice = mutation({
61
+ args: { ...commissionFields, invoiceId: v.string() },
62
+ returns: v.object({
63
+ eligible: v.boolean(),
64
+ term: termValidator,
65
+ firstPaidInvoiceId: v.string(),
66
+ claimKey: v.string(),
67
+ replay: v.boolean(),
68
+ }),
69
+ handler: async (ctx, args) => {
70
+ let stored = await ctx.db
71
+ .query("commissionTerms")
72
+ .withIndex("by_subscription", (q) =>
73
+ q.eq("subscriptionId", args.subscriptionId),
74
+ )
75
+ .first();
76
+ if (stored && stored.affiliateAccountId !== args.affiliateAccountId)
77
+ throw new Error("Subscription affiliate identity cannot change.");
78
+ if (!stored) {
79
+ const fields = {
80
+ subscriptionId: args.subscriptionId,
81
+ term: args.term,
82
+ affiliateAccountId: args.affiliateAccountId,
83
+ firstPaidInvoiceId: args.firstPaidInvoiceId,
84
+ };
85
+ const id = await ctx.db.insert("commissionTerms", fields);
86
+ stored = (await ctx.db.get("commissionTerms", id))!;
87
+ }
88
+ const existing = await ctx.db
89
+ .query("commissionDecisions")
90
+ .withIndex("by_subscription_invoice", (q) =>
91
+ q
92
+ .eq("subscriptionId", args.subscriptionId)
93
+ .eq("invoiceId", args.invoiceId),
94
+ )
95
+ .first();
96
+ const claimKey =
97
+ existing?.claimKey ??
98
+ `commission:${args.subscriptionId}:${args.invoiceId}`;
99
+ const eligible =
100
+ existing?.eligible ??
101
+ (!!stored.affiliateAccountId &&
102
+ (stored.term === "forever" ||
103
+ args.invoiceId === stored.firstPaidInvoiceId));
104
+ if (!existing)
105
+ await ctx.db.insert("commissionDecisions", {
106
+ subscriptionId: args.subscriptionId,
107
+ invoiceId: args.invoiceId,
108
+ eligible,
109
+ claimKey,
110
+ });
111
+ return {
112
+ eligible,
113
+ term: stored.term,
114
+ firstPaidInvoiceId: stored.firstPaidInvoiceId,
115
+ claimKey,
116
+ replay: !!existing,
117
+ };
118
+ },
119
+ });
@@ -0,0 +1,38 @@
1
+ import { v } from "convex/values";
2
+ import { query } from "../_generated/server.js";
3
+ import { allocationFields, commissionFields } from "./schema.js";
4
+ const doc = v.object({
5
+ ...allocationFields,
6
+ _id: v.id("settlementAllocations"),
7
+ _creationTime: v.number(),
8
+ });
9
+ export const getSettlementAllocation = query({
10
+ args: { sourceChargeId: v.string() },
11
+ returns: v.union(doc, v.null()),
12
+ handler: (ctx, args) =>
13
+ ctx.db
14
+ .query("settlementAllocations")
15
+ .withIndex("by_source_charge", (q) =>
16
+ q.eq("sourceChargeId", args.sourceChargeId),
17
+ )
18
+ .first(),
19
+ });
20
+ export const getCommissionTerm = query({
21
+ args: { subscriptionId: v.string() },
22
+ returns: v.union(v.object(commissionFields), v.null()),
23
+ handler: async (ctx, args) => {
24
+ const row = await ctx.db
25
+ .query("commissionTerms")
26
+ .withIndex("by_subscription", (q) =>
27
+ q.eq("subscriptionId", args.subscriptionId),
28
+ )
29
+ .first();
30
+ if (!row) return null;
31
+ return {
32
+ subscriptionId: row.subscriptionId,
33
+ term: row.term,
34
+ affiliateAccountId: row.affiliateAccountId,
35
+ firstPaidInvoiceId: row.firstPaidInvoiceId,
36
+ };
37
+ },
38
+ });
@@ -0,0 +1,45 @@
1
+ import { defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+ import { splitRecipientRoleValidator } from "../lib/fees.js";
4
+ export const termValidator = v.union(
5
+ v.literal("forever"),
6
+ v.literal("one_time"),
7
+ );
8
+ export const allocationFields = {
9
+ sourceChargeId: v.string(),
10
+ paymentIntentId: v.optional(v.string()),
11
+ sourceInvoiceId: v.optional(v.string()),
12
+ gross: v.number(),
13
+ currency: v.string(),
14
+ processingFee: v.number(),
15
+ platformNet: v.number(),
16
+ adjustmentAmount: v.optional(v.number()),
17
+ paymentId: v.optional(v.string()),
18
+ transfers: v.array(
19
+ v.object({
20
+ destinationAccountId: v.string(),
21
+ role: splitRecipientRoleValidator,
22
+ amount: v.number(),
23
+ }),
24
+ ),
25
+ };
26
+ export const allocationsTable = defineTable(allocationFields).index(
27
+ "by_source_charge",
28
+ ["sourceChargeId"],
29
+ );
30
+ export const commissionFields = {
31
+ subscriptionId: v.string(),
32
+ term: termValidator,
33
+ affiliateAccountId: v.optional(v.string()),
34
+ firstPaidInvoiceId: v.string(),
35
+ };
36
+ export const commissionTermsTable = defineTable(commissionFields).index(
37
+ "by_subscription",
38
+ ["subscriptionId"],
39
+ );
40
+ export const commissionDecisionsTable = defineTable({
41
+ subscriptionId: v.string(),
42
+ invoiceId: v.string(),
43
+ eligible: v.boolean(),
44
+ claimKey: v.string(),
45
+ }).index("by_subscription_invoice", ["subscriptionId", "invoiceId"]);
@@ -0,0 +1,77 @@
1
+ // @vitest-environment edge-runtime
2
+ import { convexTest } from "convex-test";
3
+ import { expect, it } from "vitest";
4
+ import { api } from "./_generated/api.js";
5
+ import schema from "./schema.js";
6
+ const modules = import.meta.glob("./**/*.*s");
7
+ it("keeps a claimed allocation immutable and isolated per source charge", async () => {
8
+ const t = convexTest(schema, modules);
9
+ const opts = {
10
+ sourceChargeId: "ch_1",
11
+ paymentIntentId: "pi_1",
12
+ gross: 100,
13
+ currency: "usd",
14
+ processingFee: 3,
15
+ platformNet: 7,
16
+ transfers: [
17
+ { destinationAccountId: "acct_c", amount: 90, role: "store" as const },
18
+ ],
19
+ };
20
+ const first = await t.mutation(
21
+ api.settlement.mutations.reserveSettlementAllocation,
22
+ opts,
23
+ );
24
+ const replay = await t.mutation(
25
+ api.settlement.mutations.reserveSettlementAllocation,
26
+ {
27
+ ...opts,
28
+ transfers: [{ ...opts.transfers[0], amount: 80 }],
29
+ platformNet: 17,
30
+ },
31
+ );
32
+ expect(replay).toEqual(first);
33
+ await expect(
34
+ t.mutation(api.settlement.mutations.reserveSettlementAllocation, {
35
+ ...opts,
36
+ sourceChargeId: "ch_invalid",
37
+ gross: 1,
38
+ }),
39
+ ).rejects.toThrow();
40
+ });
41
+ it("persists the term, identity and earliest invoice despite replay and term changes", async () => {
42
+ const t = convexTest(schema, modules);
43
+ const opts = {
44
+ subscriptionId: "sub_1",
45
+ invoiceId: "in_renewal",
46
+ firstPaidInvoiceId: "in_first",
47
+ term: "one_time" as const,
48
+ affiliateAccountId: "acct_a",
49
+ };
50
+ expect(
51
+ await t.mutation(api.settlement.mutations.claimCommissionInvoice, opts),
52
+ ).toMatchObject({
53
+ eligible: false,
54
+ firstPaidInvoiceId: "in_first",
55
+ term: "one_time",
56
+ replay: false,
57
+ });
58
+ expect(
59
+ await t.mutation(api.settlement.mutations.claimCommissionInvoice, {
60
+ ...opts,
61
+ invoiceId: "in_first",
62
+ term: "forever",
63
+ }),
64
+ ).toMatchObject({ eligible: true, term: "one_time" });
65
+ expect(
66
+ await t.mutation(api.settlement.mutations.claimCommissionInvoice, {
67
+ ...opts,
68
+ invoiceId: "in_first",
69
+ }),
70
+ ).toMatchObject({ eligible: true, replay: true });
71
+ await expect(
72
+ t.mutation(api.settlement.mutations.claimCommissionInvoice, {
73
+ ...opts,
74
+ affiliateAccountId: "acct_other",
75
+ }),
76
+ ).rejects.toThrow();
77
+ });
@@ -469,3 +469,51 @@ describe("claimReversalSlices (BTS-63)", () => {
469
469
  expect(rival.slices).toEqual([]);
470
470
  });
471
471
  });
472
+
473
+ it("paginates an account's newest transfers beyond 50 without duplicates or other accounts", async () => {
474
+ const t = convexTest(schema, modules);
475
+ for (let i = 0; i < 57; i++) {
476
+ await t.mutation(api.connect.mutations.upsertTransfer, {
477
+ stripeTransferId: `tr_page_${i}`,
478
+ destinationAccountId: "acct_pages",
479
+ amount: 100,
480
+ currency: "usd",
481
+ status: "paid",
482
+ });
483
+ }
484
+ await t.mutation(api.connect.mutations.upsertTransfer, {
485
+ stripeTransferId: "tr_foreign",
486
+ destinationAccountId: "acct_other",
487
+ amount: 200,
488
+ currency: "usd",
489
+ status: "paid",
490
+ });
491
+ let cursor: string | null = null;
492
+ const ids: string[] = [];
493
+ const sizes: number[] = [];
494
+ for (;;) {
495
+ const result: {
496
+ page: { stripeTransferId: string }[];
497
+ isDone: boolean;
498
+ continueCursor: string;
499
+ } = await t.query(api.connect.queries.paginateTransfersByAccount, {
500
+ destinationAccountId: "acct_pages",
501
+ paginationOpts: { numItems: 10, cursor },
502
+ });
503
+ sizes.push(result.page.length);
504
+ ids.push(...result.page.map((row) => row.stripeTransferId));
505
+ if (result.isDone) break;
506
+ cursor = result.continueCursor;
507
+ }
508
+ expect(sizes).toEqual([10, 10, 10, 10, 10, 7]);
509
+ expect(ids).toEqual(
510
+ Array.from({ length: 57 }, (_, i) => `tr_page_${56 - i}`),
511
+ );
512
+ expect(new Set(ids).size).toBe(57);
513
+ const empty = await t.query(api.connect.queries.paginateTransfersByAccount, {
514
+ destinationAccountId: "acct_empty",
515
+ paginationOpts: { numItems: 10, cursor: null },
516
+ });
517
+ expect(empty.page).toEqual([]);
518
+ expect(empty.isDone).toBe(true);
519
+ });
@@ -10,6 +10,7 @@
10
10
  * tree rendering without throwing via react-dom/server, the disputes_list vs
11
11
  * payment_disputes mount switch, and the headless overrides.
12
12
  */
13
+ import * as barrel from "../index.js";
13
14
  import { createElement } from "react";
14
15
  import { renderToStaticMarkup } from "react-dom/server";
15
16
  import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -136,8 +137,7 @@ describe("EmbeddedDisputes", () => {
136
137
  });
137
138
 
138
139
  describe("react barrel exports", () => {
139
- it("exports ConnectProvider and EmbeddedDisputes", async () => {
140
- const barrel = await import("../index.js");
140
+ it("exports ConnectProvider and EmbeddedDisputes", () => {
141
141
  expect(barrel.ConnectProvider).toBe(ConnectProvider);
142
142
  expect(barrel.EmbeddedDisputes).toBe(EmbeddedDisputes);
143
143
  });
@@ -1,41 +1,26 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const elements = vi.hoisted(() => ({
4
+ checkout: Symbol("checkout-payment-element"),
5
+ paymentIntent: Symbol("elements-payment-element"),
6
+ }));
7
+ vi.mock("@stripe/react-stripe-js", async () => ({
8
+ ...(await vi.importActual<typeof import("@stripe/react-stripe-js")>(
9
+ "@stripe/react-stripe-js",
10
+ )),
11
+ PaymentElement: elements.paymentIntent,
12
+ }));
13
+ vi.mock("@stripe/react-stripe-js/checkout", async () => ({
14
+ ...(await vi.importActual<typeof import("@stripe/react-stripe-js/checkout")>(
15
+ "@stripe/react-stripe-js/checkout",
16
+ )),
17
+ PaymentElement: elements.checkout,
18
+ }));
19
+ import * as reactExports from "./index.js";
2
20
 
3
21
  describe("better-stripe/react PaymentElement exports", () => {
4
- afterEach(() => {
5
- vi.resetModules();
6
- vi.doUnmock("@stripe/react-stripe-js");
7
- vi.doUnmock("@stripe/react-stripe-js/checkout");
8
- });
9
-
10
- it("uses checkout PaymentElement as the default export and keeps the Elements variant aliased", async () => {
11
- const checkoutPaymentElement = Symbol("checkout-payment-element");
12
- const elementsPaymentElement = Symbol("elements-payment-element");
13
-
14
- vi.doMock("@stripe/react-stripe-js", async () => {
15
- const actual = await vi.importActual<
16
- typeof import("@stripe/react-stripe-js")
17
- >("@stripe/react-stripe-js");
18
-
19
- return {
20
- ...actual,
21
- PaymentElement: elementsPaymentElement,
22
- };
23
- });
24
-
25
- vi.doMock("@stripe/react-stripe-js/checkout", async () => {
26
- const actual = await vi.importActual<
27
- typeof import("@stripe/react-stripe-js/checkout")
28
- >("@stripe/react-stripe-js/checkout");
29
-
30
- return {
31
- ...actual,
32
- PaymentElement: checkoutPaymentElement,
33
- };
34
- });
35
-
36
- const reactExports = await import("./index.js");
37
-
38
- expect(reactExports.PaymentElement).toBe(checkoutPaymentElement);
39
- expect(reactExports.ElementsPaymentElement).toBe(elementsPaymentElement);
22
+ it("uses checkout PaymentElement as the default export and keeps the Elements variant aliased", () => {
23
+ expect(reactExports.PaymentElement).toBe(elements.checkout);
24
+ expect(reactExports.ElementsPaymentElement).toBe(elements.paymentIntent);
40
25
  });
41
26
  });
@@ -0,0 +1,90 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, expect, it, vi } from "vitest";
5
+ import { AffiliatePayoutEditor } from "./AffiliatePayoutEditor.js";
6
+ afterEach(cleanup);
7
+ it("saves either a percentage or a currency amount and clears inheritance", async () => {
8
+ const save = vi.fn().mockResolvedValue(undefined);
9
+ render(
10
+ <AffiliatePayoutEditor
11
+ scopeKey="price"
12
+ affiliateBasisPoints={null}
13
+ effectiveBasisPoints={2000}
14
+ version={0}
15
+ currency="eur"
16
+ onSave={save}
17
+ />,
18
+ );
19
+ const input = screen.getByLabelText("Affiliate payout");
20
+ await userEvent.type(input, "12.5");
21
+ await userEvent.click(screen.getByRole("button", { name: "Save" }));
22
+ expect(save).toHaveBeenLastCalledWith({
23
+ affiliateBasisPoints: 1250,
24
+ affiliateFixedAmount: null,
25
+ affiliateCommissionTerm: null,
26
+ expectedVersion: 0,
27
+ });
28
+ await userEvent.click(
29
+ screen.getByRole("button", { name: "Fixed amount in EUR" }),
30
+ );
31
+ await userEvent.click(screen.getByRole("button", { name: "Save" }));
32
+ expect(save).toHaveBeenLastCalledWith({
33
+ affiliateBasisPoints: null,
34
+ affiliateFixedAmount: 12.5,
35
+ affiliateCommissionTerm: null,
36
+ expectedVersion: 0,
37
+ });
38
+ await userEvent.click(
39
+ screen.getByRole("button", { name: "Clear affiliate payout" }),
40
+ );
41
+ expect(save).toHaveBeenLastCalledWith({
42
+ affiliateBasisPoints: null,
43
+ affiliateFixedAmount: null,
44
+ affiliateCommissionTerm: null,
45
+ expectedVersion: 0,
46
+ });
47
+ expect((input as HTMLInputElement).value).toBe("");
48
+ });
49
+ it("rejects excessive percentages and invalid decimals", async () => {
50
+ const save = vi.fn();
51
+ render(
52
+ <AffiliatePayoutEditor
53
+ scopeKey="product"
54
+ affiliateBasisPoints={null}
55
+ effectiveBasisPoints={2000}
56
+ version={0}
57
+ onSave={save}
58
+ />,
59
+ );
60
+ await userEvent.type(screen.getByLabelText("Affiliate payout"), "101");
61
+ await userEvent.click(screen.getByRole("button", { name: "Save" }));
62
+ expect(screen.getByRole("alert").textContent).toMatch(/100/);
63
+ expect(save).not.toHaveBeenCalled();
64
+ });
65
+ it("saves a one-time subscription commission and clears its term to inheritance", async () => {
66
+ const save = vi.fn().mockResolvedValue(undefined);
67
+ render(
68
+ <AffiliatePayoutEditor
69
+ scopeKey="term"
70
+ affiliateBasisPoints={1000}
71
+ effectiveBasisPoints={1000}
72
+ version={0}
73
+ onSave={save}
74
+ />,
75
+ );
76
+ await userEvent.selectOptions(
77
+ screen.getByLabelText("Commission term"),
78
+ "one_time",
79
+ );
80
+ await userEvent.click(screen.getByRole("button", { name: "Save" }));
81
+ expect(save).toHaveBeenLastCalledWith(
82
+ expect.objectContaining({ affiliateCommissionTerm: "one_time" }),
83
+ );
84
+ await userEvent.click(
85
+ screen.getByRole("button", { name: "Clear affiliate payout" }),
86
+ );
87
+ expect(save).toHaveBeenLastCalledWith(
88
+ expect.objectContaining({ affiliateCommissionTerm: null }),
89
+ );
90
+ });