@fantastic.dev/better-stripe 0.10.2 → 0.12.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 (140) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +220 -4
  3. package/dist/client/billing/balanceFunding.d.ts +36 -0
  4. package/dist/client/billing/balanceFunding.d.ts.map +1 -0
  5. package/dist/client/billing/balanceFunding.js +107 -0
  6. package/dist/client/billing/balanceFunding.js.map +1 -0
  7. package/dist/client/billing/invoicePreview.d.ts +28 -0
  8. package/dist/client/billing/invoicePreview.d.ts.map +1 -0
  9. package/dist/client/billing/invoicePreview.js +56 -0
  10. package/dist/client/billing/invoicePreview.js.map +1 -0
  11. package/dist/client/charge-currency-format.d.ts +3 -0
  12. package/dist/client/charge-currency-format.d.ts.map +1 -0
  13. package/dist/client/charge-currency-format.js +22 -0
  14. package/dist/client/charge-currency-format.js.map +1 -0
  15. package/dist/client/charge-currency.d.ts +14 -0
  16. package/dist/client/charge-currency.d.ts.map +1 -0
  17. package/dist/client/charge-currency.js +85 -0
  18. package/dist/client/charge-currency.js.map +1 -0
  19. package/dist/client/costRecovery.d.ts +115 -0
  20. package/dist/client/costRecovery.d.ts.map +1 -0
  21. package/dist/client/costRecovery.js +50 -0
  22. package/dist/client/costRecovery.js.map +1 -0
  23. package/dist/client/index.d.ts +28 -64
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +25 -0
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/products/prices.d.ts +4 -2
  28. package/dist/client/products/prices.d.ts.map +1 -1
  29. package/dist/client/products/prices.js +5 -4
  30. package/dist/client/products/prices.js.map +1 -1
  31. package/dist/client/products/products.d.ts +4 -2
  32. package/dist/client/products/products.d.ts.map +1 -1
  33. package/dist/client/products/products.js +7 -5
  34. package/dist/client/products/products.js.map +1 -1
  35. package/dist/client/settlement/processingCosts.d.ts +56 -0
  36. package/dist/client/settlement/processingCosts.d.ts.map +1 -0
  37. package/dist/client/settlement/processingCosts.js +173 -0
  38. package/dist/client/settlement/processingCosts.js.map +1 -0
  39. package/dist/client/utils/webhookEndpoints.d.ts +1 -1
  40. package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
  41. package/dist/client/utils/webhookEndpoints.js +1 -0
  42. package/dist/client/utils/webhookEndpoints.js.map +1 -1
  43. package/dist/client/webhooks/processors.d.ts.map +1 -1
  44. package/dist/client/webhooks/processors.js +1 -0
  45. package/dist/client/webhooks/processors.js.map +1 -1
  46. package/dist/component/_generated/component.d.ts +244 -0
  47. package/dist/component/_generated/component.d.ts.map +1 -1
  48. package/dist/component/costRecovery/claims.d.ts +68 -0
  49. package/dist/component/costRecovery/claims.d.ts.map +1 -0
  50. package/dist/component/costRecovery/claims.js +85 -0
  51. package/dist/component/costRecovery/claims.js.map +1 -0
  52. package/dist/component/costRecovery/completion.d.ts +33 -0
  53. package/dist/component/costRecovery/completion.d.ts.map +1 -0
  54. package/dist/component/costRecovery/completion.js +69 -0
  55. package/dist/component/costRecovery/completion.js.map +1 -0
  56. package/dist/component/costRecovery/helpers.d.ts +62 -0
  57. package/dist/component/costRecovery/helpers.d.ts.map +1 -0
  58. package/dist/component/costRecovery/helpers.js +71 -0
  59. package/dist/component/costRecovery/helpers.js.map +1 -0
  60. package/dist/component/costRecovery/mutations.d.ts +138 -0
  61. package/dist/component/costRecovery/mutations.d.ts.map +1 -0
  62. package/dist/component/costRecovery/mutations.js +36 -0
  63. package/dist/component/costRecovery/mutations.js.map +1 -0
  64. package/dist/component/costRecovery/queries.d.ts +102 -0
  65. package/dist/component/costRecovery/queries.d.ts.map +1 -0
  66. package/dist/component/costRecovery/queries.js +73 -0
  67. package/dist/component/costRecovery/queries.js.map +1 -0
  68. package/dist/component/costRecovery/reservations.d.ts +83 -0
  69. package/dist/component/costRecovery/reservations.d.ts.map +1 -0
  70. package/dist/component/costRecovery/reservations.js +134 -0
  71. package/dist/component/costRecovery/reservations.js.map +1 -0
  72. package/dist/component/costRecovery/schema.d.ts +340 -0
  73. package/dist/component/costRecovery/schema.d.ts.map +1 -0
  74. package/dist/component/costRecovery/schema.js +109 -0
  75. package/dist/component/costRecovery/schema.js.map +1 -0
  76. package/dist/component/schema.d.ts +119 -0
  77. package/dist/component/schema.js +5 -0
  78. package/dist/component/schema.js.map +1 -1
  79. package/dist/react/components/PriceCard.d.ts +3 -1
  80. package/dist/react/components/PriceCard.d.ts.map +1 -1
  81. package/dist/react/components/PriceCard.js +2 -2
  82. package/dist/react/components/PriceCard.js.map +1 -1
  83. package/dist/react/index.d.ts +1 -0
  84. package/dist/react/index.d.ts.map +1 -1
  85. package/dist/react/index.js +1 -0
  86. package/dist/react/index.js.map +1 -1
  87. package/dist/react/lib/price-helpers.d.ts +4 -6
  88. package/dist/react/lib/price-helpers.d.ts.map +1 -1
  89. package/dist/react/lib/price-helpers.js +16 -9
  90. package/dist/react/lib/price-helpers.js.map +1 -1
  91. package/dist/react/workflows/PlanPricingEditor.d.ts +4 -0
  92. package/dist/react/workflows/PlanPricingEditor.d.ts.map +1 -1
  93. package/dist/react/workflows/PlanPricingEditor.js +12 -9
  94. package/dist/react/workflows/PlanPricingEditor.js.map +1 -1
  95. package/docs/react-workflows.md +91 -0
  96. package/package.json +2 -2
  97. package/src/client/billing/balanceFunding.test.ts +257 -0
  98. package/src/client/billing/balanceFunding.ts +183 -0
  99. package/src/client/billing/balanceFundingTransport.test.ts +89 -0
  100. package/src/client/billing/invoicePreview.test.ts +123 -0
  101. package/src/client/billing/invoicePreview.ts +94 -0
  102. package/src/client/billing/invoicePreviewTransport.test.ts +70 -0
  103. package/src/client/charge-currency-format.ts +29 -0
  104. package/src/client/charge-currency.test.ts +113 -0
  105. package/src/client/charge-currency.ts +119 -0
  106. package/src/client/costRecovery.test.ts +76 -0
  107. package/src/client/costRecovery.ts +149 -0
  108. package/src/client/index.ts +76 -73
  109. package/src/client/products/catalog-idempotency.test.ts +116 -0
  110. package/src/client/products/prices.ts +18 -11
  111. package/src/client/products/products.ts +17 -11
  112. package/src/client/settlement/processingCosts.test.ts +349 -0
  113. package/src/client/settlement/processingCosts.ts +315 -0
  114. package/src/client/settlement/processingCostsTransport.test.ts +135 -0
  115. package/src/client/utils/webhookEndpoints.test.ts +5 -0
  116. package/src/client/utils/webhookEndpoints.ts +1 -0
  117. package/src/client/webhooks/hooks.test.ts +9 -0
  118. package/src/client/webhooks/payout-canceled.integration.test.ts +192 -0
  119. package/src/client/webhooks/processors.ts +1 -0
  120. package/src/component/_generated/api.ts +12 -0
  121. package/src/component/_generated/component.ts +308 -0
  122. package/src/component/cost-recovery.test.ts +344 -0
  123. package/src/component/costRecovery/claims.ts +109 -0
  124. package/src/component/costRecovery/completion.ts +92 -0
  125. package/src/component/costRecovery/helpers.ts +83 -0
  126. package/src/component/costRecovery/mutations.ts +43 -0
  127. package/src/component/costRecovery/queries.ts +88 -0
  128. package/src/component/costRecovery/reservations.ts +164 -0
  129. package/src/component/costRecovery/schema.ts +127 -0
  130. package/src/component/schema.ts +10 -0
  131. package/src/react/components/PayoutSchedule.render.test.tsx +15 -0
  132. package/src/react/components/PriceCard.render.test.tsx +37 -0
  133. package/src/react/components/PriceCard.tsx +9 -1
  134. package/src/react/index.test.ts +5 -0
  135. package/src/react/index.ts +7 -0
  136. package/src/react/lib/price-helpers.test.ts +15 -2
  137. package/src/react/lib/price-helpers.ts +25 -8
  138. package/src/react/workflows/Billing.test.tsx +1 -1
  139. package/src/react/workflows/PlanPricingEditor.currency.test.tsx +164 -0
  140. package/src/react/workflows/PlanPricingEditor.tsx +26 -10
