@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.
- package/CHANGELOG.md +23 -0
- package/README.md +220 -4
- package/dist/client/billing/balanceFunding.d.ts +36 -0
- package/dist/client/billing/balanceFunding.d.ts.map +1 -0
- package/dist/client/billing/balanceFunding.js +107 -0
- package/dist/client/billing/balanceFunding.js.map +1 -0
- package/dist/client/billing/invoicePreview.d.ts +28 -0
- package/dist/client/billing/invoicePreview.d.ts.map +1 -0
- package/dist/client/billing/invoicePreview.js +56 -0
- package/dist/client/billing/invoicePreview.js.map +1 -0
- package/dist/client/charge-currency-format.d.ts +3 -0
- package/dist/client/charge-currency-format.d.ts.map +1 -0
- package/dist/client/charge-currency-format.js +22 -0
- package/dist/client/charge-currency-format.js.map +1 -0
- package/dist/client/charge-currency.d.ts +14 -0
- package/dist/client/charge-currency.d.ts.map +1 -0
- package/dist/client/charge-currency.js +85 -0
- package/dist/client/charge-currency.js.map +1 -0
- package/dist/client/costRecovery.d.ts +115 -0
- package/dist/client/costRecovery.d.ts.map +1 -0
- package/dist/client/costRecovery.js +50 -0
- package/dist/client/costRecovery.js.map +1 -0
- package/dist/client/index.d.ts +28 -64
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +25 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/products/prices.d.ts +4 -2
- package/dist/client/products/prices.d.ts.map +1 -1
- package/dist/client/products/prices.js +5 -4
- package/dist/client/products/prices.js.map +1 -1
- package/dist/client/products/products.d.ts +4 -2
- package/dist/client/products/products.d.ts.map +1 -1
- package/dist/client/products/products.js +7 -5
- package/dist/client/products/products.js.map +1 -1
- package/dist/client/settlement/processingCosts.d.ts +56 -0
- package/dist/client/settlement/processingCosts.d.ts.map +1 -0
- package/dist/client/settlement/processingCosts.js +173 -0
- package/dist/client/settlement/processingCosts.js.map +1 -0
- package/dist/client/utils/webhookEndpoints.d.ts +1 -1
- package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
- package/dist/client/utils/webhookEndpoints.js +1 -0
- package/dist/client/utils/webhookEndpoints.js.map +1 -1
- package/dist/client/webhooks/processors.d.ts.map +1 -1
- package/dist/client/webhooks/processors.js +1 -0
- package/dist/client/webhooks/processors.js.map +1 -1
- package/dist/component/_generated/component.d.ts +244 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/costRecovery/claims.d.ts +68 -0
- package/dist/component/costRecovery/claims.d.ts.map +1 -0
- package/dist/component/costRecovery/claims.js +85 -0
- package/dist/component/costRecovery/claims.js.map +1 -0
- package/dist/component/costRecovery/completion.d.ts +33 -0
- package/dist/component/costRecovery/completion.d.ts.map +1 -0
- package/dist/component/costRecovery/completion.js +69 -0
- package/dist/component/costRecovery/completion.js.map +1 -0
- package/dist/component/costRecovery/helpers.d.ts +62 -0
- package/dist/component/costRecovery/helpers.d.ts.map +1 -0
- package/dist/component/costRecovery/helpers.js +71 -0
- package/dist/component/costRecovery/helpers.js.map +1 -0
- package/dist/component/costRecovery/mutations.d.ts +138 -0
- package/dist/component/costRecovery/mutations.d.ts.map +1 -0
- package/dist/component/costRecovery/mutations.js +36 -0
- package/dist/component/costRecovery/mutations.js.map +1 -0
- package/dist/component/costRecovery/queries.d.ts +102 -0
- package/dist/component/costRecovery/queries.d.ts.map +1 -0
- package/dist/component/costRecovery/queries.js +73 -0
- package/dist/component/costRecovery/queries.js.map +1 -0
- package/dist/component/costRecovery/reservations.d.ts +83 -0
- package/dist/component/costRecovery/reservations.d.ts.map +1 -0
- package/dist/component/costRecovery/reservations.js +134 -0
- package/dist/component/costRecovery/reservations.js.map +1 -0
- package/dist/component/costRecovery/schema.d.ts +340 -0
- package/dist/component/costRecovery/schema.d.ts.map +1 -0
- package/dist/component/costRecovery/schema.js +109 -0
- package/dist/component/costRecovery/schema.js.map +1 -0
- package/dist/component/schema.d.ts +119 -0
- package/dist/component/schema.js +5 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/react/components/PriceCard.d.ts +3 -1
- package/dist/react/components/PriceCard.d.ts.map +1 -1
- package/dist/react/components/PriceCard.js +2 -2
- package/dist/react/components/PriceCard.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/price-helpers.d.ts +4 -6
- package/dist/react/lib/price-helpers.d.ts.map +1 -1
- package/dist/react/lib/price-helpers.js +16 -9
- package/dist/react/lib/price-helpers.js.map +1 -1
- package/dist/react/workflows/PlanPricingEditor.d.ts +4 -0
- package/dist/react/workflows/PlanPricingEditor.d.ts.map +1 -1
- package/dist/react/workflows/PlanPricingEditor.js +12 -9
- package/dist/react/workflows/PlanPricingEditor.js.map +1 -1
- package/docs/react-workflows.md +91 -0
- package/package.json +2 -2
- package/src/client/billing/balanceFunding.test.ts +257 -0
- package/src/client/billing/balanceFunding.ts +183 -0
- package/src/client/billing/balanceFundingTransport.test.ts +89 -0
- package/src/client/billing/invoicePreview.test.ts +123 -0
- package/src/client/billing/invoicePreview.ts +94 -0
- package/src/client/billing/invoicePreviewTransport.test.ts +70 -0
- package/src/client/charge-currency-format.ts +29 -0
- package/src/client/charge-currency.test.ts +113 -0
- package/src/client/charge-currency.ts +119 -0
- package/src/client/costRecovery.test.ts +76 -0
- package/src/client/costRecovery.ts +149 -0
- package/src/client/index.ts +76 -73
- package/src/client/products/catalog-idempotency.test.ts +116 -0
- package/src/client/products/prices.ts +18 -11
- package/src/client/products/products.ts +17 -11
- package/src/client/settlement/processingCosts.test.ts +349 -0
- package/src/client/settlement/processingCosts.ts +315 -0
- package/src/client/settlement/processingCostsTransport.test.ts +135 -0
- package/src/client/utils/webhookEndpoints.test.ts +5 -0
- package/src/client/utils/webhookEndpoints.ts +1 -0
- package/src/client/webhooks/hooks.test.ts +9 -0
- package/src/client/webhooks/payout-canceled.integration.test.ts +192 -0
- package/src/client/webhooks/processors.ts +1 -0
- package/src/component/_generated/api.ts +12 -0
- package/src/component/_generated/component.ts +308 -0
- package/src/component/cost-recovery.test.ts +344 -0
- package/src/component/costRecovery/claims.ts +109 -0
- package/src/component/costRecovery/completion.ts +92 -0
- package/src/component/costRecovery/helpers.ts +83 -0
- package/src/component/costRecovery/mutations.ts +43 -0
- package/src/component/costRecovery/queries.ts +88 -0
- package/src/component/costRecovery/reservations.ts +164 -0
- package/src/component/costRecovery/schema.ts +127 -0
- package/src/component/schema.ts +10 -0
- package/src/react/components/PayoutSchedule.render.test.tsx +15 -0
- package/src/react/components/PriceCard.render.test.tsx +37 -0
- package/src/react/components/PriceCard.tsx +9 -1
- package/src/react/index.test.ts +5 -0
- package/src/react/index.ts +7 -0
- package/src/react/lib/price-helpers.test.ts +15 -2
- package/src/react/lib/price-helpers.ts +25 -8
- package/src/react/workflows/Billing.test.tsx +1 -1
- package/src/react/workflows/PlanPricingEditor.currency.test.tsx +164 -0
- package/src/react/workflows/PlanPricingEditor.tsx +26 -10
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|