@efaj/pulumi-dynamic-stripe 0.0.2 → 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 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
@@ -1,7 +1,7 @@
1
1
  # pulumi-dynamic-stripe
2
2
 
3
- ![NPM Version](https://img.shields.io/npm/v/pulumi-dynamic-stripe)
4
- ![License](https://img.shields.io/npm/l/pulumi-dynamic-stripe)
3
+ ![NPM Version](https://img.shields.io/npm/v/@efaj/pulumi-dynamic-stripe)
4
+ ![License](https://img.shields.io/npm/l/@efaj/pulumi-dynamic-stripe)
5
5
  [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fefaj%2Fpulumi-dynamic-stripe.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fefaj%2Fpulumi-dynamic-stripe?ref=badge_shield)
6
6
 
7
7
  A native Pulumi Dynamic Provider for Stripe.
@@ -28,7 +28,7 @@ Contributions to add more dynamic providers for other Stripe resources are highl
28
28
  ## Installation
29
29
 
30
30
  ```bash
31
- npm install pulumi-dynamic-stripe
31
+ npm install @efaj/pulumi-dynamic-stripe
32
32
  ```
33
33
 
34
34
  ## Usage
@@ -37,7 +37,7 @@ Here is a full example of provisioning a Product, attaching a Price to it, and g
37
37
 
38
38
  ```typescript
39
39
  import * as pulumi from "@pulumi/pulumi";
40
- import { Product, Price, PaymentLink } from "pulumi-dynamic-stripe";
40
+ import { Product, Price, PaymentLink } from "@efaj/pulumi-dynamic-stripe";
41
41
 
42
42
  const config = new pulumi.Config();
43
43
  const stripeApiKey = config.requireSecret("stripeApiKey");
@@ -71,3 +71,34 @@ export const paymentUrl = premiumPaymentLink.url;
71
71
  ## How it works
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
+
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
+
83
+ ## Development
84
+
85
+ If you wish to contribute or build the project locally, please use the provided `Makefile`.
86
+
87
+ ```bash
88
+ # Install dependencies
89
+ make install
90
+
91
+ # Build the TypeScript source
92
+ make build
93
+
94
+ # Run the Vitest test suite
95
+ make test
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
- const stripe = new stripe_1.default(inputs.apiKey, { apiVersion: '2022-11-15' });
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
- adjustable_quantity: {
50
- enabled: true,
51
- minimum: 1,
52
- maximum: 100,
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
- const stripe = new stripe_1.default(props.apiKey, { apiVersion: '2022-11-15' });
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);
@@ -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>;
@@ -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
- const stripe = new stripe_1.default(inputs.apiKey, { apiVersion: '2022-11-15' });
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
- const stripe = new stripe_1.default(props.apiKey, { apiVersion: '2022-11-15' });
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;
@@ -39,9 +39,10 @@ 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
- const stripe = new stripe_1.default(inputs.apiKey, { apiVersion: '2022-11-15' });
45
+ const stripe = new stripe_1.default(inputs.apiKey);
45
46
  const product = await stripe.products.create({
46
47
  name: inputs.name,
47
48
  description: inputs.description,
@@ -71,7 +72,7 @@ class StripeProductProvider {
71
72
  };
72
73
  }
73
74
  async update(id, olds, news) {
74
- const stripe = new stripe_1.default(news.apiKey, { apiVersion: '2022-11-15' });
75
+ const stripe = new stripe_1.default(news.apiKey);
75
76
  await stripe.products.update(id, {
76
77
  name: news.name,
77
78
  description: news.description,
@@ -85,8 +86,7 @@ class StripeProductProvider {
85
86
  };
86
87
  }
87
88
  async delete(id, props) {
88
- const stripe = new stripe_1.default(props.apiKey, { apiVersion: '2022-11-15' });
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;
@@ -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
@@ -1,3 +1,4 @@
1
1
  export * from "./StripePaymentLink";
2
2
  export * from "./StripeProduct";
3
3
  export * from "./StripePrice";
4
+ export * from "./enums";
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);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efaj/pulumi-dynamic-stripe",
3
- "version": "0.0.2",
3
+ "version": "0.1.0-5ac2a7e",
4
4
  "description": "A Pulumi Dynamic Provider for Stripe, supporting modern features like Payment Links.",
5
5
  "keywords": [
6
6
  "pulumi",
@@ -22,17 +22,15 @@
22
22
  "scripts": {
23
23
  "build": "tsc",
24
24
  "prepare": "tsc",
25
- "test": "jest"
25
+ "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
28
  "@pulumi/pulumi": "^3.259.0",
29
29
  "stripe": "^22.6.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/jest": "^30.0.0",
33
32
  "@types/node": "^26.4.0",
34
- "jest": "^30.4.2",
35
- "ts-jest": "^29.4.12",
33
+ "vitest": "^2.0.5",
36
34
  "typescript": "^6.0.3"
37
35
  }
38
- }
36
+ }
@@ -1,32 +1,33 @@
1
1
  import { StripePaymentLinkProvider, StripeProductProvider, StripePriceProvider } from "../src/index";
2
2
  import Stripe from "stripe";
3
- import { describe, it, expect, jest, beforeEach } from '@jest/globals';
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ import type { Mock } from 'vitest';
4
5
 
5
6
  // Mock the Stripe SDK
6
- jest.mock("stripe");
7
+ vi.mock("stripe");
7
8
 
8
- const MockedStripe = Stripe as jest.MockedClass<typeof Stripe>;
9
+ const MockedStripe = Stripe as any;
9
10
 
10
11
  describe("Stripe Providers", () => {
11
- let mockPaymentLinksCreate: jest.Mock<any>;
12
- let mockPaymentLinksUpdate: jest.Mock<any>;
13
- let mockProductsCreate: jest.Mock<any>;
14
- let mockProductsUpdate: jest.Mock<any>;
15
- let mockPricesCreate: jest.Mock<any>;
16
- let mockPricesUpdate: jest.Mock<any>;
12
+ let mockPaymentLinksCreate: Mock<any>;
13
+ let mockPaymentLinksUpdate: Mock<any>;
14
+ let mockProductsCreate: Mock<any>;
15
+ let mockProductsUpdate: Mock<any>;
16
+ let mockPricesCreate: Mock<any>;
17
+ let mockPricesUpdate: Mock<any>;
17
18
 
18
19
  beforeEach(() => {
19
20
  // Reset mocks before each test
20
- jest.clearAllMocks();
21
+ vi.clearAllMocks();
21
22
 
22
- mockPaymentLinksCreate = jest.fn<any>().mockResolvedValue({ id: "plink_123", url: "https://stripe.com/plink_123" } as any);
23
- mockPaymentLinksUpdate = jest.fn<any>().mockResolvedValue({ id: "plink_123" } as any);
23
+ mockPaymentLinksCreate = vi.fn<any>().mockResolvedValue({ id: "plink_123", url: "https://stripe.com/plink_123" } as any);
24
+ mockPaymentLinksUpdate = vi.fn<any>().mockResolvedValue({ id: "plink_123" } as any);
24
25
 
25
- mockProductsCreate = jest.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
26
- mockProductsUpdate = jest.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
26
+ mockProductsCreate = vi.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
27
+ mockProductsUpdate = vi.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
27
28
 
28
- mockPricesCreate = jest.fn<any>().mockResolvedValue({ id: "price_123" } as any);
29
- mockPricesUpdate = jest.fn<any>().mockResolvedValue({ id: "price_123" } as any);
29
+ mockPricesCreate = vi.fn<any>().mockResolvedValue({ id: "price_123" } as any);
30
+ mockPricesUpdate = vi.fn<any>().mockResolvedValue({ id: "price_123" } as any);
30
31
 
31
32
  // Setup the mocked Stripe instance's internal resource objects
32
33
  MockedStripe.mockImplementation(() => {
@@ -61,6 +62,7 @@ describe("Stripe Providers", () => {
61
62
  redirectUrl: "https://example.com/success",
62
63
  allowPromotionCodes: true,
63
64
  managedPayments: true,
65
+ automaticTax: true,
64
66
  },
65
67
  expectedPayload: {
66
68
  line_items: [{
@@ -71,6 +73,7 @@ describe("Stripe Providers", () => {
71
73
  after_completion: { type: 'redirect', redirect: { url: "https://example.com/success" } },
72
74
  allow_promotion_codes: true,
73
75
  managed_payments: { enabled: true },
76
+ automatic_tax: { enabled: true },
74
77
  metadata: { sparks: "100" }
75
78
  }
76
79
  },
@@ -92,6 +95,27 @@ describe("Stripe Providers", () => {
92
95
  allow_promotion_codes: undefined,
93
96
  metadata: { sparks: "50" }
94
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
+ }
95
119
  }
96
120
  ];
97
121
 
@@ -102,7 +126,7 @@ describe("Stripe Providers", () => {
102
126
  expect(result.outs?.paymentLinkId).toBe("plink_123");
103
127
  expect(result.outs?.url).toBe("https://stripe.com/plink_123");
104
128
 
105
- expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey, { apiVersion: '2022-11-15' as any });
129
+ expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
106
130
  expect(mockPaymentLinksCreate).toHaveBeenCalledWith(expectedPayload);
107
131
  });
108
132
  });
@@ -111,10 +135,10 @@ describe("Stripe Providers", () => {
111
135
  const diffCases = [
112
136
  {
113
137
  name: "all properties change",
114
- olds: { priceId: "old", sparksAmount: "10", redirectUrl: "url1", allowPromotionCodes: false, managedPayments: false, apiKey: "old_key" },
115
- 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" },
116
140
  expectedChanges: true,
117
- expectedReplaces: ["priceId", "sparksAmount", "redirectUrl", "allowPromotionCodes", "managedPayments", "apiKey"]
141
+ expectedReplaces: ["priceId", "sparksAmount", "redirectUrl", "allowPromotionCodes", "managedPayments", "automaticTax", "apiKey", "quantity", "adjustableQuantity"]
118
142
  },
119
143
  {
120
144
  name: "some properties change",
@@ -123,6 +147,20 @@ describe("Stripe Providers", () => {
123
147
  expectedChanges: true,
124
148
  expectedReplaces: ["sparksAmount"]
125
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
+ },
126
164
  {
127
165
  name: "no properties change",
128
166
  olds: { priceId: "same", sparksAmount: "10" },
@@ -141,9 +179,85 @@ describe("Stripe Providers", () => {
141
179
  });
142
180
 
143
181
  describe("delete", () => {
144
- it("should deactivate the payment link", async () => {
145
- await provider.delete("plink_123", { apiKey: "sk_test_123" } as any);
146
- expect(mockPaymentLinksUpdate).toHaveBeenCalledWith("plink_123", { active: false });
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;
147
261
  });
148
262
  });
149
263
  });
@@ -187,7 +301,7 @@ describe("Stripe Providers", () => {
187
301
  expect(result.id).toBe("prod_123");
188
302
  expect(result.outs?.productId).toBe("prod_123");
189
303
 
190
- expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey, { apiVersion: '2022-11-15' as any });
304
+ expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
191
305
  expect(mockProductsCreate).toHaveBeenCalledWith(expectedPayload);
192
306
  });
193
307
  });
@@ -240,9 +354,85 @@ describe("Stripe Providers", () => {
240
354
  });
241
355
 
242
356
  describe("delete", () => {
243
- it("should deactivate the product on delete", async () => {
244
- await provider.delete("prod_123", { apiKey: "sk_test_123" } as any);
245
- expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", { active: false });
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;
246
436
  });
247
437
  });
248
438
  });
@@ -252,6 +442,22 @@ describe("Stripe Providers", () => {
252
442
 
253
443
  describe("create", () => {
254
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
+ },
255
461
  {
256
462
  name: "required properties provided",
257
463
  inputs: {
@@ -264,6 +470,63 @@ describe("Stripe Providers", () => {
264
470
  product: "prod_123",
265
471
  unit_amount: 1000,
266
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" }
267
530
  }
268
531
  }
269
532
  ];
@@ -274,7 +537,7 @@ describe("Stripe Providers", () => {
274
537
  expect(result.id).toBe("price_123");
275
538
  expect(result.outs?.priceId).toBe("price_123");
276
539
 
277
- expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey, { apiVersion: '2022-11-15' as any });
540
+ expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
278
541
  expect(mockPricesCreate).toHaveBeenCalledWith(expectedPayload);
279
542
  });
280
543
  });
@@ -283,10 +546,17 @@ describe("Stripe Providers", () => {
283
546
  const diffCases = [
284
547
  {
285
548
  name: "all properties change",
286
- olds: { productId: "prod_1", unitAmount: 100, currency: "usd", apiKey: "old_key" },
287
- 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" },
288
551
  expectedChanges: true,
289
- 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"]
290
560
  },
291
561
  {
292
562
  name: "some properties change",
@@ -313,9 +583,85 @@ describe("Stripe Providers", () => {
313
583
  });
314
584
 
315
585
  describe("delete", () => {
316
- it("should deactivate the price on delete", async () => {
317
- await provider.delete("price_123", { apiKey: "sk_test_123" } as any);
318
- expect(mockPricesUpdate).toHaveBeenCalledWith("price_123", { active: false });
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;
319
665
  });
320
666
  });
321
667
  });
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts'],
6
+ },
7
+ });
package/jest.config.js DELETED
@@ -1,5 +0,0 @@
1
- module.exports = {
2
- preset: 'ts-jest',
3
- testEnvironment: 'node',
4
- testMatch: ['**/tests/**/*.test.ts'],
5
- };