@@ -1,3 +1,27 @@
1
+ import { CostRecoveryClient } from "./costRecovery.js";
2
+ export * from "./charge-currency.js";
3
+ export * from "./costRecovery.js";
4
+ import type { SubscriptionStatus } from "../component/billing/validators.js";
5
+ import * as processingCostsImpl from "./settlement/processingCosts.js";
6
+ export type {
7
+ ProcessingCostContext,
8
+ ProcessingCostOptions,
9
+ ProcessingCostTransaction,
10
+ ProcessingCostRecord,
11
+ ProcessingCostNotReadyReason,
12
+ } from "./settlement/processingCosts.js";
13
+ import * as balanceFundingImpl from "./billing/balanceFunding.js";
14
+ export type {
15
+ BalanceFundingCapability,
16
+ BalanceFundingPreparation,
17
+ PrepareBalanceFundingOptions,
18
+ SetBalanceFundingOptions,
19
+ } from "./billing/balanceFunding.js";
20
+ import * as invoicePreviewImpl from "./billing/invoicePreview.js";
21
+ export type {
22
+ UpcomingInvoicePreview,
23
+ UpcomingInvoicePreviewOptions,
24
+ } from "./billing/invoicePreview.js";
1
25
  import type { HttpRouter } from "convex/server";
