@efaj/pulumi-dynamic-stripe 0.0.1 → 0.0.2-0639e18
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/LICENSE +0 -0
- package/Makefile +15 -0
- package/README.md +27 -4
- package/dist/StripePaymentLink.d.ts +27 -0
- package/dist/StripePaymentLink.js +126 -0
- package/dist/StripePrice.d.ts +22 -0
- package/dist/StripePrice.js +95 -0
- package/dist/StripeProduct.d.ts +23 -0
- package/dist/StripeProduct.js +109 -0
- package/dist/index.d.ts +3 -33
- package/dist/index.js +5 -148
- package/package.json +4 -2
- package/tests/index.test.ts +551 -0
- package/vitest.config.ts +7 -0
package/LICENSE
CHANGED
|
File without changes
|
package/Makefile
ADDED
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pulumi-dynamic-stripe
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-

|
|
3
|
+

|
|
4
|
+

|
|
5
5
|
[](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,26 @@ 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
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
export interface StripePaymentLinkArgs {
|
|
3
|
+
apiKey: pulumi.Input<string>;
|
|
4
|
+
priceId: pulumi.Input<string>;
|
|
5
|
+
sparksAmount: pulumi.Input<string>;
|
|
6
|
+
redirectUrl: pulumi.Input<string>;
|
|
7
|
+
allowPromotionCodes?: pulumi.Input<boolean>;
|
|
8
|
+
managedPayments?: pulumi.Input<boolean>;
|
|
9
|
+
}
|
|
10
|
+
export interface StripePaymentLinkProviderArgs {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
priceId: string;
|
|
13
|
+
sparksAmount: string;
|
|
14
|
+
redirectUrl: string;
|
|
15
|
+
allowPromotionCodes?: boolean;
|
|
16
|
+
managedPayments?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class StripePaymentLinkProvider implements pulumi.dynamic.ResourceProvider {
|
|
19
|
+
create(inputs: StripePaymentLinkProviderArgs): Promise<pulumi.dynamic.CreateResult>;
|
|
20
|
+
diff(id: string, olds: StripePaymentLinkProviderArgs, news: StripePaymentLinkProviderArgs): Promise<pulumi.dynamic.DiffResult>;
|
|
21
|
+
delete(id: string, props: StripePaymentLinkProviderArgs): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare class PaymentLink extends pulumi.dynamic.Resource {
|
|
24
|
+
readonly url: pulumi.Output<string>;
|
|
25
|
+
readonly paymentLinkId: pulumi.Output<string>;
|
|
26
|
+
constructor(name: string, args: StripePaymentLinkArgs, opts?: pulumi.CustomResourceOptions);
|
|
27
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.PaymentLink = exports.StripePaymentLinkProvider = void 0;
|
|
40
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
41
|
+
const stripe_1 = __importDefault(require("stripe"));
|
|
42
|
+
class StripePaymentLinkProvider {
|
|
43
|
+
async create(inputs) {
|
|
44
|
+
const stripe = new stripe_1.default(inputs.apiKey);
|
|
45
|
+
const paymentLink = await stripe.paymentLinks.create({
|
|
46
|
+
line_items: [{
|
|
47
|
+
price: inputs.priceId,
|
|
48
|
+
quantity: 1,
|
|
49
|
+
adjustable_quantity: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
minimum: 1,
|
|
52
|
+
maximum: 100,
|
|
53
|
+
},
|
|
54
|
+
}],
|
|
55
|
+
after_completion: {
|
|
56
|
+
type: 'redirect',
|
|
57
|
+
redirect: {
|
|
58
|
+
url: inputs.redirectUrl,
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
allow_promotion_codes: inputs.allowPromotionCodes,
|
|
62
|
+
...(inputs.managedPayments ? { managed_payments: { enabled: true } } : {}),
|
|
63
|
+
metadata: {
|
|
64
|
+
sparks: inputs.sparksAmount
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
id: paymentLink.id,
|
|
69
|
+
outs: {
|
|
70
|
+
...inputs,
|
|
71
|
+
paymentLinkId: paymentLink.id,
|
|
72
|
+
url: paymentLink.url,
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async diff(id, olds, news) {
|
|
77
|
+
const replaces = [];
|
|
78
|
+
if (olds.priceId !== news.priceId)
|
|
79
|
+
replaces.push("priceId");
|
|
80
|
+
if (olds.sparksAmount !== news.sparksAmount)
|
|
81
|
+
replaces.push("sparksAmount");
|
|
82
|
+
if (olds.redirectUrl !== news.redirectUrl)
|
|
83
|
+
replaces.push("redirectUrl");
|
|
84
|
+
if (olds.allowPromotionCodes !== news.allowPromotionCodes)
|
|
85
|
+
replaces.push("allowPromotionCodes");
|
|
86
|
+
if (olds.managedPayments !== news.managedPayments)
|
|
87
|
+
replaces.push("managedPayments");
|
|
88
|
+
if (olds.apiKey !== news.apiKey)
|
|
89
|
+
replaces.push("apiKey");
|
|
90
|
+
return {
|
|
91
|
+
changes: replaces.length > 0,
|
|
92
|
+
replaces,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async delete(id, props) {
|
|
96
|
+
const apiKey = props.apiKey || process.env.STRIPE_SECRET_KEY || process.env.STRIPE_API_KEY;
|
|
97
|
+
if (!apiKey) {
|
|
98
|
+
console.warn(`[StripePaymentLinkProvider] Skipping deletion of ${id} due to missing apiKey in state and environment variables.`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const stripe = new stripe_1.default(apiKey);
|
|
103
|
+
await stripe.paymentLinks.update(id, { active: false });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.warn(`[StripePaymentLinkProvider] Failed to deactivate payment link ${id}:`, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.StripePaymentLinkProvider = StripePaymentLinkProvider;
|
|
111
|
+
const stripePaymentLinkProvider = new StripePaymentLinkProvider();
|
|
112
|
+
class PaymentLink extends pulumi.dynamic.Resource {
|
|
113
|
+
constructor(name, args, opts) {
|
|
114
|
+
super(stripePaymentLinkProvider, name, {
|
|
115
|
+
apiKey: args.apiKey,
|
|
116
|
+
priceId: args.priceId,
|
|
117
|
+
sparksAmount: args.sparksAmount,
|
|
118
|
+
redirectUrl: args.redirectUrl,
|
|
119
|
+
allowPromotionCodes: args.allowPromotionCodes,
|
|
120
|
+
managedPayments: args.managedPayments,
|
|
121
|
+
paymentLinkId: undefined,
|
|
122
|
+
url: undefined,
|
|
123
|
+
}, opts);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.PaymentLink = PaymentLink;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
export interface StripePriceArgs {
|
|
3
|
+
apiKey: pulumi.Input<string>;
|
|
4
|
+
productId: pulumi.Input<string>;
|
|
5
|
+
unitAmount: pulumi.Input<number>;
|
|
6
|
+
currency: pulumi.Input<string>;
|
|
7
|
+
}
|
|
8
|
+
export interface StripePriceProviderArgs {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
productId: string;
|
|
11
|
+
unitAmount: number;
|
|
12
|
+
currency: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class StripePriceProvider implements pulumi.dynamic.ResourceProvider {
|
|
15
|
+
create(inputs: StripePriceProviderArgs): Promise<pulumi.dynamic.CreateResult>;
|
|
16
|
+
diff(id: string, olds: StripePriceProviderArgs, news: StripePriceProviderArgs): Promise<pulumi.dynamic.DiffResult>;
|
|
17
|
+
delete(id: string, props: StripePriceProviderArgs): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare class Price extends pulumi.dynamic.Resource {
|
|
20
|
+
readonly priceId: pulumi.Output<string>;
|
|
21
|
+
constructor(name: string, args: StripePriceArgs, opts?: pulumi.CustomResourceOptions);
|
|
22
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Price = exports.StripePriceProvider = void 0;
|
|
40
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
41
|
+
const stripe_1 = __importDefault(require("stripe"));
|
|
42
|
+
class StripePriceProvider {
|
|
43
|
+
async create(inputs) {
|
|
44
|
+
const stripe = new stripe_1.default(inputs.apiKey);
|
|
45
|
+
const price = await stripe.prices.create({
|
|
46
|
+
product: inputs.productId,
|
|
47
|
+
unit_amount: inputs.unitAmount,
|
|
48
|
+
currency: inputs.currency,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
id: price.id,
|
|
52
|
+
outs: {
|
|
53
|
+
...inputs,
|
|
54
|
+
priceId: price.id,
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async diff(id, olds, news) {
|
|
59
|
+
const replaces = [];
|
|
60
|
+
if (olds.productId !== news.productId)
|
|
61
|
+
replaces.push("productId");
|
|
62
|
+
if (olds.unitAmount !== news.unitAmount)
|
|
63
|
+
replaces.push("unitAmount");
|
|
64
|
+
if (olds.currency !== news.currency)
|
|
65
|
+
replaces.push("currency");
|
|
66
|
+
if (olds.apiKey !== news.apiKey)
|
|
67
|
+
replaces.push("apiKey");
|
|
68
|
+
return {
|
|
69
|
+
changes: replaces.length > 0,
|
|
70
|
+
replaces,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async delete(id, props) {
|
|
74
|
+
const apiKey = props.apiKey || process.env.STRIPE_SECRET_KEY || process.env.STRIPE_API_KEY;
|
|
75
|
+
if (!apiKey) {
|
|
76
|
+
console.warn(`[StripePriceProvider] Skipping deletion of ${id} due to missing apiKey in state and environment variables.`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const stripe = new stripe_1.default(apiKey);
|
|
81
|
+
await stripe.prices.update(id, { active: false });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.warn(`[StripePriceProvider] Failed to deactivate price ${id}:`, error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.StripePriceProvider = StripePriceProvider;
|
|
89
|
+
const stripePriceProvider = new StripePriceProvider();
|
|
90
|
+
class Price extends pulumi.dynamic.Resource {
|
|
91
|
+
constructor(name, args, opts) {
|
|
92
|
+
super(stripePriceProvider, name, { ...args, priceId: undefined }, opts);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.Price = Price;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as pulumi from "@pulumi/pulumi";
|
|
2
|
+
export interface StripeProductArgs {
|
|
3
|
+
apiKey: pulumi.Input<string>;
|
|
4
|
+
name: pulumi.Input<string>;
|
|
5
|
+
description?: pulumi.Input<string>;
|
|
6
|
+
taxCode?: pulumi.Input<string>;
|
|
7
|
+
}
|
|
8
|
+
export interface StripeProductProviderArgs {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
taxCode?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class StripeProductProvider implements pulumi.dynamic.ResourceProvider {
|
|
15
|
+
create(inputs: StripeProductProviderArgs): Promise<pulumi.dynamic.CreateResult>;
|
|
16
|
+
diff(id: string, olds: StripeProductProviderArgs, news: StripeProductProviderArgs): Promise<pulumi.dynamic.DiffResult>;
|
|
17
|
+
update(id: string, olds: StripeProductProviderArgs, news: StripeProductProviderArgs): Promise<pulumi.dynamic.UpdateResult>;
|
|
18
|
+
delete(id: string, props: StripeProductProviderArgs): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare class Product extends pulumi.dynamic.Resource {
|
|
21
|
+
readonly productId: pulumi.Output<string>;
|
|
22
|
+
constructor(name: string, args: StripeProductArgs, opts?: pulumi.CustomResourceOptions);
|
|
23
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Product = exports.StripeProductProvider = void 0;
|
|
40
|
+
const pulumi = __importStar(require("@pulumi/pulumi"));
|
|
41
|
+
const stripe_1 = __importDefault(require("stripe"));
|
|
42
|
+
class StripeProductProvider {
|
|
43
|
+
async create(inputs) {
|
|
44
|
+
const stripe = new stripe_1.default(inputs.apiKey);
|
|
45
|
+
const product = await stripe.products.create({
|
|
46
|
+
name: inputs.name,
|
|
47
|
+
description: inputs.description,
|
|
48
|
+
tax_code: inputs.taxCode,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
id: product.id,
|
|
52
|
+
outs: {
|
|
53
|
+
...inputs,
|
|
54
|
+
productId: product.id,
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async diff(id, olds, news) {
|
|
59
|
+
const changes = [];
|
|
60
|
+
if (olds.name !== news.name)
|
|
61
|
+
changes.push("name");
|
|
62
|
+
if (olds.description !== news.description)
|
|
63
|
+
changes.push("description");
|
|
64
|
+
if (olds.taxCode !== news.taxCode)
|
|
65
|
+
changes.push("taxCode");
|
|
66
|
+
if (olds.apiKey !== news.apiKey)
|
|
67
|
+
changes.push("apiKey");
|
|
68
|
+
return {
|
|
69
|
+
changes: changes.length > 0,
|
|
70
|
+
replaces: [], // None require replacement, all can be updated
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async update(id, olds, news) {
|
|
74
|
+
const stripe = new stripe_1.default(news.apiKey);
|
|
75
|
+
await stripe.products.update(id, {
|
|
76
|
+
name: news.name,
|
|
77
|
+
description: news.description,
|
|
78
|
+
tax_code: news.taxCode,
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
outs: {
|
|
82
|
+
...news,
|
|
83
|
+
productId: id,
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async delete(id, props) {
|
|
88
|
+
const apiKey = props.apiKey || process.env.STRIPE_SECRET_KEY || process.env.STRIPE_API_KEY;
|
|
89
|
+
if (!apiKey) {
|
|
90
|
+
console.warn(`[StripeProductProvider] Skipping deletion of ${id} due to missing apiKey in state and environment variables.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const stripe = new stripe_1.default(apiKey);
|
|
95
|
+
await stripe.products.update(id, { active: false });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.warn(`[StripeProductProvider] Failed to deactivate product ${id}:`, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.StripeProductProvider = StripeProductProvider;
|
|
103
|
+
const stripeProductProvider = new StripeProductProvider();
|
|
104
|
+
class Product extends pulumi.dynamic.Resource {
|
|
105
|
+
constructor(name, args, opts) {
|
|
106
|
+
super(stripeProductProvider, name, { ...args, productId: undefined }, opts);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.Product = Product;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,33 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
priceId: pulumi.Input<string>;
|
|
5
|
-
sparksAmount: pulumi.Input<string>;
|
|
6
|
-
redirectUrl: pulumi.Input<string>;
|
|
7
|
-
allowPromotionCodes?: pulumi.Input<boolean>;
|
|
8
|
-
}
|
|
9
|
-
export declare class PaymentLink extends pulumi.dynamic.Resource {
|
|
10
|
-
readonly url: pulumi.Output<string>;
|
|
11
|
-
readonly paymentLinkId: pulumi.Output<string>;
|
|
12
|
-
constructor(name: string, args: StripePaymentLinkArgs, opts?: any);
|
|
13
|
-
}
|
|
14
|
-
interface StripeProductArgs {
|
|
15
|
-
apiKey: pulumi.Input<string>;
|
|
16
|
-
name: pulumi.Input<string>;
|
|
17
|
-
description?: pulumi.Input<string>;
|
|
18
|
-
}
|
|
19
|
-
export declare class Product extends pulumi.dynamic.Resource {
|
|
20
|
-
readonly productId: pulumi.Output<string>;
|
|
21
|
-
constructor(name: string, args: StripeProductArgs, opts?: any);
|
|
22
|
-
}
|
|
23
|
-
interface StripePriceArgs {
|
|
24
|
-
apiKey: pulumi.Input<string>;
|
|
25
|
-
productId: pulumi.Input<string>;
|
|
26
|
-
unitAmount: pulumi.Input<number>;
|
|
27
|
-
currency: pulumi.Input<string>;
|
|
28
|
-
}
|
|
29
|
-
export declare class Price extends pulumi.dynamic.Resource {
|
|
30
|
-
readonly priceId: pulumi.Output<string>;
|
|
31
|
-
constructor(name: string, args: StripePriceArgs, opts?: any);
|
|
32
|
-
}
|
|
33
|
-
export {};
|
|
1
|
+
export * from "./StripePaymentLink";
|
|
2
|
+
export * from "./StripeProduct";
|
|
3
|
+
export * from "./StripePrice";
|
package/dist/index.js
CHANGED
|
@@ -10,153 +10,10 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
|
|
|
10
10
|
if (k2 === undefined) k2 = k;
|
|
11
11
|
o[k2] = m[k];
|
|
12
12
|
}));
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
37
15
|
};
|
|
38
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const stripe_1 = __importDefault(require("stripe"));
|
|
43
|
-
class StripePaymentLinkProvider {
|
|
44
|
-
async create(inputs) {
|
|
45
|
-
const stripe = new stripe_1.default(inputs.apiKey, { apiVersion: '2022-11-15' }); // Using older API version to avoid typings mismatch if any
|
|
46
|
-
const paymentLink = await stripe.paymentLinks.create({
|
|
47
|
-
line_items: [{
|
|
48
|
-
price: inputs.priceId,
|
|
49
|
-
quantity: 1,
|
|
50
|
-
adjustable_quantity: {
|
|
51
|
-
enabled: true,
|
|
52
|
-
minimum: 1,
|
|
53
|
-
maximum: 100,
|
|
54
|
-
},
|
|
55
|
-
}],
|
|
56
|
-
after_completion: {
|
|
57
|
-
type: 'redirect',
|
|
58
|
-
redirect: {
|
|
59
|
-
url: inputs.redirectUrl,
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
allow_promotion_codes: inputs.allowPromotionCodes,
|
|
63
|
-
metadata: {
|
|
64
|
-
sparks: inputs.sparksAmount
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
return {
|
|
68
|
-
id: paymentLink.id,
|
|
69
|
-
outs: {
|
|
70
|
-
...inputs,
|
|
71
|
-
paymentLinkId: paymentLink.id,
|
|
72
|
-
url: paymentLink.url,
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
async delete(id, props) {
|
|
77
|
-
const stripe = new stripe_1.default(props.apiKey, { apiVersion: '2022-11-15' });
|
|
78
|
-
// Payment links cannot be deleted, but they can be deactivated
|
|
79
|
-
await stripe.paymentLinks.update(id, { active: false });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const stripePaymentLinkProvider = new StripePaymentLinkProvider();
|
|
83
|
-
class PaymentLink extends pulumi.dynamic.Resource {
|
|
84
|
-
constructor(name, args, opts) {
|
|
85
|
-
super(stripePaymentLinkProvider, name, {
|
|
86
|
-
apiKey: args.apiKey,
|
|
87
|
-
priceId: args.priceId,
|
|
88
|
-
sparksAmount: args.sparksAmount,
|
|
89
|
-
redirectUrl: args.redirectUrl,
|
|
90
|
-
allowPromotionCodes: args.allowPromotionCodes,
|
|
91
|
-
paymentLinkId: undefined,
|
|
92
|
-
url: undefined,
|
|
93
|
-
}, opts);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
exports.PaymentLink = PaymentLink;
|
|
97
|
-
class StripeProductProvider {
|
|
98
|
-
async create(inputs) {
|
|
99
|
-
const stripe = new stripe_1.default(inputs.apiKey, { apiVersion: '2022-11-15' });
|
|
100
|
-
const product = await stripe.products.create({
|
|
101
|
-
name: inputs.name,
|
|
102
|
-
description: inputs.description,
|
|
103
|
-
});
|
|
104
|
-
return {
|
|
105
|
-
id: product.id,
|
|
106
|
-
outs: {
|
|
107
|
-
...inputs,
|
|
108
|
-
productId: product.id,
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
async update(id, olds, news) {
|
|
113
|
-
const stripe = new stripe_1.default(news.apiKey, { apiVersion: '2022-11-15' });
|
|
114
|
-
await stripe.products.update(id, {
|
|
115
|
-
name: news.name,
|
|
116
|
-
description: news.description,
|
|
117
|
-
});
|
|
118
|
-
return {
|
|
119
|
-
outs: {
|
|
120
|
-
...news,
|
|
121
|
-
productId: id,
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
async delete(id, props) {
|
|
126
|
-
const stripe = new stripe_1.default(props.apiKey, { apiVersion: '2022-11-15' });
|
|
127
|
-
await stripe.products.update(id, { active: false });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
class Product extends pulumi.dynamic.Resource {
|
|
131
|
-
constructor(name, args, opts) {
|
|
132
|
-
super(new StripeProductProvider(), name, { ...args, productId: undefined }, opts);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
exports.Product = Product;
|
|
136
|
-
class StripePriceProvider {
|
|
137
|
-
async create(inputs) {
|
|
138
|
-
const stripe = new stripe_1.default(inputs.apiKey, { apiVersion: '2022-11-15' });
|
|
139
|
-
const price = await stripe.prices.create({
|
|
140
|
-
product: inputs.productId,
|
|
141
|
-
unit_amount: inputs.unitAmount,
|
|
142
|
-
currency: inputs.currency,
|
|
143
|
-
});
|
|
144
|
-
return {
|
|
145
|
-
id: price.id,
|
|
146
|
-
outs: {
|
|
147
|
-
...inputs,
|
|
148
|
-
priceId: price.id,
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
async delete(id, props) {
|
|
153
|
-
const stripe = new stripe_1.default(props.apiKey, { apiVersion: '2022-11-15' });
|
|
154
|
-
await stripe.prices.update(id, { active: false });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
class Price extends pulumi.dynamic.Resource {
|
|
158
|
-
constructor(name, args, opts) {
|
|
159
|
-
super(new StripePriceProvider(), name, { ...args, priceId: undefined }, opts);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
exports.Price = Price;
|
|
17
|
+
__exportStar(require("./StripePaymentLink"), exports);
|
|
18
|
+
__exportStar(require("./StripeProduct"), exports);
|
|
19
|
+
__exportStar(require("./StripePrice"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@efaj/pulumi-dynamic-stripe",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2-0639e18",
|
|
4
4
|
"description": "A Pulumi Dynamic Provider for Stripe, supporting modern features like Payment Links.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pulumi",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"types": "dist/index.d.ts",
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc",
|
|
24
|
-
"prepare": "tsc"
|
|
24
|
+
"prepare": "tsc",
|
|
25
|
+
"test": "vitest run"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@pulumi/pulumi": "^3.259.0",
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/node": "^26.4.0",
|
|
33
|
+
"vitest": "^2.0.5",
|
|
32
34
|
"typescript": "^6.0.3"
|
|
33
35
|
}
|
|
34
36
|
}
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { StripePaymentLinkProvider, StripeProductProvider, StripePriceProvider } from "../src/index";
|
|
2
|
+
import Stripe from "stripe";
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import type { Mock } from 'vitest';
|
|
5
|
+
|
|
6
|
+
// Mock the Stripe SDK
|
|
7
|
+
vi.mock("stripe");
|
|
8
|
+
|
|
9
|
+
const MockedStripe = Stripe as any;
|
|
10
|
+
|
|
11
|
+
describe("Stripe Providers", () => {
|
|
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>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Reset mocks before each test
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
|
|
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);
|
|
25
|
+
|
|
26
|
+
mockProductsCreate = vi.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
|
|
27
|
+
mockProductsUpdate = vi.fn<any>().mockResolvedValue({ id: "prod_123" } as any);
|
|
28
|
+
|
|
29
|
+
mockPricesCreate = vi.fn<any>().mockResolvedValue({ id: "price_123" } as any);
|
|
30
|
+
mockPricesUpdate = vi.fn<any>().mockResolvedValue({ id: "price_123" } as any);
|
|
31
|
+
|
|
32
|
+
// Setup the mocked Stripe instance's internal resource objects
|
|
33
|
+
MockedStripe.mockImplementation(() => {
|
|
34
|
+
return {
|
|
35
|
+
paymentLinks: {
|
|
36
|
+
create: mockPaymentLinksCreate,
|
|
37
|
+
update: mockPaymentLinksUpdate,
|
|
38
|
+
},
|
|
39
|
+
products: {
|
|
40
|
+
create: mockProductsCreate,
|
|
41
|
+
update: mockProductsUpdate,
|
|
42
|
+
},
|
|
43
|
+
prices: {
|
|
44
|
+
create: mockPricesCreate,
|
|
45
|
+
update: mockPricesUpdate,
|
|
46
|
+
},
|
|
47
|
+
} as any;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("StripePaymentLinkProvider", () => {
|
|
52
|
+
const provider = new StripePaymentLinkProvider();
|
|
53
|
+
|
|
54
|
+
describe("create", () => {
|
|
55
|
+
const createCases = [
|
|
56
|
+
{
|
|
57
|
+
name: "all properties provided",
|
|
58
|
+
inputs: {
|
|
59
|
+
apiKey: "sk_test_123",
|
|
60
|
+
priceId: "price_abc",
|
|
61
|
+
sparksAmount: "100",
|
|
62
|
+
redirectUrl: "https://example.com/success",
|
|
63
|
+
allowPromotionCodes: true,
|
|
64
|
+
managedPayments: true,
|
|
65
|
+
},
|
|
66
|
+
expectedPayload: {
|
|
67
|
+
line_items: [{
|
|
68
|
+
price: "price_abc",
|
|
69
|
+
quantity: 1,
|
|
70
|
+
adjustable_quantity: { enabled: true, minimum: 1, maximum: 100 },
|
|
71
|
+
}],
|
|
72
|
+
after_completion: { type: 'redirect', redirect: { url: "https://example.com/success" } },
|
|
73
|
+
allow_promotion_codes: true,
|
|
74
|
+
managed_payments: { enabled: true },
|
|
75
|
+
metadata: { sparks: "100" }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "only required properties provided",
|
|
80
|
+
inputs: {
|
|
81
|
+
apiKey: "sk_test_123",
|
|
82
|
+
priceId: "price_abc",
|
|
83
|
+
sparksAmount: "50",
|
|
84
|
+
redirectUrl: "https://example.com/success",
|
|
85
|
+
},
|
|
86
|
+
expectedPayload: {
|
|
87
|
+
line_items: [{
|
|
88
|
+
price: "price_abc",
|
|
89
|
+
quantity: 1,
|
|
90
|
+
adjustable_quantity: { enabled: true, minimum: 1, maximum: 100 },
|
|
91
|
+
}],
|
|
92
|
+
after_completion: { type: 'redirect', redirect: { url: "https://example.com/success" } },
|
|
93
|
+
allow_promotion_codes: undefined,
|
|
94
|
+
metadata: { sparks: "50" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
it.each(createCases)("should correctly map Pulumi inputs to Stripe API payload when $name", async ({ inputs, expectedPayload }) => {
|
|
100
|
+
const result = await provider.create(inputs as any);
|
|
101
|
+
|
|
102
|
+
expect(result.id).toBe("plink_123");
|
|
103
|
+
expect(result.outs?.paymentLinkId).toBe("plink_123");
|
|
104
|
+
expect(result.outs?.url).toBe("https://stripe.com/plink_123");
|
|
105
|
+
|
|
106
|
+
expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
|
|
107
|
+
expect(mockPaymentLinksCreate).toHaveBeenCalledWith(expectedPayload);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("diff", () => {
|
|
112
|
+
const diffCases = [
|
|
113
|
+
{
|
|
114
|
+
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" },
|
|
117
|
+
expectedChanges: true,
|
|
118
|
+
expectedReplaces: ["priceId", "sparksAmount", "redirectUrl", "allowPromotionCodes", "managedPayments", "apiKey"]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "some properties change",
|
|
122
|
+
olds: { priceId: "old", sparksAmount: "10", redirectUrl: "url1" },
|
|
123
|
+
news: { priceId: "old", sparksAmount: "20", redirectUrl: "url1" },
|
|
124
|
+
expectedChanges: true,
|
|
125
|
+
expectedReplaces: ["sparksAmount"]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "no properties change",
|
|
129
|
+
olds: { priceId: "same", sparksAmount: "10" },
|
|
130
|
+
news: { priceId: "same", sparksAmount: "10" },
|
|
131
|
+
expectedChanges: false,
|
|
132
|
+
expectedReplaces: []
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
it.each(diffCases)("should correctly detect diff when $name", async ({ olds, news, expectedChanges, expectedReplaces }) => {
|
|
137
|
+
const diffResult = await provider.diff("plink_123", olds as any, news as any);
|
|
138
|
+
expect(diffResult.changes).toBe(expectedChanges);
|
|
139
|
+
expect(diffResult.replaces).toEqual(expect.arrayContaining(expectedReplaces));
|
|
140
|
+
expect(diffResult.replaces?.length).toBe(expectedReplaces.length);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("delete", () => {
|
|
145
|
+
const deleteCases = [
|
|
146
|
+
{
|
|
147
|
+
name: "apiKey is in state",
|
|
148
|
+
state: { apiKey: "sk_test_state" },
|
|
149
|
+
envSecret: undefined,
|
|
150
|
+
envApi: undefined,
|
|
151
|
+
mockError: false,
|
|
152
|
+
expectStripeInit: "sk_test_state",
|
|
153
|
+
expectUpdate: true,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
|
|
157
|
+
state: {},
|
|
158
|
+
envSecret: "sk_test_secret",
|
|
159
|
+
envApi: undefined,
|
|
160
|
+
mockError: false,
|
|
161
|
+
expectStripeInit: "sk_test_secret",
|
|
162
|
+
expectUpdate: true,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "apiKey is missing, falls back to STRIPE_API_KEY",
|
|
166
|
+
state: {},
|
|
167
|
+
envSecret: undefined,
|
|
168
|
+
envApi: "sk_test_api",
|
|
169
|
+
mockError: false,
|
|
170
|
+
expectStripeInit: "sk_test_api",
|
|
171
|
+
expectUpdate: true,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "apiKey is missing everywhere (NoOp)",
|
|
175
|
+
state: {},
|
|
176
|
+
envSecret: undefined,
|
|
177
|
+
envApi: undefined,
|
|
178
|
+
mockError: false,
|
|
179
|
+
expectStripeInit: null,
|
|
180
|
+
expectUpdate: false,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "Stripe SDK throws an error (caught safely)",
|
|
184
|
+
state: { apiKey: "sk_test_error" },
|
|
185
|
+
envSecret: undefined,
|
|
186
|
+
envApi: undefined,
|
|
187
|
+
mockError: true,
|
|
188
|
+
expectStripeInit: "sk_test_error",
|
|
189
|
+
expectUpdate: true,
|
|
190
|
+
}
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
it.each(deleteCases)("should handle deletion when $name", async ({ state, envSecret, envApi, mockError, expectStripeInit, expectUpdate }) => {
|
|
194
|
+
const originalSecret = process.env.STRIPE_SECRET_KEY;
|
|
195
|
+
const originalApi = process.env.STRIPE_API_KEY;
|
|
196
|
+
|
|
197
|
+
if (envSecret !== undefined) process.env.STRIPE_SECRET_KEY = envSecret;
|
|
198
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
199
|
+
|
|
200
|
+
if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
|
|
201
|
+
else delete process.env.STRIPE_API_KEY;
|
|
202
|
+
|
|
203
|
+
if (mockError) {
|
|
204
|
+
mockPaymentLinksUpdate.mockRejectedValueOnce(new Error("Stripe error"));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await provider.delete("plink_123", state as any);
|
|
208
|
+
|
|
209
|
+
if (expectStripeInit) {
|
|
210
|
+
expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (expectUpdate) {
|
|
214
|
+
expect(mockPaymentLinksUpdate).toHaveBeenCalledWith("plink_123", { active: false });
|
|
215
|
+
} else {
|
|
216
|
+
expect(mockPaymentLinksUpdate).not.toHaveBeenCalled();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (originalSecret !== undefined) process.env.STRIPE_SECRET_KEY = originalSecret;
|
|
220
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
221
|
+
|
|
222
|
+
if (originalApi !== undefined) process.env.STRIPE_API_KEY = originalApi;
|
|
223
|
+
else delete process.env.STRIPE_API_KEY;
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("StripeProductProvider", () => {
|
|
229
|
+
const provider = new StripeProductProvider();
|
|
230
|
+
|
|
231
|
+
describe("create", () => {
|
|
232
|
+
const createCases = [
|
|
233
|
+
{
|
|
234
|
+
name: "all properties provided",
|
|
235
|
+
inputs: {
|
|
236
|
+
apiKey: "sk_test_123",
|
|
237
|
+
name: "Full Product",
|
|
238
|
+
description: "A complete product",
|
|
239
|
+
taxCode: "txcd_123",
|
|
240
|
+
},
|
|
241
|
+
expectedPayload: {
|
|
242
|
+
name: "Full Product",
|
|
243
|
+
description: "A complete product",
|
|
244
|
+
tax_code: "txcd_123",
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "only required properties provided",
|
|
249
|
+
inputs: {
|
|
250
|
+
apiKey: "sk_test_123",
|
|
251
|
+
name: "Minimal Product",
|
|
252
|
+
},
|
|
253
|
+
expectedPayload: {
|
|
254
|
+
name: "Minimal Product",
|
|
255
|
+
description: undefined,
|
|
256
|
+
tax_code: undefined,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
it.each(createCases)("should correctly create a product when $name", async ({ inputs, expectedPayload }) => {
|
|
262
|
+
const result = await provider.create(inputs as any);
|
|
263
|
+
|
|
264
|
+
expect(result.id).toBe("prod_123");
|
|
265
|
+
expect(result.outs?.productId).toBe("prod_123");
|
|
266
|
+
|
|
267
|
+
expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
|
|
268
|
+
expect(mockProductsCreate).toHaveBeenCalledWith(expectedPayload);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("diff", () => {
|
|
273
|
+
const diffCases = [
|
|
274
|
+
{
|
|
275
|
+
name: "all properties change",
|
|
276
|
+
olds: { name: "old name", description: "old desc", taxCode: "tx_1", apiKey: "old_key" },
|
|
277
|
+
news: { name: "new name", description: "new desc", taxCode: "tx_2", apiKey: "new_key" },
|
|
278
|
+
expectedChanges: true,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "some properties change",
|
|
282
|
+
olds: { name: "old name", description: "old desc" },
|
|
283
|
+
news: { name: "old name", description: "new desc" },
|
|
284
|
+
expectedChanges: true,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "no properties change",
|
|
288
|
+
olds: { name: "same name", description: "same desc" },
|
|
289
|
+
news: { name: "same name", description: "same desc" },
|
|
290
|
+
expectedChanges: false,
|
|
291
|
+
}
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
it.each(diffCases)("should correctly detect diff when $name", async ({ olds, news, expectedChanges }) => {
|
|
295
|
+
const diffResult = await provider.diff("prod_123", olds as any, news as any);
|
|
296
|
+
expect(diffResult.changes).toBe(expectedChanges);
|
|
297
|
+
expect(diffResult.replaces).toHaveLength(0);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("update", () => {
|
|
302
|
+
it("should correctly update a product", async () => {
|
|
303
|
+
const result = await provider.update("prod_123",
|
|
304
|
+
{ apiKey: "sk_test_123", name: "Old Name" } as any,
|
|
305
|
+
{ apiKey: "sk_test_123", name: "New Name", description: "New Desc", taxCode: "txcd_999" } as any
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(result.outs?.productId).toBe("prod_123");
|
|
309
|
+
expect(result.outs?.name).toBe("New Name");
|
|
310
|
+
|
|
311
|
+
expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", {
|
|
312
|
+
name: "New Name",
|
|
313
|
+
description: "New Desc",
|
|
314
|
+
tax_code: "txcd_999",
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("delete", () => {
|
|
320
|
+
const deleteCases = [
|
|
321
|
+
{
|
|
322
|
+
name: "apiKey is in state",
|
|
323
|
+
state: { apiKey: "sk_test_state" },
|
|
324
|
+
envSecret: undefined,
|
|
325
|
+
envApi: undefined,
|
|
326
|
+
mockError: false,
|
|
327
|
+
expectStripeInit: "sk_test_state",
|
|
328
|
+
expectUpdate: true,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
|
|
332
|
+
state: {},
|
|
333
|
+
envSecret: "sk_test_secret",
|
|
334
|
+
envApi: undefined,
|
|
335
|
+
mockError: false,
|
|
336
|
+
expectStripeInit: "sk_test_secret",
|
|
337
|
+
expectUpdate: true,
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "apiKey is missing, falls back to STRIPE_API_KEY",
|
|
341
|
+
state: {},
|
|
342
|
+
envSecret: undefined,
|
|
343
|
+
envApi: "sk_test_api",
|
|
344
|
+
mockError: false,
|
|
345
|
+
expectStripeInit: "sk_test_api",
|
|
346
|
+
expectUpdate: true,
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "apiKey is missing everywhere (NoOp)",
|
|
350
|
+
state: {},
|
|
351
|
+
envSecret: undefined,
|
|
352
|
+
envApi: undefined,
|
|
353
|
+
mockError: false,
|
|
354
|
+
expectStripeInit: null,
|
|
355
|
+
expectUpdate: false,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "Stripe SDK throws an error (caught safely)",
|
|
359
|
+
state: { apiKey: "sk_test_error" },
|
|
360
|
+
envSecret: undefined,
|
|
361
|
+
envApi: undefined,
|
|
362
|
+
mockError: true,
|
|
363
|
+
expectStripeInit: "sk_test_error",
|
|
364
|
+
expectUpdate: true,
|
|
365
|
+
}
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
it.each(deleteCases)("should handle deletion when $name", async ({ state, envSecret, envApi, mockError, expectStripeInit, expectUpdate }) => {
|
|
369
|
+
const originalSecret = process.env.STRIPE_SECRET_KEY;
|
|
370
|
+
const originalApi = process.env.STRIPE_API_KEY;
|
|
371
|
+
|
|
372
|
+
if (envSecret !== undefined) process.env.STRIPE_SECRET_KEY = envSecret;
|
|
373
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
374
|
+
|
|
375
|
+
if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
|
|
376
|
+
else delete process.env.STRIPE_API_KEY;
|
|
377
|
+
|
|
378
|
+
if (mockError) {
|
|
379
|
+
mockProductsUpdate.mockRejectedValueOnce(new Error("Stripe error"));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await provider.delete("prod_123", state as any);
|
|
383
|
+
|
|
384
|
+
if (expectStripeInit) {
|
|
385
|
+
expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (expectUpdate) {
|
|
389
|
+
expect(mockProductsUpdate).toHaveBeenCalledWith("prod_123", { active: false });
|
|
390
|
+
} else {
|
|
391
|
+
expect(mockProductsUpdate).not.toHaveBeenCalled();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (originalSecret !== undefined) process.env.STRIPE_SECRET_KEY = originalSecret;
|
|
395
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
396
|
+
|
|
397
|
+
if (originalApi !== undefined) process.env.STRIPE_API_KEY = originalApi;
|
|
398
|
+
else delete process.env.STRIPE_API_KEY;
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("StripePriceProvider", () => {
|
|
404
|
+
const provider = new StripePriceProvider();
|
|
405
|
+
|
|
406
|
+
describe("create", () => {
|
|
407
|
+
const createCases = [
|
|
408
|
+
{
|
|
409
|
+
name: "required properties provided",
|
|
410
|
+
inputs: {
|
|
411
|
+
apiKey: "sk_test_123",
|
|
412
|
+
productId: "prod_123",
|
|
413
|
+
unitAmount: 1000,
|
|
414
|
+
currency: "usd",
|
|
415
|
+
},
|
|
416
|
+
expectedPayload: {
|
|
417
|
+
product: "prod_123",
|
|
418
|
+
unit_amount: 1000,
|
|
419
|
+
currency: "usd",
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
it.each(createCases)("should correctly create a price when $name", async ({ inputs, expectedPayload }) => {
|
|
425
|
+
const result = await provider.create(inputs as any);
|
|
426
|
+
|
|
427
|
+
expect(result.id).toBe("price_123");
|
|
428
|
+
expect(result.outs?.priceId).toBe("price_123");
|
|
429
|
+
|
|
430
|
+
expect(MockedStripe).toHaveBeenCalledWith(inputs.apiKey);
|
|
431
|
+
expect(mockPricesCreate).toHaveBeenCalledWith(expectedPayload);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe("diff", () => {
|
|
436
|
+
const diffCases = [
|
|
437
|
+
{
|
|
438
|
+
name: "all properties change",
|
|
439
|
+
olds: { productId: "prod_1", unitAmount: 100, currency: "usd", apiKey: "old_key" },
|
|
440
|
+
news: { productId: "prod_2", unitAmount: 200, currency: "eur", apiKey: "new_key" },
|
|
441
|
+
expectedChanges: true,
|
|
442
|
+
expectedReplaces: ["productId", "unitAmount", "currency", "apiKey"]
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "some properties change",
|
|
446
|
+
olds: { productId: "prod_1", unitAmount: 100, currency: "usd" },
|
|
447
|
+
news: { productId: "prod_1", unitAmount: 200, currency: "usd" },
|
|
448
|
+
expectedChanges: true,
|
|
449
|
+
expectedReplaces: ["unitAmount"]
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "no properties change",
|
|
453
|
+
olds: { productId: "prod_1", unitAmount: 100, currency: "usd" },
|
|
454
|
+
news: { productId: "prod_1", unitAmount: 100, currency: "usd" },
|
|
455
|
+
expectedChanges: false,
|
|
456
|
+
expectedReplaces: []
|
|
457
|
+
}
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
it.each(diffCases)("should correctly detect diff when $name", async ({ olds, news, expectedChanges, expectedReplaces }) => {
|
|
461
|
+
const diffResult = await provider.diff("price_123", olds as any, news as any);
|
|
462
|
+
expect(diffResult.changes).toBe(expectedChanges);
|
|
463
|
+
expect(diffResult.replaces).toEqual(expect.arrayContaining(expectedReplaces));
|
|
464
|
+
expect(diffResult.replaces?.length).toBe(expectedReplaces.length);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe("delete", () => {
|
|
469
|
+
const deleteCases = [
|
|
470
|
+
{
|
|
471
|
+
name: "apiKey is in state",
|
|
472
|
+
state: { apiKey: "sk_test_state" },
|
|
473
|
+
envSecret: undefined,
|
|
474
|
+
envApi: undefined,
|
|
475
|
+
mockError: false,
|
|
476
|
+
expectStripeInit: "sk_test_state",
|
|
477
|
+
expectUpdate: true,
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "apiKey is missing, falls back to STRIPE_SECRET_KEY",
|
|
481
|
+
state: {},
|
|
482
|
+
envSecret: "sk_test_secret",
|
|
483
|
+
envApi: undefined,
|
|
484
|
+
mockError: false,
|
|
485
|
+
expectStripeInit: "sk_test_secret",
|
|
486
|
+
expectUpdate: true,
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "apiKey is missing, falls back to STRIPE_API_KEY",
|
|
490
|
+
state: {},
|
|
491
|
+
envSecret: undefined,
|
|
492
|
+
envApi: "sk_test_api",
|
|
493
|
+
mockError: false,
|
|
494
|
+
expectStripeInit: "sk_test_api",
|
|
495
|
+
expectUpdate: true,
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "apiKey is missing everywhere (NoOp)",
|
|
499
|
+
state: {},
|
|
500
|
+
envSecret: undefined,
|
|
501
|
+
envApi: undefined,
|
|
502
|
+
mockError: false,
|
|
503
|
+
expectStripeInit: null,
|
|
504
|
+
expectUpdate: false,
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "Stripe SDK throws an error (caught safely)",
|
|
508
|
+
state: { apiKey: "sk_test_error" },
|
|
509
|
+
envSecret: undefined,
|
|
510
|
+
envApi: undefined,
|
|
511
|
+
mockError: true,
|
|
512
|
+
expectStripeInit: "sk_test_error",
|
|
513
|
+
expectUpdate: true,
|
|
514
|
+
}
|
|
515
|
+
];
|
|
516
|
+
|
|
517
|
+
it.each(deleteCases)("should handle deletion when $name", async ({ state, envSecret, envApi, mockError, expectStripeInit, expectUpdate }) => {
|
|
518
|
+
const originalSecret = process.env.STRIPE_SECRET_KEY;
|
|
519
|
+
const originalApi = process.env.STRIPE_API_KEY;
|
|
520
|
+
|
|
521
|
+
if (envSecret !== undefined) process.env.STRIPE_SECRET_KEY = envSecret;
|
|
522
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
523
|
+
|
|
524
|
+
if (envApi !== undefined) process.env.STRIPE_API_KEY = envApi;
|
|
525
|
+
else delete process.env.STRIPE_API_KEY;
|
|
526
|
+
|
|
527
|
+
if (mockError) {
|
|
528
|
+
mockPricesUpdate.mockRejectedValueOnce(new Error("Stripe error"));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await provider.delete("price_123", state as any);
|
|
532
|
+
|
|
533
|
+
if (expectStripeInit) {
|
|
534
|
+
expect(MockedStripe).toHaveBeenCalledWith(expectStripeInit);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (expectUpdate) {
|
|
538
|
+
expect(mockPricesUpdate).toHaveBeenCalledWith("price_123", { active: false });
|
|
539
|
+
} else {
|
|
540
|
+
expect(mockPricesUpdate).not.toHaveBeenCalled();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (originalSecret !== undefined) process.env.STRIPE_SECRET_KEY = originalSecret;
|
|
544
|
+
else delete process.env.STRIPE_SECRET_KEY;
|
|
545
|
+
|
|
546
|
+
if (originalApi !== undefined) process.env.STRIPE_API_KEY = originalApi;
|
|
547
|
+
else delete process.env.STRIPE_API_KEY;
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
});
|