@efaj/pulumi-dynamic-stripe 0.0.2-bfb0df3 → 0.1.0-5ac2a7e
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 +8 -0
- package/README.md +16 -0
- package/dist/StripePaymentLink.d.ts +14 -0
- package/dist/StripePaymentLink.js +24 -8
- package/dist/StripePrice.d.ts +43 -2
- package/dist/StripePrice.js +38 -3
- package/dist/StripeProduct.js +2 -2
- package/dist/enums.d.ts +23 -0
- package/dist/enums.js +31 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +26 -0
- package/docs/troubleshooting-stuck-state.md +63 -0
- package/package.json +1 -1
- package/tests/index.test.ts +360 -15
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0](https://github.com/efaj/pulumi-dynamic-stripe/compare/v0.0.2...v0.1.0) (2026-08-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Handle delete case w/o apiKey gracefully ([0639e18](https://github.com/efaj/pulumi-dynamic-stripe/commit/0639e18beff72d6d4fe251be171e2e2e4bc47bda))
|
package/README.md
CHANGED
|
@@ -72,6 +72,14 @@ export const paymentUrl = premiumPaymentLink.url;
|
|
|
72
72
|
|
|
73
73
|
Behind the scenes, this library defines custom Pulumi Dynamic Resource Providers that implement the CRUD operations for Stripe entities via the `@pulumi/pulumi` and `stripe` Node packages.
|
|
74
74
|
|
|
75
|
+
## Resource Deletion & State Fallbacks
|
|
76
|
+
|
|
77
|
+
Stripe entities generally cannot be hard-deleted via the API; instead, this provider safely **deactivates** them (e.g., setting `active: false` on Payment Links, Products, and Prices) when Pulumi destroys the resource.
|
|
78
|
+
|
|
79
|
+
Due to the way Pulumi manages dynamic provider state, if an input property like `apiKey` is not explicitly declared as a public output on the resource class, it may be scrubbed from the state file. When Pulumi attempts to delete a replaced or destroyed resource, it reads from the old state. If the `apiKey` is missing from the state during deletion, the provider will gracefully attempt to fall back to the `STRIPE_SECRET_KEY` or `STRIPE_API_KEY` environment variables.
|
|
80
|
+
|
|
81
|
+
If no API key is found in the state or environment variables, the provider will log a warning and **skip the Stripe deactivation (NoOp)** to allow Pulumi to successfully finish deleting the resource from its internal state without crashing.
|
|
82
|
+
|
|
75
83
|
## Development
|
|
76
84
|
|
|
77
85
|
If you wish to contribute or build the project locally, please use the provided `Makefile`.
|
|
@@ -86,3 +94,11 @@ make build
|
|
|
86
94
|
# Run the Vitest test suite
|
|
87
95
|
make test
|
|
88
96
|
```
|
|
97
|
+
|
|
98
|
+
### Contributing & Releases
|
|
99
|
+
|
|
100
|
+
This project uses [Release Please](https://github.com/googleapis/release-please) to automate versioning, changelog generation, and NPM publishing.
|
|
101
|
+
|
|
102
|
+
When contributing, you **must** format your commit messages using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) (e.g., `feat: add new resource`, `fix: handle edge case`).
|
|
103
|
+
|
|
104
|
+
When your PR is merged into `main`, Release Please will automatically open a "Release PR" bumping the semantic version and updating the `CHANGELOG.md`. Merging that Release PR will trigger the GitHub Action to cut a GitHub Release and publish the new version to NPM.
|
|
@@ -6,6 +6,13 @@ export interface StripePaymentLinkArgs {
|
|
|
6
6
|
redirectUrl: pulumi.Input<string>;
|
|
7
7
|
allowPromotionCodes?: pulumi.Input<boolean>;
|
|
8
8
|
managedPayments?: pulumi.Input<boolean>;
|
|
9
|
+
automaticTax?: pulumi.Input<boolean>;
|
|
10
|
+
quantity?: pulumi.Input<number>;
|
|
11
|
+
adjustableQuantity?: pulumi.Input<{
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
minimum?: number;
|
|
14
|
+
maximum?: number;
|
|
15
|
+
}>;
|
|
9
16
|
}
|
|
10
17
|
export interface StripePaymentLinkProviderArgs {
|
|
11
18
|
apiKey: string;
|
|
@@ -14,6 +21,13 @@ export interface StripePaymentLinkProviderArgs {
|
|
|
14
21
|
redirectUrl: string;
|
|
15
22
|
allowPromotionCodes?: boolean;
|
|
16
23
|
managedPayments?: boolean;
|
|
24
|
+
automaticTax?: boolean;
|
|
25
|
+
quantity?: number;
|
|
26
|
+
adjustableQuantity?: {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
minimum?: number;
|
|
29
|
+
maximum?: number;
|
|
30
|
+
};
|
|
17
31
|
}
|
|
18
32
|
export declare class StripePaymentLinkProvider implements pulumi.dynamic.ResourceProvider {
|
|
19
33
|
create(inputs: StripePaymentLinkProviderArgs): Promise<pulumi.dynamic.CreateResult>;
|
|
@@ -39,18 +39,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.PaymentLink = exports.StripePaymentLinkProvider = void 0;
|
|
40
40
|
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
41
41
|
const stripe_1 = __importDefault(require("stripe"));
|
|
42
|
+
const utils_1 = require("./utils");
|
|
42
43
|
class StripePaymentLinkProvider {
|
|
43
44
|
async create(inputs) {
|
|
44
45
|
const stripe = new stripe_1.default(inputs.apiKey);
|
|
45
46
|
const paymentLink = await stripe.paymentLinks.create({
|
|
46
47
|
line_items: [{
|
|
47
48
|
price: inputs.priceId,
|
|
48
|
-
quantity: 1,
|
|
49
|
-
|
|
50
|
-
enabled
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
quantity: inputs.quantity ?? 1,
|
|
50
|
+
...(inputs.adjustableQuantity !== undefined
|
|
51
|
+
? (inputs.adjustableQuantity.enabled
|
|
52
|
+
? { adjustable_quantity: inputs.adjustableQuantity }
|
|
53
|
+
: {})
|
|
54
|
+
: {
|
|
55
|
+
adjustable_quantity: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
minimum: 1,
|
|
58
|
+
maximum: 100,
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
54
61
|
}],
|
|
55
62
|
after_completion: {
|
|
56
63
|
type: 'redirect',
|
|
@@ -60,6 +67,7 @@ class StripePaymentLinkProvider {
|
|
|
60
67
|
},
|
|
61
68
|
allow_promotion_codes: inputs.allowPromotionCodes,
|
|
62
69
|
...(inputs.managedPayments ? { managed_payments: { enabled: true } } : {}),
|
|
70
|
+
...(inputs.automaticTax ? { automatic_tax: { enabled: true } } : {}),
|
|
63
71
|
metadata: {
|
|
64
72
|
sparks: inputs.sparksAmount
|
|
65
73
|
}
|
|
@@ -85,16 +93,21 @@ class StripePaymentLinkProvider {
|
|
|
85
93
|
replaces.push("allowPromotionCodes");
|
|
86
94
|
if (olds.managedPayments !== news.managedPayments)
|
|
87
95
|
replaces.push("managedPayments");
|
|
96
|
+
if (olds.automaticTax !== news.automaticTax)
|
|
97
|
+
replaces.push("automaticTax");
|
|
88
98
|
if (olds.apiKey !== news.apiKey)
|
|
89
99
|
replaces.push("apiKey");
|
|
100
|
+
if (olds.quantity !== news.quantity)
|
|
101
|
+
replaces.push("quantity");
|
|
102
|
+
if (JSON.stringify(olds.adjustableQuantity) !== JSON.stringify(news.adjustableQuantity))
|
|
103
|
+
replaces.push("adjustableQuantity");
|
|
90
104
|
return {
|
|
91
105
|
changes: replaces.length > 0,
|
|
92
106
|
replaces,
|
|
93
107
|
};
|
|
94
108
|
}
|
|
95
109
|
async delete(id, props) {
|
|
96
|
-
|
|
97
|
-
await stripe.paymentLinks.update(id, { active: false });
|
|
110
|
+
await (0, utils_1.safeDeactivateStripeResource)("StripePaymentLinkProvider", id, props.apiKey, async (stripe) => { await stripe.paymentLinks.update(id, { active: false }); });
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
113
|
exports.StripePaymentLinkProvider = StripePaymentLinkProvider;
|
|
@@ -108,6 +121,9 @@ class PaymentLink extends pulumi.dynamic.Resource {
|
|
|
108
121
|
redirectUrl: args.redirectUrl,
|
|
109
122
|
allowPromotionCodes: args.allowPromotionCodes,
|
|
110
123
|
managedPayments: args.managedPayments,
|
|
124
|
+
automaticTax: args.automaticTax,
|
|
125
|
+
quantity: args.quantity,
|
|
126
|
+
adjustableQuantity: args.adjustableQuantity,
|
|
111
127
|
paymentLinkId: undefined,
|
|
112
128
|
url: undefined,
|
|
113
129
|
}, opts);
|
package/dist/StripePrice.d.ts
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
1
|
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
import { TaxBehavior, BillingScheme, TiersMode, RecurringInterval, UsageType } from "./enums";
|
|
3
|
+
export interface RecurringArgs {
|
|
4
|
+
interval: pulumi.Input<RecurringInterval>;
|
|
5
|
+
intervalCount?: pulumi.Input<number>;
|
|
6
|
+
usageType?: pulumi.Input<UsageType>;
|
|
7
|
+
}
|
|
8
|
+
export interface TierArgs {
|
|
9
|
+
upTo: pulumi.Input<number | "inf">;
|
|
10
|
+
unitAmount?: pulumi.Input<number>;
|
|
11
|
+
flatAmount?: pulumi.Input<number>;
|
|
12
|
+
}
|
|
13
|
+
export interface TransformQuantityArgs {
|
|
14
|
+
divideBy: pulumi.Input<number>;
|
|
15
|
+
round: pulumi.Input<"up" | "down">;
|
|
16
|
+
}
|
|
2
17
|
export interface StripePriceArgs {
|
|
3
18
|
apiKey: pulumi.Input<string>;
|
|
4
19
|
productId: pulumi.Input<string>;
|
|
5
|
-
unitAmount: pulumi.Input<number>;
|
|
6
20
|
currency: pulumi.Input<string>;
|
|
21
|
+
unitAmount?: pulumi.Input<number>;
|
|
22
|
+
taxBehavior?: pulumi.Input<TaxBehavior>;
|
|
23
|
+
recurring?: pulumi.Input<RecurringArgs>;
|
|
24
|
+
billingScheme?: pulumi.Input<BillingScheme>;
|
|
25
|
+
tiersMode?: pulumi.Input<TiersMode>;
|
|
26
|
+
tiers?: pulumi.Input<pulumi.Input<TierArgs>[]>;
|
|
27
|
+
transformQuantity?: pulumi.Input<TransformQuantityArgs>;
|
|
28
|
+
}
|
|
29
|
+
export interface RecurringProviderArgs {
|
|
30
|
+
interval: RecurringInterval;
|
|
31
|
+
intervalCount?: number;
|
|
32
|
+
usageType?: UsageType;
|
|
33
|
+
}
|
|
34
|
+
export interface TierProviderArgs {
|
|
35
|
+
upTo: number | "inf";
|
|
36
|
+
unitAmount?: number;
|
|
37
|
+
flatAmount?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface TransformQuantityProviderArgs {
|
|
40
|
+
divideBy: number;
|
|
41
|
+
round: "up" | "down";
|
|
7
42
|
}
|
|
8
43
|
export interface StripePriceProviderArgs {
|
|
9
44
|
apiKey: string;
|
|
10
45
|
productId: string;
|
|
11
|
-
unitAmount: number;
|
|
12
46
|
currency: string;
|
|
47
|
+
unitAmount?: number;
|
|
48
|
+
taxBehavior?: TaxBehavior;
|
|
49
|
+
recurring?: RecurringProviderArgs;
|
|
50
|
+
billingScheme?: BillingScheme;
|
|
51
|
+
tiersMode?: TiersMode;
|
|
52
|
+
tiers?: TierProviderArgs[];
|
|
53
|
+
transformQuantity?: TransformQuantityProviderArgs;
|
|
13
54
|
}
|
|
14
55
|
export declare class StripePriceProvider implements pulumi.dynamic.ResourceProvider {
|
|
15
56
|
create(inputs: StripePriceProviderArgs): Promise<pulumi.dynamic.CreateResult>;
|
package/dist/StripePrice.js
CHANGED
|
@@ -39,13 +39,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.Price = exports.StripePriceProvider = void 0;
|
|
40
40
|
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
41
41
|
const stripe_1 = __importDefault(require("stripe"));
|
|
42
|
+
const utils_1 = require("./utils");
|
|
42
43
|
class StripePriceProvider {
|
|
43
44
|
async create(inputs) {
|
|
44
45
|
const stripe = new stripe_1.default(inputs.apiKey);
|
|
45
46
|
const price = await stripe.prices.create({
|
|
46
47
|
product: inputs.productId,
|
|
47
|
-
unit_amount: inputs.unitAmount,
|
|
48
48
|
currency: inputs.currency,
|
|
49
|
+
...(inputs.unitAmount !== undefined ? { unit_amount: inputs.unitAmount } : {}),
|
|
50
|
+
tax_behavior: inputs.taxBehavior,
|
|
51
|
+
...(inputs.recurring ? {
|
|
52
|
+
recurring: {
|
|
53
|
+
interval: inputs.recurring.interval,
|
|
54
|
+
interval_count: inputs.recurring.intervalCount,
|
|
55
|
+
usage_type: inputs.recurring.usageType,
|
|
56
|
+
}
|
|
57
|
+
} : {}),
|
|
58
|
+
billing_scheme: inputs.billingScheme,
|
|
59
|
+
tiers_mode: inputs.tiersMode,
|
|
60
|
+
...(inputs.tiers ? {
|
|
61
|
+
tiers: inputs.tiers.map(t => ({
|
|
62
|
+
up_to: t.upTo,
|
|
63
|
+
unit_amount: t.unitAmount,
|
|
64
|
+
flat_amount: t.flatAmount,
|
|
65
|
+
}))
|
|
66
|
+
} : {}),
|
|
67
|
+
...(inputs.transformQuantity ? {
|
|
68
|
+
transform_quantity: {
|
|
69
|
+
divide_by: inputs.transformQuantity.divideBy,
|
|
70
|
+
round: inputs.transformQuantity.round,
|
|
71
|
+
}
|
|
72
|
+
} : {}),
|
|
49
73
|
});
|
|
50
74
|
return {
|
|
51
75
|
id: price.id,
|
|
@@ -63,6 +87,18 @@ class StripePriceProvider {
|
|
|
63
87
|
replaces.push("unitAmount");
|
|
64
88
|
if (olds.currency !== news.currency)
|
|
65
89
|
replaces.push("currency");
|
|
90
|
+
if (olds.taxBehavior !== news.taxBehavior)
|
|
91
|
+
replaces.push("taxBehavior");
|
|
92
|
+
if (JSON.stringify(olds.recurring) !== JSON.stringify(news.recurring))
|
|
93
|
+
replaces.push("recurring");
|
|
94
|
+
if (olds.billingScheme !== news.billingScheme)
|
|
95
|
+
replaces.push("billingScheme");
|
|
96
|
+
if (olds.tiersMode !== news.tiersMode)
|
|
97
|
+
replaces.push("tiersMode");
|
|
98
|
+
if (JSON.stringify(olds.tiers) !== JSON.stringify(news.tiers))
|
|
99
|
+
replaces.push("tiers");
|
|
100
|
+
if (JSON.stringify(olds.transformQuantity) !== JSON.stringify(news.transformQuantity))
|
|
101
|
+
replaces.push("transformQuantity");
|
|
66
102
|
if (olds.apiKey !== news.apiKey)
|
|
67
103
|
replaces.push("apiKey");
|
|
68
104
|
return {
|
|
@@ -71,8 +107,7 @@ class StripePriceProvider {
|
|
|
71
107
|
};
|
|
72
108
|
}
|
|
73
109
|
async delete(id, props) {
|
|
74
|
-
|
|
75
|
-
await stripe.prices.update(id, { active: false });
|
|
110
|
+
await (0, utils_1.safeDeactivateStripeResource)("StripePriceProvider", id, props.apiKey, async (stripe) => { await stripe.prices.update(id, { active: false }); });
|
|
76
111
|
}
|
|
77
112
|
}
|
|
78
113
|
exports.StripePriceProvider = StripePriceProvider;
|
package/dist/StripeProduct.js
CHANGED
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.Product = exports.StripeProductProvider = void 0;
|
|
40
40
|
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
41
41
|
const stripe_1 = __importDefault(require("stripe"));
|
|
42
|
+
const utils_1 = require("./utils");
|
|
42
43
|
class StripeProductProvider {
|
|
43
44
|
async create(inputs) {
|
|
44
45
|
const stripe = new stripe_1.default(inputs.apiKey);
|
|
@@ -85,8 +86,7 @@ class StripeProductProvider {
|
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
async delete(id, props) {
|
|
88
|
-
|
|
89
|
-
await stripe.products.update(id, { active: false });
|
|
89
|
+
await (0, utils_1.safeDeactivateStripeResource)("StripeProductProvider", id, props.apiKey, async (stripe) => { await stripe.products.update(id, { active: false }); });
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
exports.StripeProductProvider = StripeProductProvider;
|
package/dist/enums.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare enum TaxBehavior {
|
|
2
|
+
Inclusive = "inclusive",
|
|
3
|
+
Exclusive = "exclusive",
|
|
4
|
+
Unspecified = "unspecified"
|
|
5
|
+
}
|
|
6
|
+
export declare enum BillingScheme {
|
|
7
|
+
PerUnit = "per_unit",
|
|
8
|
+
Tiered = "tiered"
|
|
9
|
+
}
|
|
10
|
+
export declare enum TiersMode {
|
|
11
|
+
Graduated = "graduated",
|
|
12
|
+
Volume = "volume"
|
|
13
|
+
}
|
|
14
|
+
export declare enum RecurringInterval {
|
|
15
|
+
Day = "day",
|
|
16
|
+
Week = "week",
|
|
17
|
+
Month = "month",
|
|
18
|
+
Year = "year"
|
|
19
|
+
}
|
|
20
|
+
export declare enum UsageType {
|
|
21
|
+
Metered = "metered",
|
|
22
|
+
Licensed = "licensed"
|
|
23
|
+
}
|
package/dist/enums.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UsageType = exports.RecurringInterval = exports.TiersMode = exports.BillingScheme = exports.TaxBehavior = void 0;
|
|
4
|
+
var TaxBehavior;
|
|
5
|
+
(function (TaxBehavior) {
|
|
6
|
+
TaxBehavior["Inclusive"] = "inclusive";
|
|
7
|
+
TaxBehavior["Exclusive"] = "exclusive";
|
|
8
|
+
TaxBehavior["Unspecified"] = "unspecified";
|
|
9
|
+
})(TaxBehavior || (exports.TaxBehavior = TaxBehavior = {}));
|
|
10
|
+
var BillingScheme;
|
|
11
|
+
(function (BillingScheme) {
|
|
12
|
+
BillingScheme["PerUnit"] = "per_unit";
|
|
13
|
+
BillingScheme["Tiered"] = "tiered";
|
|
14
|
+
})(BillingScheme || (exports.BillingScheme = BillingScheme = {}));
|
|
15
|
+
var TiersMode;
|
|
16
|
+
(function (TiersMode) {
|
|
17
|
+
TiersMode["Graduated"] = "graduated";
|
|
18
|
+
TiersMode["Volume"] = "volume";
|
|
19
|
+
})(TiersMode || (exports.TiersMode = TiersMode = {}));
|
|
20
|
+
var RecurringInterval;
|
|
21
|
+
(function (RecurringInterval) {
|
|
22
|
+
RecurringInterval["Day"] = "day";
|
|
23
|
+
RecurringInterval["Week"] = "week";
|
|
24
|
+
RecurringInterval["Month"] = "month";
|
|
25
|
+
RecurringInterval["Year"] = "year";
|
|
26
|
+
})(RecurringInterval || (exports.RecurringInterval = RecurringInterval = {}));
|
|
27
|
+
var UsageType;
|
|
28
|
+
(function (UsageType) {
|
|
29
|
+
UsageType["Metered"] = "metered";
|
|
30
|
+
UsageType["Licensed"] = "licensed";
|
|
31
|
+
})(UsageType || (exports.UsageType = UsageType = {}));
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./StripePaymentLink"), exports);
|
|
18
18
|
__exportStar(require("./StripeProduct"), exports);
|
|
19
19
|
__exportStar(require("./StripePrice"), exports);
|
|
20
|
+
__exportStar(require("./enums"), exports);
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
/**
|
|
3
|
+
* Safely deactivates a Stripe resource during Pulumi deletion.
|
|
4
|
+
* Falls back to environment variables if the apiKey is missing from state.
|
|
5
|
+
* Prevents Pulumi state engine crashes when a resource cannot be deactivated.
|
|
6
|
+
*/
|
|
7
|
+
export declare function safeDeactivateStripeResource(providerName: string, resourceId: string, apiKeyFromState: string | undefined, deactivateFn: (stripe: Stripe) => Promise<void>): Promise<void>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.safeDeactivateStripeResource = safeDeactivateStripeResource;
|
|
7
|
+
const stripe_1 = __importDefault(require("stripe"));
|
|
8
|
+
/**
|
|
9
|
+
* Safely deactivates a Stripe resource during Pulumi deletion.
|
|
10
|
+
* Falls back to environment variables if the apiKey is missing from state.
|
|
11
|
+
* Prevents Pulumi state engine crashes when a resource cannot be deactivated.
|
|
12
|
+
*/
|
|
13
|
+
async function safeDeactivateStripeResource(providerName, resourceId, apiKeyFromState, deactivateFn) {
|
|
14
|
+
const apiKey = apiKeyFromState || process.env.STRIPE_SECRET_KEY || process.env.STRIPE_API_KEY;
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
console.warn(`[${providerName}] Skipping deletion of ${resourceId} due to missing apiKey in state and environment variables.`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const stripe = new stripe_1.default(apiKey);
|
|
21
|
+
await deactivateFn(stripe);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.warn(`[${providerName}] Failed to deactivate ${resourceId}:`, error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Troubleshooting Stuck State in Pulumi Dynamic Providers
|
|
2
|
+
|
|
3
|
+
## The Problem
|
|
4
|
+
|
|
5
|
+
When using `pulumi-dynamic-stripe` versions **0.0.1** and **0.0.2**, you might encounter an issue where Pulumi fails to delete a Stripe resource (e.g., a Payment Link) during a `pulumi up`, `pulumi destroy`, or replacement operation.
|
|
6
|
+
|
|
7
|
+
The stack will show the resource as `deleting failed` with an error similar to this:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Error: Neither apiKey nor config.authenticator provided
|
|
11
|
+
at Stripe._setAuthenticator (node_modules/stripe/src/stripe.core.ts:1319:13)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Why This Happens
|
|
15
|
+
|
|
16
|
+
This is fundamentally caused by how Pulumi Dynamic Providers function under the hood.
|
|
17
|
+
|
|
18
|
+
When you deploy a resource using a Dynamic Provider, Pulumi takes the **JavaScript source code of your provider methods** (like `create`, `update`, `diff`, and importantly `delete`) and **serializes the compiled code directly into the Pulumi state file in the cloud**.
|
|
19
|
+
|
|
20
|
+
In versions `0.0.1` and `0.0.2` of this library, the `delete` method had a bug where it crashed if it couldn't find an API key in the Pulumi state (due to how state diffs and replacement work). Although we patched this bug in the library's source code by adding environment variable fallbacks and `try/catch` handlers, **Pulumi does not use your newly updated local code to delete older resources.**
|
|
21
|
+
|
|
22
|
+
Instead, Pulumi downloads the *old, buggy, serialized `delete` function* that was saved in the cloud during the resource's initial creation, and attempts to run it. When that old code inevitably crashes, the resource gets stuck in a perpetual `deleting failed` state.
|
|
23
|
+
|
|
24
|
+
## The Solution
|
|
25
|
+
|
|
26
|
+
Since you cannot fix an old resource's deletion behavior by updating your local provider package, you must manually excise the corrupted resource from the Pulumi state.
|
|
27
|
+
|
|
28
|
+
### Step 1: Export the Pulumi State
|
|
29
|
+
First, export your stack's state to a local JSON file:
|
|
30
|
+
```bash
|
|
31
|
+
pulumi stack export > state.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Step 2: Identify and Remove the Corrupted Resource
|
|
35
|
+
Open `state.json` in a text editor and search for the resource that is stuck. You are looking for a JSON object in the `deployment.resources` array that matches your resource's URN and has `"delete": true` set.
|
|
36
|
+
|
|
37
|
+
For example, look for something like this and **delete the entire JSON object from the array**:
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"urn": "urn:pulumi:prod.quiz.crenet-games::quizai-infrastructure::pulumi-nodejs:dynamic:Resource::sparks-link",
|
|
41
|
+
"custom": true,
|
|
42
|
+
"delete": true,
|
|
43
|
+
"id": "plink_1U8pkf1i5elBtkZIXxmnduNm",
|
|
44
|
+
"type": "pulumi-nodejs:dynamic:Resource",
|
|
45
|
+
"inputs": { ... }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
*Alternatively, you can script this step using Node or `jq`.*
|
|
50
|
+
|
|
51
|
+
### Step 3: Import the Fixed State
|
|
52
|
+
Once the offending resource object is removed from the JSON array, save the file and import it back to Pulumi:
|
|
53
|
+
```bash
|
|
54
|
+
pulumi stack import --file state.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Step 4: Re-run Pulumi Up
|
|
58
|
+
Now that the stuck state is gone, Pulumi will no longer attempt to run the broken deletion hook. Run your update command:
|
|
59
|
+
```bash
|
|
60
|
+
pulumi up -y
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Moving forward, any new resources created by the updated provider will have the new, robust, crash-free `delete` function serialized into the state, permanently solving this issue.
|
package/package.json
CHANGED
package/tests/index.test.ts
CHANGED
|
@@ -62,6 +62,7 @@ describe("Stripe Providers", () => {
|
|
|
62
62
|
redirectUrl: "https://example.com/success",
|
|
63
63
|
allowPromotionCodes: true,
|
|
64
64
|
managedPayments: true,
|
|
65
|
+
automaticTax: true,
|
|
65
66
|
},
|
|
66
67
|
expectedPayload: {
|
|
67
68
|
line_items: [{
|
|
@@ -72,6 +73,7 @@ describe("Stripe Providers", () => {
|
|
|
72
73
|
after_completion: { type: 'redirect', redirect: { url: "https://example.com/success" } },
|
|
73
74
|
allow_promotion_codes: true,
|
|
74
75
|
managed_payments: { enabled: true },
|
|
76
|
+
automatic_tax: { enabled: true },
|
|
75
77
|
metadata: { sparks: "100" }
|
|
76
78
|
}
|
|
77
79
|
},
|
|
@@ -93,6 +95,27 @@ describe("Stripe Providers", () => {
|
|
|
93
95
|
allow_promotion_codes: undefined,
|
|
94
96
|
metadata: { sparks: "50" }
|
|
95
97
|
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "custom quantity and adjustableQuantity provided",
|
|
101
|
+
inputs: {
|
|
102
|
+
apiKey: "sk_test_123",
|
|
103
|
+
priceId: "price_abc",
|
|
104
|
+
sparksAmount: "100",
|
|
105
|
+
redirectUrl: "https://example.com/success",
|
|
106
|
+
quantity: 5,
|
|
107
|
+
adjustableQuantity: { enabled: true, maximum: 5 }
|
|
108
|
+
},
|
|
109
|
+
expectedPayload: {
|
|
110
|
+
line_items: [{
|
|
111
|
+
price: "price_abc",
|
|
112
|
+
quantity: 5,
|
|
113
|
+
adjustable_quantity: { enabled: true, maximum: 5 },
|
|
114
|
+
}],
|
|
115
|
+
after_completion: { type: 'redirect', redirect: { url: "https://example.com/success" } },
|
|
116
|
+
allow_promotion_codes: undefined,
|
|
117
|
+
metadata: { sparks: "100" }
|
|
118
|
+
}
|
|
96
119
|
}
|
|
97
120
|
];
|
|
98
121
|
|
|
@@ -112,10 +135,10 @@ describe("Stripe Providers", () => {
|
|
|
112
135
|
const diffCases = [
|
|
113
136
|
{
|
|
114
137
|
name: "all properties change",
|
|
115
|
-
olds: { priceId: "old", sparksAmount: "10", redirectUrl: "url1", allowPromotionCodes: false, managedPayments: false, apiKey: "old_key" },
|
|
116
|
-
news: { priceId: "new", sparksAmount: "20", redirectUrl: "url2", allowPromotionCodes: true, managedPayments: true, apiKey: "new_key" },
|
|
138
|
+
olds: { priceId: "old", sparksAmount: "10", redirectUrl: "url1", allowPromotionCodes: false, managedPayments: false, automaticTax: false, quantity: 1, adjustableQuantity: { enabled: true }, apiKey: "old_key" },
|
|
139
|
+
news: { priceId: "new", sparksAmount: "20", redirectUrl: "url2", allowPromotionCodes: true, managedPayments: true, automaticTax: true, quantity: 2, adjustableQuantity: { enabled: false }, apiKey: "new_key" },
|
|
117
140
|
expectedChanges: true,
|
|
118
|
-
expectedReplaces: ["priceId", "sparksAmount", "redirectUrl", "allowPromotionCodes", "managedPayments", "apiKey"]
|
|
141
|
+
expectedReplaces: ["priceId", "sparksAmount", "redirectUrl", "allowPromotionCodes", "managedPayments", "automaticTax", "apiKey", "quantity", "adjustableQuantity"]
|
|
119
142
|
},
|
|
120
143
|
{
|
|
121
144
|
name: "some properties change",
|
|
@@ -124,6 +147,20 @@ describe("Stripe Providers", () => {
|
|
|
124
147
|
expectedChanges: true,
|
|
125
148
|
expectedReplaces: ["sparksAmount"]
|
|
126
149
|
},
|
|
150
|
+
{
|
|
151
|
+
name: "quantity changes",
|
|
152
|
+
olds: { priceId: "same", sparksAmount: "10", quantity: 1 },
|
|
153
|
+
news: { priceId: "same", sparksAmount: "10", quantity: 2 },
|
|
154
|
+
expectedChanges: true,
|
|
155
|
+
expectedReplaces: ["quantity"]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "adjustableQuantity changes",
|
|
159
|
+
olds: { priceId: "same", sparksAmount: "10", adjustableQuantity: { enabled: true, minimum: 1, maximum: 10 } },
|
|
160
|
+
news: { priceId: "same", sparksAmount: "10", adjustableQuantity: { enabled: true, minimum: 2, maximum: 10 } },
|
|
161
|
+
expectedChanges: true,
|
|
162
|
+
expectedReplaces: ["adjustableQuantity"]
|
|
163
|
+
},
|
|
127
164
|
{
|
|
128
165
|
name: "no properties change",
|
|
129
166
|
olds: { priceId: "same", sparksAmount: "10" },
|
|
@@ -142,9 +179,85 @@ describe("Stripe Providers", () => {
|
|
|
142
179
|
});
|
|
143
180
|
|
|
144
181
|
describe("delete", () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
const deleteCases = [
|
|
183
|
+
{
|
|
184
|
+
name: "apiKey is in state",
|
|
185
|
+
state: { apiKey: "sk_test_state" },
|
|
186
|
+
envSecret: undefined,
|
|
187
|
+
envApi: undefined,
|
|
188
|
+
mockError: false,
|
|
189
|
+
expectStripeInit: "sk_test_state",
|
|
190
|
+
expectUpdate: true,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
|
|
194
|
+
state: {},
|
|
195
|
+
envSecret: "sk_test_secret",
|
|
196
|
+
envApi: undefined,
|
|
197
|
+
mockError: false,
|
|
198
|
+
expectStripeInit: "sk_test_secret",
|
|
199
|
+
expectUpdate: true,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "apiKey is missing, falls back to STRIPE_API_KEY",
|
|
203
|
+
state: {},
|
|
204
|
+
envSecret: undefined,
|
|
205
|
+
envApi: "sk_test_api",
|
|
206
|
+
mockError: false,
|
|
207
|
+
expectStripeInit: "sk_test_api",
|
|
208
|
+
expectUpdate: true,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "apiKey is missing everywhere (NoOp)",
|
|
212
|
+
state: {},
|
|
213
|
+
envSecret: undefined,
|
|
214
|
+
envApi: undefined,
|
|
215
|
+
mockError: false,
|
|
216
|
+
expectStripeInit: null,
|
|
217
|
+
expectUpdate: false,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "Stripe SDK throws an error (caught safely)",
|
|
221
|
+
state: { apiKey: "sk_test_error" },
|
|
222
|
+
envSecret: undefined,
|
|
223
|
+
envApi: undefined,
|
|
224
|
+
mockError: true,
|
|
225
|
+
expectStripeInit: "sk_test_error",
|
|
226
|
+
expectUpdate: true,
|
|
227
|
+
}
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
it.each(deleteCases)("should handle deletion when $name", async ({ state, envSecret, envApi, mockError, expectStripeInit, expectUpdate }) => {
|
|
231
|
+
const originalSecret = process.env.STRIPE_SECRET_KEY;
|
|
232
|
+
const originalApi = process.env.STRIPE_API_KEY;
|
|
233
|
+
|
|
234
|
+
if (envSecret !== undefined) process.env.STRIPE_SECRET_KEY = envSecret;
|
|
235
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
236
|
+
|
|
237
|
+
if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
|
|
238
|
+
else delete process.env.STRIPE_API_KEY;
|
|
239
|
+
|
|
240
|
+
if (mockError) {
|
|
241
|
+
mockPaymentLinksUpdate.mockRejectedValueOnce(new Error("Stripe error"));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await provider.delete("plink_123", state as any);
|
|
245
|
+
|
|
246
|
+
if (expectStripeInit) {
|
|
247
|
+
expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (expectUpdate) {
|
|
251
|
+
expect(mockPaymentLinksUpdate).toHaveBeenCalledWith("plink_123", { active: false });
|
|
252
|
+
} else {
|
|
253
|
+
expect(mockPaymentLinksUpdate).not.toHaveBeenCalled();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (originalSecret !== undefined) process.env.STRIPE_SECRET_KEY = originalSecret;
|
|
257
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
258
|
+
|
|
259
|
+
if (originalApi !== undefined) process.env.STRIPE_API_KEY = originalApi;
|
|
260
|
+
else delete process.env.STRIPE_API_KEY;
|
|
148
261
|
});
|
|
149
262
|
});
|
|
150
263
|
});
|
|
@@ -241,9 +354,85 @@ describe("Stripe Providers", () => {
|
|
|
241
354
|
});
|
|
242
355
|
|
|
243
356
|
describe("delete", () => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
357
|
+
const deleteCases = [
|
|
358
|
+
{
|
|
359
|
+
name: "apiKey is in state",
|
|
360
|
+
state: { apiKey: "sk_test_state" },
|
|
361
|
+
envSecret: undefined,
|
|
362
|
+
envApi: undefined,
|
|
363
|
+
mockError: false,
|
|
364
|
+
expectStripeInit: "sk_test_state",
|
|
365
|
+
expectUpdate: true,
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
|
|
369
|
+
state: {},
|
|
370
|
+
envSecret: "sk_test_secret",
|
|
371
|
+
envApi: undefined,
|
|
372
|
+
mockError: false,
|
|
373
|
+
expectStripeInit: "sk_test_secret",
|
|
374
|
+
expectUpdate: true,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: "apiKey is missing, falls back to STRIPE_API_KEY",
|
|
378
|
+
state: {},
|
|
379
|
+
envSecret: undefined,
|
|
380
|
+
envApi: "sk_test_api",
|
|
381
|
+
mockError: false,
|
|
382
|
+
expectStripeInit: "sk_test_api",
|
|
383
|
+
expectUpdate: true,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "apiKey is missing everywhere (NoOp)",
|
|
387
|
+
state: {},
|
|
388
|
+
envSecret: undefined,
|
|
389
|
+
envApi: undefined,
|
|
390
|
+
mockError: false,
|
|
391
|
+
expectStripeInit: null,
|
|
392
|
+
expectUpdate: false,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "Stripe SDK throws an error (caught safely)",
|
|
396
|
+
state: { apiKey: "sk_test_error" },
|
|
397
|
+
envSecret: undefined,
|
|
398
|
+
envApi: undefined,
|
|
399
|
+
mockError: true,
|
|
400
|
+
expectStripeInit: "sk_test_error",
|
|
401
|
+
expectUpdate: true,
|
|
402
|
+
}
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
it.each(deleteCases)("should handle deletion when $name", async ({ state, envSecret, envApi, mockError, expectStripeInit, expectUpdate }) => {
|
|
406
|
+
const originalSecret = process.env.STRIPE_SECRET_KEY;
|
|
407
|
+
const originalApi = process.env.STRIPE_API_KEY;
|
|
408
|
+
|
|
409
|
+
if (envSecret !== undefined) process.env.STRIPE_SECRET_KEY = envSecret;
|
|
410
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
411
|
+
|
|
412
|
+
if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
|
|
413
|
+
else delete process.env.STRIPE_API_KEY;
|
|
414
|
+
|
|
415
|
+
if (mockError) {
|
|
416
|
+
mockProductsUpdate.mockRejectedValueOnce(new Error("Stripe error"));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
await provider.delete("prod_123", state as any);
|
|
420
|
+
|
|
421
|
+
if (expectStripeInit) {
|
|
422
|
+
expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (expectUpdate) {
|
|
426
|
+
expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", { active: false });
|
|
427
|
+
} else {
|
|
428
|
+
expect(mockProductsUpdate).not.toHaveBeenCalled();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (originalSecret !== undefined) process.env.STRIPE_SECRET_KEY = originalSecret;
|
|
432
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
433
|
+
|
|
434
|
+
if (originalApi !== undefined) process.env.STRIPE_API_KEY = originalApi;
|
|
435
|
+
else delete process.env.STRIPE_API_KEY;
|
|
247
436
|
});
|
|
248
437
|
});
|
|
249
438
|
});
|
|
@@ -253,6 +442,22 @@ describe("Stripe Providers", () => {
|
|
|
253
442
|
|
|
254
443
|
describe("create", () => {
|
|
255
444
|
const createCases = [
|
|
445
|
+
{
|
|
446
|
+
name: "all properties provided",
|
|
447
|
+
inputs: {
|
|
448
|
+
apiKey: "sk_test_123",
|
|
449
|
+
productId: "prod_123",
|
|
450
|
+
unitAmount: 1000,
|
|
451
|
+
currency: "usd",
|
|
452
|
+
taxBehavior: "inclusive",
|
|
453
|
+
},
|
|
454
|
+
expectedPayload: {
|
|
455
|
+
product: "prod_123",
|
|
456
|
+
unit_amount: 1000,
|
|
457
|
+
currency: "usd",
|
|
458
|
+
tax_behavior: "inclusive",
|
|
459
|
+
}
|
|
460
|
+
},
|
|
256
461
|
{
|
|
257
462
|
name: "required properties provided",
|
|
258
463
|
inputs: {
|
|
@@ -265,6 +470,63 @@ describe("Stripe Providers", () => {
|
|
|
265
470
|
product: "prod_123",
|
|
266
471
|
unit_amount: 1000,
|
|
267
472
|
currency: "usd",
|
|
473
|
+
tax_behavior: undefined,
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "recurring pricing provided",
|
|
478
|
+
inputs: {
|
|
479
|
+
apiKey: "sk_test_123",
|
|
480
|
+
productId: "prod_123",
|
|
481
|
+
unitAmount: 1000,
|
|
482
|
+
currency: "usd",
|
|
483
|
+
recurring: { interval: "month", intervalCount: 1, usageType: "licensed" }
|
|
484
|
+
},
|
|
485
|
+
expectedPayload: {
|
|
486
|
+
product: "prod_123",
|
|
487
|
+
unit_amount: 1000,
|
|
488
|
+
currency: "usd",
|
|
489
|
+
recurring: { interval: "month", interval_count: 1, usage_type: "licensed" }
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "tiered pricing provided",
|
|
494
|
+
inputs: {
|
|
495
|
+
apiKey: "sk_test_123",
|
|
496
|
+
productId: "prod_123",
|
|
497
|
+
currency: "usd",
|
|
498
|
+
billingScheme: "tiered",
|
|
499
|
+
tiersMode: "volume",
|
|
500
|
+
tiers: [
|
|
501
|
+
{ upTo: 10, unitAmount: 1000 },
|
|
502
|
+
{ upTo: "inf", unitAmount: 800 }
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
expectedPayload: {
|
|
506
|
+
product: "prod_123",
|
|
507
|
+
currency: "usd",
|
|
508
|
+
billing_scheme: "tiered",
|
|
509
|
+
tiers_mode: "volume",
|
|
510
|
+
tiers: [
|
|
511
|
+
{ up_to: 10, unit_amount: 1000, flat_amount: undefined },
|
|
512
|
+
{ up_to: "inf", unit_amount: 800, flat_amount: undefined }
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "package pricing provided",
|
|
518
|
+
inputs: {
|
|
519
|
+
apiKey: "sk_test_123",
|
|
520
|
+
productId: "prod_123",
|
|
521
|
+
unitAmount: 5000,
|
|
522
|
+
currency: "usd",
|
|
523
|
+
transformQuantity: { divideBy: 5, round: "up" }
|
|
524
|
+
},
|
|
525
|
+
expectedPayload: {
|
|
526
|
+
product: "prod_123",
|
|
527
|
+
unit_amount: 5000,
|
|
528
|
+
currency: "usd",
|
|
529
|
+
transform_quantity: { divide_by: 5, round: "up" }
|
|
268
530
|
}
|
|
269
531
|
}
|
|
270
532
|
];
|
|
@@ -284,10 +546,17 @@ describe("Stripe Providers", () => {
|
|
|
284
546
|
const diffCases = [
|
|
285
547
|
{
|
|
286
548
|
name: "all properties change",
|
|
287
|
-
olds: { productId: "prod_1", unitAmount: 100, currency: "usd", apiKey: "old_key" },
|
|
288
|
-
news: { productId: "prod_2", unitAmount: 200, currency: "eur", apiKey: "new_key" },
|
|
549
|
+
olds: { productId: "prod_1", unitAmount: 100, currency: "usd", taxBehavior: "inclusive", apiKey: "old_key" },
|
|
550
|
+
news: { productId: "prod_2", unitAmount: 200, currency: "eur", taxBehavior: "exclusive", apiKey: "new_key" },
|
|
289
551
|
expectedChanges: true,
|
|
290
|
-
expectedReplaces: ["productId", "unitAmount", "currency", "apiKey"]
|
|
552
|
+
expectedReplaces: ["productId", "unitAmount", "currency", "taxBehavior", "apiKey"]
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: "pricing models properties change",
|
|
556
|
+
olds: { productId: "prod_1", currency: "usd", recurring: { interval: "month" }, billingScheme: "per_unit", tiersMode: "volume", tiers: [{ upTo: 10 }], transformQuantity: { divideBy: 5 } },
|
|
557
|
+
news: { productId: "prod_1", currency: "usd", recurring: { interval: "year" }, billingScheme: "tiered", tiersMode: "graduated", tiers: [{ upTo: 20 }], transformQuantity: { divideBy: 10 } },
|
|
558
|
+
expectedChanges: true,
|
|
559
|
+
expectedReplaces: ["recurring", "billingScheme", "tiersMode", "tiers", "transformQuantity"]
|
|
291
560
|
},
|
|
292
561
|
{
|
|
293
562
|
name: "some properties change",
|
|
@@ -314,9 +583,85 @@ describe("Stripe Providers", () => {
|
|
|
314
583
|
});
|
|
315
584
|
|
|
316
585
|
describe("delete", () => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
586
|
+
const deleteCases = [
|
|
587
|
+
{
|
|
588
|
+
name: "apiKey is in state",
|
|
589
|
+
state: { apiKey: "sk_test_state" },
|
|
590
|
+
envSecret: undefined,
|
|
591
|
+
envApi: undefined,
|
|
592
|
+
mockError: false,
|
|
593
|
+
expectStripeInit: "sk_test_state",
|
|
594
|
+
expectUpdate: true,
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
|
|
598
|
+
state: {},
|
|
599
|
+
envSecret: "sk_test_secret",
|
|
600
|
+
envApi: undefined,
|
|
601
|
+
mockError: false,
|
|
602
|
+
expectStripeInit: "sk_test_secret",
|
|
603
|
+
expectUpdate: true,
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
name: "apiKey is missing, falls back to STRIPE_API_KEY",
|
|
607
|
+
state: {},
|
|
608
|
+
envSecret: undefined,
|
|
609
|
+
envApi: "sk_test_api",
|
|
610
|
+
mockError: false,
|
|
611
|
+
expectStripeInit: "sk_test_api",
|
|
612
|
+
expectUpdate: true,
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "apiKey is missing everywhere (NoOp)",
|
|
616
|
+
state: {},
|
|
617
|
+
envSecret: undefined,
|
|
618
|
+
envApi: undefined,
|
|
619
|
+
mockError: false,
|
|
620
|
+
expectStripeInit: null,
|
|
621
|
+
expectUpdate: false,
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: "Stripe SDK throws an error (caught safely)",
|
|
625
|
+
state: { apiKey: "sk_test_error" },
|
|
626
|
+
envSecret: undefined,
|
|
627
|
+
envApi: undefined,
|
|
628
|
+
mockError: true,
|
|
629
|
+
expectStripeInit: "sk_test_error",
|
|
630
|
+
expectUpdate: true,
|
|
631
|
+
}
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
it.each(deleteCases)("should handle deletion when $name", async ({ state, envSecret, envApi, mockError, expectStripeInit, expectUpdate }) => {
|
|
635
|
+
const originalSecret = process.env.STRIPE_SECRET_KEY;
|
|
636
|
+
const originalApi = process.env.STRIPE_API_KEY;
|
|
637
|
+
|
|
638
|
+
if (envSecret !== undefined) process.env.STRIPE_SECRET_KEY = envSecret;
|
|
639
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
640
|
+
|
|
641
|
+
if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
|
|
642
|
+
else delete process.env.STRIPE_API_KEY;
|
|
643
|
+
|
|
644
|
+
if (mockError) {
|
|
645
|
+
mockPricesUpdate.mockRejectedValueOnce(new Error("Stripe error"));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await provider.delete("price_123", state as any);
|
|
649
|
+
|
|
650
|
+
if (expectStripeInit) {
|
|
651
|
+
expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (expectUpdate) {
|
|
655
|
+
expect(mockPricesUpdate).toHaveBeenCalledWith("price_123", { active: false });
|
|
656
|
+
} else {
|
|
657
|
+
expect(mockPricesUpdate).not.toHaveBeenCalled();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (originalSecret !== undefined) process.env.STRIPE_SECRET_KEY = originalSecret;
|
|
661
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
662
|
+
|
|
663
|
+
if (originalApi !== undefined) process.env.STRIPE_API_KEY = originalApi;
|
|
664
|
+
else delete process.env.STRIPE_API_KEY;
|
|
320
665
|
});
|
|
321
666
|
});
|
|
322
667
|
});
|