2
26
  import { registerClientRoutes } from "./previewRoutes.js";
3
27
  import {
@@ -47,10 +71,6 @@ import { v } from "convex/values";
47
71
  import Stripe from "stripe";
48
72
 
49
73
  import type { ComponentApi } from "../component/_generated/component.js";
50
- import type {
51
- InvoiceStatus,
52
- SubscriptionStatus,
53
- } from "../component/billing/validators.js";
54
74
  import type {
55
75
  DisputeStatus,
56
76
  PayoutStatus,
@@ -495,16 +515,7 @@ export class BetterStripe {
495
515
 
496
516
  // PRODUCT METHODS
497
517
 
498
- async createProduct(
499
- ctx: RunCtx,
500
- opts: {
501
- name: string;
502
- description?: string;
503
- active?: boolean;
504
- accountId?: string;
505
- metadata?: Record<string, string>;
506
- },
507
- ) {
518
+ async createProduct(ctx: RunCtx, opts: productsImpl.CreateProductOptions) {
508
519
  return productsImpl.createProduct(this.stripe(), this.component, ctx, {
509
520
  ...opts,
510
521
  // Stamp the per-store descriptor at create so it covers the first invoice
@@ -582,19 +593,7 @@ export class BetterStripe {
582
593
 
583
594
  // PRICE METHODS
584
595
 
585
- async createPrice(
586
- ctx: RunCtx,
587
- opts: {
588
- stripeProductId: string;
589
- unitAmount: number;
590
- currency?: string;
591
- type: "one_time" | "recurring";
592
- interval?: "day" | "week" | "month" | "year";
593
- intervalCount?: number;
594
- nickname?: string;
595
- metadata?: Record<string, string>;
596
- },
597
- ) {
596
+ async createPrice(ctx: RunCtx, opts: pricesImpl.CreatePriceOptions) {
598
597
  return pricesImpl.createPrice(this.stripe(), this.component, ctx, opts);
599
598
  }
600
599
 
@@ -644,19 +643,7 @@ export class BetterStripe {
644
643
 
645
644
  async upsertPrice(
646
645
  ctx: RunCtx,
647
- opts: {
648
- stripePriceId: string;
649
- productId: string;
650
- stripeProductId: string;
651
- nickname?: string;
652
- unitAmount: number;
653
- currency: string;
654
- active: boolean;
655
- type: "one_time" | "recurring";
656
- interval?: "day" | "week" | "month" | "year";
657
- intervalCount?: number;
658
- metadata?: Record<string, unknown>;
659
- },
646
+ opts: Parameters<typeof pricesImpl.upsertPrice>[2],
660
647
  ) {
661
648
  return pricesImpl.upsertPrice(this.component, ctx, opts);
662
649
  }
@@ -954,23 +941,7 @@ export class BetterStripe {
954
941
 
955
942
  async upsertSubscription(
956
943
  ctx: RunCtx,
957
- opts: {
958
- stripeSubscriptionId: string;
959
- accountId?: string;
960
- userId: string;
961
- orgId?: string;
962
- status: SubscriptionStatus;
963
- priceId?: string;
964
- quantity?: number;
965
- currentPeriodStart?: string;
966
- currentPeriodEnd?: string;
967
- cancelAtPeriodEnd: boolean;
968
- canceledAt?: string;
969
- isTrialing: boolean;
970
- trialStart?: string;
971
- trialEnd?: string;
972
- metadata?: Record<string, unknown>;
973
- },
944
+ opts: Parameters<typeof subscriptionsImpl.upsertSubscription>[2],
974
945
  ) {
975
946
  return subscriptionsImpl.upsertSubscription(this.component, ctx, opts);
976
947
  }
@@ -1014,22 +985,7 @@ export class BetterStripe {
1014
985
 
1015
986
  async upsertInvoice(
1016
987
  ctx: RunCtx,
1017
- opts: {
1018
- stripeInvoiceId: string;
1019
- userId: string;
1020
- orgId?: string;
1021
- accountId?: string;
1022
- subscriptionId?: string;
1023
- status: InvoiceStatus;
1024
- currency: string;
1025
- amountDue: number;
1026
- amountPaid: number;
1027
- hostedInvoiceUrl?: string;
1028
- invoicePdf?: string;
1029
- periodStart?: string;
1030
- periodEnd?: string;
1031
- metadata?: Record<string, unknown>;
1032
- },
988
+ opts: Parameters<typeof invoicesImpl.upsertInvoice>[2],
1033
989
  ) {
1034
990
  return invoicesImpl.upsertInvoice(this.component, ctx, opts);
1035
991
  }
@@ -1042,6 +998,42 @@ export class BetterStripe {
1042
998
  return configImpl.getStripeMode(this.component, ctx);
1043
999
  }
1044
1000
 
1001
+ getBalanceFundingCapability() {
1002
+ return balanceFundingImpl.getBalanceFundingCapability(
1003
+ this.stripeApiVersion,
1004
+ );
1005
+ }
1006
+
1007
+ async prepareBalanceFunding(
1008
+ _ctx: RunCtx,
1009
+ opts: balanceFundingImpl.PrepareBalanceFundingOptions,
1010
+ ) {
1011
+ return balanceFundingImpl.prepareBalanceFunding(
1012
+ this.stripe(),
1013
+ this.stripeApiVersion,
1014
+ opts,
1015
+ );
1016
+ }
1017
+
1018
+ async setBalanceFunding(
1019
+ _ctx: RunCtx,
1020
+ opts: balanceFundingImpl.SetBalanceFundingOptions,
1021
+ ) {
1022
+ return balanceFundingImpl.setBalanceFunding(
1023
+ this.stripe(),
1024
+ this.stripeApiVersion,
1025
+ opts,
1026
+ );
1027
+ }
1028
+
1029
+ /** Authorize the customer in the host; validates subscription ownership. */
1030
+ async getUpcomingInvoicePreview(
1031
+ _ctx: RunCtx,
1032
+ opts: invoicePreviewImpl.UpcomingInvoicePreviewOptions,
1033
+ ) {
1034
+ return invoicePreviewImpl.getUpcomingInvoicePreview(this.stripe(), opts);
1035
+ }
1036
+
1045
1037
  async getInvoiceFromStripe(ctx: RunCtx, opts: { stripeInvoiceId: string }) {
1046
1038
  return invoicesImpl.getInvoiceFromStripe(this.stripe(), ctx, opts);
1047
1039
  }
@@ -1272,6 +1264,17 @@ export class BetterStripe {
1272
1264
  ) {
1273
1265
  return providerReads.getCouponFromStripe(this.stripe(), opts);
1274
1266
  }
1267
+ get costRecovery() {
1268
+ return new CostRecoveryClient(this.component);
1269
+ }
1270
+
1271
+ async getProcessingCostContext(
1272
+ _ctx: RunCtx,
1273
+ opts: processingCostsImpl.ProcessingCostOptions,
1274
+ ) {
1275
+ return processingCostsImpl.getProcessingCostContext(this.stripe(), opts);
1276
+ }
1277
+
1275
1278
  async getVerifiedSale(
1276
1279
  _ctx: RunCtx,
1277
1280
  opts: saleContextImpl.VerifiedSaleOptions,
@@ -0,0 +1,116 @@
1
+ import type Stripe from "stripe";
2
+ import { type FunctionReference, componentsGeneric } from "convex/server";
3
+ import { expect, it, vi } from "vitest";
4
+ import schema from "../../component/schema.js";
5
+ import { initConvexTest } from "../setup.test.js";
6
+ import { BetterStripe } from "../index.js";
7
+ import { getStripeClient, type Component, type RunCtx } from "../helpers.js";
8
+ const modules = import.meta.glob("../../component/**/*.{ts,js,tsx,jsx}");
9
+
10
+ it("reuses provider products/prices after persistence failure and keeps old subscription prices", async () => {
11
+ const t = initConvexTest();
12
+ t.registerComponent("betterStripe", schema, modules);
13
+ const component = componentsGeneric().betterStripe as unknown as Component;
14
+ const key = `sk_test_catalog_${crypto.randomUUID()}`;
15
+ const stripe = new BetterStripe(component, { STRIPE_SECRET_KEY: key });
16
+ const sdk = getStripeClient(key);
17
+ const products = new Map<string, Stripe.Product>();
18
+ const prices = new Map<string, Stripe.Price>();
19
+ const productCreate = vi
20
+ .spyOn(sdk.products, "create")
21
+ .mockImplementation(async (params, options) => {
22
+ const id = options?.idempotencyKey ?? crypto.randomUUID();
23
+ if (!products.has(id))
24
+ products.set(id, {
25
+ id: `prod_${products.size}`,
26
+ ...params,
27
+ active: true,
28
+ } as Stripe.Product);
29
+ return products.get(id)! as Stripe.Response<Stripe.Product>;
30
+ });
31
+ const priceCreate = vi
32
+ .spyOn(sdk.prices, "create")
33
+ .mockImplementation(async (params, options) => {
34
+ const id = options?.idempotencyKey ?? crypto.randomUUID();
35
+ if (!prices.has(id))
36
+ prices.set(id, {
37
+ id: `price_${prices.size}`,
38
+ ...params,
39
+ active: true,
40
+ } as unknown as Stripe.Price);
41
+ return prices.get(id)! as Stripe.Response<Stripe.Price>;
42
+ });
43
+ let failNext = true;
44
+ const ctx = {
45
+ runQuery: (
46
+ ref: FunctionReference<"query">,
47
+ args: Record<string, unknown>,
48
+ ) => t.query(ref, args),
49
+ runMutation: (
50
+ ref: FunctionReference<"mutation">,
51
+ args: Record<string, unknown>,
52
+ ) => {
53
+ if (failNext) {
54
+ failNext = false;
55
+ throw new Error("simulated persistence outage");
56
+ }
57
+ return t.mutation(ref, args);
58
+ },
59
+ } as unknown as RunCtx;
60
+ const productOptions = {
61
+ name: "Retry-safe plan",
62
+ idempotencyKey: "catalog:product:plan",
63
+ };
64
+ await expect(stripe.createProduct(ctx, productOptions)).rejects.toThrow();
65
+ const product = await stripe.createProduct(ctx, productOptions);
66
+ expect(products.size).toBe(1);
67
+ expect(productCreate.mock.calls[1][1]).toEqual({
68
+ idempotencyKey: productOptions.idempotencyKey,
69
+ });
70
+ expect(product.productId).not.toBeNull();
71
+
72
+ const priceOptions = {
73
+ stripeProductId: product.stripeProductId,
74
+ unitAmount: 900,
75
+ type: "recurring" as const,
76
+ interval: "month" as const,
77
+ idempotencyKey: "catalog:plan:version:1",
78
+ };
79
+ failNext = true;
80
+ await expect(stripe.createPrice(ctx, priceOptions)).rejects.toThrow();
81
+ const oldPrice = await stripe.createPrice(ctx, priceOptions);
82
+ expect(prices.size).toBe(1);
83
+ expect(priceCreate.mock.calls[1][1]).toEqual({
84
+ idempotencyKey: priceOptions.idempotencyKey,
85
+ });
86
+ await stripe.upsertSubscription(ctx, {
87
+ stripeSubscriptionId: "sub_existing",
88
+ userId: "buyer",
89
+ accountId: "acct_buyer",
90
+ status: "active",
91
+ priceId: oldPrice.stripePriceId,
92
+ cancelAtPeriodEnd: false,
93
+ isTrialing: false,
94
+ });
95
+ const newPrice = await stripe.createPrice(ctx, {
96
+ ...priceOptions,
97
+ unitAmount: 4900,
98
+ idempotencyKey: "catalog:plan:version:2",
99
+ });
100
+ expect(newPrice.stripePriceId).not.toBe(oldPrice.stripePriceId);
101
+ expect(prices.size).toBe(2);
102
+ expect(
103
+ (
104
+ await stripe.getSubscriptionByStripeId(ctx, {
105
+ stripeSubscriptionId: "sub_existing",
106
+ })
107
+ )?.priceId,
108
+ ).toBe(oldPrice.stripePriceId);
109
+ expect(
110
+ (
111
+ await stripe.getPriceByStripeId(ctx, {
112
+ stripePriceId: oldPrice.stripePriceId,
113
+ })
114
+ )?.unitAmount,
115
+ ).toBe(900);
116
+ });
@@ -1,4 +1,5 @@
1
1
  import type Stripe from "stripe";
2
+ import { idempotencyOptions } from "../core/idempotency.js";
2
3
 
3
4
  import type { Component, RunCtx } from "../helpers.js";
4
5
  import { runMutationOrThrow } from "../helpers.js";
@@ -10,20 +11,23 @@ import { componentRef } from "../webhooks/helpers.js";
10
11
  // Price methods
11
12
  // =============================================================================
12
13
 
14
+ export type CreatePriceOptions = {
15
+ stripeProductId: string;
16
+ unitAmount: number;
17
+ currency?: string;
18
+ type: "one_time" | "recurring";
19
+ interval?: "day" | "week" | "month" | "year";
20
+ intervalCount?: number;
21
+ nickname?: string;
22
+ metadata?: Record<string, string>;
23
+ idempotencyKey?: string;
24
+ };
25
+
13
26
  export async function createPrice(
14
27
  stripe: Stripe,
15
28
  component: Component,
16
29
  ctx: RunCtx,
17
- opts: {
18
- stripeProductId: string;
19
- unitAmount: number;
20
- currency?: string;
21
- type: "one_time" | "recurring";
22
- interval?: "day" | "week" | "month" | "year";
23
- intervalCount?: number;
24
- nickname?: string;
25
- metadata?: Record<string, string>;
26
- },
30
+ opts: CreatePriceOptions,
27
31
  ) {
28
32
  // Validate the product exists in the component DB *before* writing to Stripe.
29
33
  // Creating the price first and throwing afterwards would orphan a price in
@@ -54,7 +58,10 @@ export async function createPrice(
54
58
  };
55
59
  }
56
60
 
57
- const price = await stripe.prices.create(priceParams);
61
+ const requestOptions = idempotencyOptions(opts.idempotencyKey);
62
+ const price = requestOptions
63
+ ? await stripe.prices.create(priceParams, requestOptions)
64
+ : await stripe.prices.create(priceParams);
58
65
 
59
66
  await runMutationOrThrow(
60
67
  ctx,
@@ -1,4 +1,5 @@
1
1
  import type Stripe from "stripe";
2
+ import { idempotencyOptions } from "../core/idempotency.js";
2
3
 
3
4
  import type { Component, RunCtx } from "../helpers.js";
4
5
  import { runMutationOrThrow } from "../helpers.js";
@@ -53,19 +54,20 @@ async function resolveProductStatementDescriptor(
53
54
  // Product methods
54
55
  // =============================================================================
55
56
 
57
+ export type CreateProductOptions = {
58
+ name: string;
59
+ description?: string;
60
+ active?: boolean;
61
+ accountId?: string;
62
+ metadata?: Record<string, string>;
63
+ idempotencyKey?: string;
64
+ };
65
+
56
66
  export async function createProduct(
57
67
  stripe: Stripe,
58
68
  component: Component,
59
69
  ctx: RunCtx,
60
- opts: {
61
- name: string;
62
- description?: string;
63
- active?: boolean;
64
- accountId?: string;
65
- metadata?: Record<string, string>;
66
- /** Platform default statement-descriptor suffix (BetterStripe option). */
67
- defaultStatementDescriptorSuffix?: string;
68
- },
70
+ opts: CreateProductOptions & { defaultStatementDescriptorSuffix?: string },
69
71
  ) {
70
72
  const statementDescriptor = await resolveProductStatementDescriptor(
71
73
  component,
@@ -74,7 +76,7 @@ export async function createProduct(
74
76
  opts.defaultStatementDescriptorSuffix,
75
77
  );
76
78
 
77
- const product = await stripe.products.create({
79
+ const params: Stripe.ProductCreateParams = {
78
80
  name: opts.name,
79
81
  description: opts.description ?? undefined,
80
82
  active: opts.active ?? true,
@@ -82,7 +84,11 @@ export async function createProduct(
82
84
  ...(statementDescriptor
83
85
  ? { statement_descriptor: statementDescriptor }
84
86
  : {}),
85
- });
87
+ };
88
+ const requestOptions = idempotencyOptions(opts.idempotencyKey);
89
+ const product = requestOptions
90
+ ? await stripe.products.create(params, requestOptions)
91
+ : await stripe.products.create(params);
86
92
 
87
93
  await runMutationOrThrow(
88
94
  ctx,