stripe-no-webhooks 0.0.8 → 0.0.11
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/README.md +118 -104
- package/bin/cli.js +41 -852
- package/bin/commands/backfill.js +389 -0
- package/bin/commands/config.js +272 -0
- package/bin/commands/generate.js +110 -0
- package/bin/commands/helpers/backfill-maps.js +279 -0
- package/bin/commands/helpers/dev-webhook-listener.js +76 -0
- package/bin/commands/helpers/sync-helpers.js +190 -0
- package/bin/commands/helpers/utils.js +168 -0
- package/bin/commands/migrate.js +104 -0
- package/bin/commands/sync.js +433 -0
- package/dist/BillingConfig-CpHPJg4Q.d.mts +54 -0
- package/dist/BillingConfig-CpHPJg4Q.d.ts +54 -0
- package/dist/client.d.mts +32 -8
- package/dist/client.d.ts +32 -8
- package/dist/client.js +82 -20
- package/dist/client.mjs +80 -19
- package/dist/index.d.mts +460 -66
- package/dist/index.d.ts +460 -66
- package/dist/index.js +2736 -168
- package/dist/index.mjs +2730 -167
- package/package.json +9 -3
- package/src/templates/PricingPage.tsx +450 -0
- package/src/templates/app-router.ts +23 -16
- package/src/templates/lib-billing.ts +27 -0
- package/src/templates/pages-router.ts +25 -18
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { getTemplatesDir, detectRouterType } = require("./helpers/utils");
|
|
4
|
+
|
|
5
|
+
const GENERATORS = {
|
|
6
|
+
"pricing-page": {
|
|
7
|
+
description: "A ready-to-use pricing page component with embedded styles",
|
|
8
|
+
template: "PricingPage.tsx",
|
|
9
|
+
defaultOutput: "components/PricingPage.tsx",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function generate(component, options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
cwd = process.cwd(),
|
|
16
|
+
logger = console,
|
|
17
|
+
output,
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
if (!component) {
|
|
21
|
+
logger.log("\nUsage: npx stripe-no-webhooks generate <component>\n");
|
|
22
|
+
logger.log("Available components:");
|
|
23
|
+
for (const [name, config] of Object.entries(GENERATORS)) {
|
|
24
|
+
logger.log(` ${name.padEnd(20)} ${config.description}`);
|
|
25
|
+
}
|
|
26
|
+
logger.log("\nExample:");
|
|
27
|
+
logger.log(" npx stripe-no-webhooks generate pricing-page");
|
|
28
|
+
logger.log(" npx stripe-no-webhooks generate pricing-page --output src/components/Pricing.tsx");
|
|
29
|
+
return { success: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const generator = GENERATORS[component];
|
|
33
|
+
if (!generator) {
|
|
34
|
+
logger.error(`\n❌ Unknown component: ${component}`);
|
|
35
|
+
logger.log("\nAvailable components:");
|
|
36
|
+
for (const name of Object.keys(GENERATORS)) {
|
|
37
|
+
logger.log(` ${name}`);
|
|
38
|
+
}
|
|
39
|
+
return { success: false, error: `Unknown component: ${component}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { useSrc } = detectRouterType(cwd);
|
|
43
|
+
|
|
44
|
+
// Determine output path
|
|
45
|
+
let outputPath;
|
|
46
|
+
if (output) {
|
|
47
|
+
outputPath = path.isAbsolute(output) ? output : path.join(cwd, output);
|
|
48
|
+
} else {
|
|
49
|
+
const defaultOutput = useSrc
|
|
50
|
+
? `src/${generator.defaultOutput}`
|
|
51
|
+
: generator.defaultOutput;
|
|
52
|
+
outputPath = path.join(cwd, defaultOutput);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if file already exists
|
|
56
|
+
if (fs.existsSync(outputPath)) {
|
|
57
|
+
logger.log(`\n⚠️ File already exists: ${path.relative(cwd, outputPath)}`);
|
|
58
|
+
logger.log(" Use --output to specify a different path");
|
|
59
|
+
return { success: false, error: "File already exists" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Read template
|
|
63
|
+
const templatesDir = getTemplatesDir();
|
|
64
|
+
const templatePath = path.join(templatesDir, generator.template);
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(templatePath)) {
|
|
67
|
+
logger.error(`\n❌ Template not found: ${generator.template}`);
|
|
68
|
+
return { success: false, error: "Template not found" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const template = fs.readFileSync(templatePath, "utf8");
|
|
72
|
+
|
|
73
|
+
// Ensure output directory exists
|
|
74
|
+
const outputDir = path.dirname(outputPath);
|
|
75
|
+
if (!fs.existsSync(outputDir)) {
|
|
76
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Write file
|
|
80
|
+
fs.writeFileSync(outputPath, template);
|
|
81
|
+
|
|
82
|
+
const relativePath = path.relative(cwd, outputPath);
|
|
83
|
+
logger.log(`\n✅ Created ${relativePath}\n`);
|
|
84
|
+
|
|
85
|
+
// Print usage instructions
|
|
86
|
+
logger.log("Usage:");
|
|
87
|
+
logger.log("─".repeat(50));
|
|
88
|
+
logger.log(`
|
|
89
|
+
import { PricingPage } from "./${path.relative(path.join(cwd, useSrc ? "src" : ""), outputPath).replace(/\\/g, "/").replace(/\.tsx$/, "")}";
|
|
90
|
+
import billingConfig from "@/billing.config";
|
|
91
|
+
|
|
92
|
+
// Get plans for your environment
|
|
93
|
+
const plans = billingConfig.test?.plans || [];
|
|
94
|
+
|
|
95
|
+
// In your page:
|
|
96
|
+
export default function PricingRoute() {
|
|
97
|
+
return (
|
|
98
|
+
<PricingPage
|
|
99
|
+
plans={plans}
|
|
100
|
+
currentPlanId="free" // Pass the user's current plan
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
`);
|
|
105
|
+
logger.log("─".repeat(50));
|
|
106
|
+
|
|
107
|
+
return { success: true, path: outputPath };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { generate };
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
export const SYNC_OBJECTS = [
|
|
2
|
+
"all",
|
|
3
|
+
"charge",
|
|
4
|
+
"checkout_session",
|
|
5
|
+
"coupon",
|
|
6
|
+
"credit_note",
|
|
7
|
+
"customer",
|
|
8
|
+
"dispute",
|
|
9
|
+
"invoice",
|
|
10
|
+
"payment_intent",
|
|
11
|
+
"payment_method",
|
|
12
|
+
"plan",
|
|
13
|
+
"price",
|
|
14
|
+
"product",
|
|
15
|
+
"refund",
|
|
16
|
+
"setup_intent",
|
|
17
|
+
"subscription",
|
|
18
|
+
"subscription_schedule",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const SYNC_ORDER = [
|
|
22
|
+
{
|
|
23
|
+
key: "product",
|
|
24
|
+
table: "products",
|
|
25
|
+
label: "Products",
|
|
26
|
+
stripeMethod: "products",
|
|
27
|
+
},
|
|
28
|
+
{ key: "price", table: "prices", label: "Prices", stripeMethod: "prices" },
|
|
29
|
+
{
|
|
30
|
+
key: "coupon",
|
|
31
|
+
table: "coupons",
|
|
32
|
+
label: "Coupons",
|
|
33
|
+
stripeMethod: "coupons",
|
|
34
|
+
},
|
|
35
|
+
{ key: "plan", table: "plans", label: "Plans", stripeMethod: "plans" },
|
|
36
|
+
{
|
|
37
|
+
key: "customer",
|
|
38
|
+
table: "customers",
|
|
39
|
+
label: "Customers",
|
|
40
|
+
stripeMethod: "customers",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "subscription",
|
|
44
|
+
table: "subscriptions",
|
|
45
|
+
label: "Subscriptions",
|
|
46
|
+
stripeMethod: "subscriptions",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "subscription_schedule",
|
|
50
|
+
table: "subscription_schedules",
|
|
51
|
+
label: "Subscription Schedules",
|
|
52
|
+
stripeMethod: "subscriptionSchedules",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "invoice",
|
|
56
|
+
table: "invoices",
|
|
57
|
+
label: "Invoices",
|
|
58
|
+
stripeMethod: "invoices",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: "charge",
|
|
62
|
+
table: "charges",
|
|
63
|
+
label: "Charges",
|
|
64
|
+
stripeMethod: "charges",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "payment_intent",
|
|
68
|
+
table: "payment_intents",
|
|
69
|
+
label: "Payment Intents",
|
|
70
|
+
stripeMethod: "paymentIntents",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: "payment_method",
|
|
74
|
+
table: "payment_methods",
|
|
75
|
+
label: "Payment Methods",
|
|
76
|
+
stripeMethod: "paymentMethods",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "setup_intent",
|
|
80
|
+
table: "setup_intents",
|
|
81
|
+
label: "Setup Intents",
|
|
82
|
+
stripeMethod: "setupIntents",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: "refund",
|
|
86
|
+
table: "refunds",
|
|
87
|
+
label: "Refunds",
|
|
88
|
+
stripeMethod: "refunds",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "dispute",
|
|
92
|
+
table: "disputes",
|
|
93
|
+
label: "Disputes",
|
|
94
|
+
stripeMethod: "disputes",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: "credit_note",
|
|
98
|
+
table: "credit_notes",
|
|
99
|
+
label: "Credit Notes",
|
|
100
|
+
stripeMethod: "creditNotes",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: "checkout_session",
|
|
104
|
+
table: "checkout_sessions",
|
|
105
|
+
label: "Checkout Sessions",
|
|
106
|
+
stripeMethod: "checkout.sessions",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
export const REQUIRED_TABLES = [
|
|
111
|
+
"customers",
|
|
112
|
+
"products",
|
|
113
|
+
"prices",
|
|
114
|
+
"subscriptions",
|
|
115
|
+
"invoices",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// Field definitions: [stripeField, dbField?, isRef?, isJson?]
|
|
119
|
+
// If dbField is omitted, uses stripeField. If isRef=true, extracts .id from objects
|
|
120
|
+
export const FIELD_MAPS = {
|
|
121
|
+
products: [
|
|
122
|
+
["name"],
|
|
123
|
+
["description"],
|
|
124
|
+
["active"],
|
|
125
|
+
["images", null, false, true],
|
|
126
|
+
["default_price", null, true],
|
|
127
|
+
["updated"],
|
|
128
|
+
],
|
|
129
|
+
prices: [
|
|
130
|
+
["active"],
|
|
131
|
+
["currency"],
|
|
132
|
+
["unit_amount"],
|
|
133
|
+
["type"],
|
|
134
|
+
["recurring", null, false, true],
|
|
135
|
+
["product", null, true],
|
|
136
|
+
["nickname"],
|
|
137
|
+
["billing_scheme"],
|
|
138
|
+
["lookup_key"],
|
|
139
|
+
],
|
|
140
|
+
customers: [
|
|
141
|
+
["email"],
|
|
142
|
+
["name"],
|
|
143
|
+
["phone"],
|
|
144
|
+
["description"],
|
|
145
|
+
["address", null, false, true],
|
|
146
|
+
["shipping", null, false, true],
|
|
147
|
+
["balance"],
|
|
148
|
+
["currency"],
|
|
149
|
+
["delinquent"],
|
|
150
|
+
["default_source"],
|
|
151
|
+
["invoice_settings", null, false, true],
|
|
152
|
+
["deleted"],
|
|
153
|
+
],
|
|
154
|
+
subscriptions: [
|
|
155
|
+
["status"],
|
|
156
|
+
["customer", null, true],
|
|
157
|
+
["current_period_start"],
|
|
158
|
+
["current_period_end"],
|
|
159
|
+
["cancel_at_period_end"],
|
|
160
|
+
["canceled_at"],
|
|
161
|
+
["items", null, false, true],
|
|
162
|
+
["latest_invoice", null, true],
|
|
163
|
+
["default_payment_method", null, true],
|
|
164
|
+
["collection_method"],
|
|
165
|
+
],
|
|
166
|
+
invoices: [
|
|
167
|
+
["status"],
|
|
168
|
+
["customer", null, true],
|
|
169
|
+
["subscription", null, true],
|
|
170
|
+
["total"],
|
|
171
|
+
["amount_due"],
|
|
172
|
+
["amount_paid"],
|
|
173
|
+
["currency"],
|
|
174
|
+
["paid"],
|
|
175
|
+
["number"],
|
|
176
|
+
["hosted_invoice_url"],
|
|
177
|
+
["invoice_pdf"],
|
|
178
|
+
["lines", null, false, true],
|
|
179
|
+
["period_start"],
|
|
180
|
+
["period_end"],
|
|
181
|
+
],
|
|
182
|
+
charges: [
|
|
183
|
+
["amount"],
|
|
184
|
+
["currency"],
|
|
185
|
+
["status"],
|
|
186
|
+
["paid"],
|
|
187
|
+
["refunded"],
|
|
188
|
+
["customer", null, true],
|
|
189
|
+
["payment_intent", null, true],
|
|
190
|
+
["invoice", null, true],
|
|
191
|
+
["description"],
|
|
192
|
+
],
|
|
193
|
+
payment_intents: [
|
|
194
|
+
["amount"],
|
|
195
|
+
["currency"],
|
|
196
|
+
["status"],
|
|
197
|
+
["customer", null, true],
|
|
198
|
+
["payment_method", null, true],
|
|
199
|
+
["description"],
|
|
200
|
+
["invoice", null, true],
|
|
201
|
+
],
|
|
202
|
+
payment_methods: [
|
|
203
|
+
["type"],
|
|
204
|
+
["customer", null, true],
|
|
205
|
+
["billing_details", null, false, true],
|
|
206
|
+
["card", null, false, true],
|
|
207
|
+
],
|
|
208
|
+
coupons: [
|
|
209
|
+
["name"],
|
|
210
|
+
["percent_off"],
|
|
211
|
+
["amount_off"],
|
|
212
|
+
["currency"],
|
|
213
|
+
["duration"],
|
|
214
|
+
["duration_in_months"],
|
|
215
|
+
["valid"],
|
|
216
|
+
["times_redeemed"],
|
|
217
|
+
["max_redemptions"],
|
|
218
|
+
],
|
|
219
|
+
plans: [
|
|
220
|
+
["active"],
|
|
221
|
+
["amount"],
|
|
222
|
+
["currency"],
|
|
223
|
+
["interval"],
|
|
224
|
+
["interval_count"],
|
|
225
|
+
["product", null, true],
|
|
226
|
+
["nickname"],
|
|
227
|
+
],
|
|
228
|
+
refunds: [
|
|
229
|
+
["amount"],
|
|
230
|
+
["currency"],
|
|
231
|
+
["status"],
|
|
232
|
+
["charge", null, true],
|
|
233
|
+
["payment_intent", null, true],
|
|
234
|
+
["reason"],
|
|
235
|
+
],
|
|
236
|
+
disputes: [
|
|
237
|
+
["amount"],
|
|
238
|
+
["currency"],
|
|
239
|
+
["status"],
|
|
240
|
+
["charge", null, true],
|
|
241
|
+
["payment_intent", null, true],
|
|
242
|
+
["reason"],
|
|
243
|
+
],
|
|
244
|
+
setup_intents: [
|
|
245
|
+
["status"],
|
|
246
|
+
["customer", null, true],
|
|
247
|
+
["payment_method", null, true],
|
|
248
|
+
["description"],
|
|
249
|
+
["usage"],
|
|
250
|
+
],
|
|
251
|
+
subscription_schedules: [
|
|
252
|
+
["status"],
|
|
253
|
+
["customer", null, true],
|
|
254
|
+
["subscription", null, true],
|
|
255
|
+
["phases", null, false, true],
|
|
256
|
+
["current_phase", null, false, true],
|
|
257
|
+
["end_behavior"],
|
|
258
|
+
],
|
|
259
|
+
credit_notes: [
|
|
260
|
+
["amount"],
|
|
261
|
+
["currency"],
|
|
262
|
+
["status"],
|
|
263
|
+
["customer", null, true],
|
|
264
|
+
["invoice", null, true],
|
|
265
|
+
["reason"],
|
|
266
|
+
["total"],
|
|
267
|
+
],
|
|
268
|
+
checkout_sessions: [
|
|
269
|
+
["status"],
|
|
270
|
+
["customer", null, true],
|
|
271
|
+
["payment_intent", null, true],
|
|
272
|
+
["subscription", null, true],
|
|
273
|
+
["mode"],
|
|
274
|
+
["payment_status"],
|
|
275
|
+
["amount_total"],
|
|
276
|
+
["currency"],
|
|
277
|
+
["url"],
|
|
278
|
+
],
|
|
279
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { execSync } = require("child_process");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
function checkStripeCLI() {
|
|
6
|
+
try {
|
|
7
|
+
execSync("stripe --version", { stdio: "pipe" });
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getNextDevPort(cwd = process.cwd()) {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(
|
|
17
|
+
fs.readFileSync(path.join(cwd, "package.json"), "utf8")
|
|
18
|
+
);
|
|
19
|
+
const match = (pkg.scripts?.dev || "").match(/-p\s*(\d+)|--port\s*(\d+)/);
|
|
20
|
+
return match ? match[1] || match[2] : "3000";
|
|
21
|
+
} catch {
|
|
22
|
+
return "3000";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function setupDev(options = {}) {
|
|
27
|
+
const { cwd = process.cwd(), logger = console, exitOnError = true } = options;
|
|
28
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(pkgPath)) {
|
|
31
|
+
logger.error("❌ package.json not found in current directory.");
|
|
32
|
+
if (exitOnError) process.exit(1);
|
|
33
|
+
return { success: false, error: "package.json not found" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const hasStripeCLI = checkStripeCLI();
|
|
37
|
+
if (!hasStripeCLI) {
|
|
38
|
+
logger.log("⚠️ Stripe CLI not found. Webhook forwarding will be skipped.");
|
|
39
|
+
logger.log(" Install it from: https://stripe.com/docs/stripe-cli\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
43
|
+
pkg.scripts = pkg.scripts || {};
|
|
44
|
+
|
|
45
|
+
const port = getNextDevPort(cwd);
|
|
46
|
+
const webhookUrl = `localhost:${port}/api/stripe/webhook`;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
pkg.scripts["dev:webhooks"] ||
|
|
50
|
+
(pkg.scripts.dev || "").includes("stripe listen")
|
|
51
|
+
) {
|
|
52
|
+
logger.log("✓ Webhook forwarding already configured in package.json");
|
|
53
|
+
return { success: true, alreadyConfigured: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stripeListenScript = `if command -v stripe >/dev/null 2>&1; then stripe listen --forward-to ${webhookUrl}; else echo "⚠️ Stripe CLI not available, skipping webhook forwarding"; fi`;
|
|
57
|
+
pkg.scripts["dev:stripe"] = stripeListenScript;
|
|
58
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
59
|
+
|
|
60
|
+
logger.log("✅ Added dev:stripe script to package.json\n");
|
|
61
|
+
logger.log(
|
|
62
|
+
` npm run dev:stripe - stripe listen --forward-to ${webhookUrl}\n`
|
|
63
|
+
);
|
|
64
|
+
logger.log(`Webhook endpoint: ${webhookUrl}`);
|
|
65
|
+
|
|
66
|
+
if (!hasStripeCLI) {
|
|
67
|
+
logger.log(
|
|
68
|
+
"\n⚠️ Note: Install the Stripe CLI to enable webhook forwarding:"
|
|
69
|
+
);
|
|
70
|
+
logger.log(" https://stripe.com/docs/stripe-cli");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { success: true, scripts: { "dev:stripe": stripeListenScript } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { setupDev, checkStripeCLI, getNextDevPort };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for the sync command.
|
|
3
|
+
* Extracted for testability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds a lookup map of Stripe products by name (case-insensitive)
|
|
8
|
+
* @param {Array} stripeProducts - Array of Stripe product objects
|
|
9
|
+
* @returns {Object} Map of lowercase product name to product object
|
|
10
|
+
*/
|
|
11
|
+
function buildProductsByNameMap(stripeProducts) {
|
|
12
|
+
const map = {};
|
|
13
|
+
for (const product of stripeProducts) {
|
|
14
|
+
const key = product.name.toLowerCase().trim();
|
|
15
|
+
if (!map[key]) {
|
|
16
|
+
map[key] = product;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return map;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds a lookup map of Stripe prices by composite key
|
|
24
|
+
* Key format: productId:amount:currency:interval
|
|
25
|
+
* @param {Array} stripePrices - Array of Stripe price objects
|
|
26
|
+
* @returns {Object} Map of composite key to price object
|
|
27
|
+
*/
|
|
28
|
+
function buildPricesByKeyMap(stripePrices) {
|
|
29
|
+
const map = {};
|
|
30
|
+
for (const price of stripePrices) {
|
|
31
|
+
const productId =
|
|
32
|
+
typeof price.product === "string" ? price.product : price.product.id;
|
|
33
|
+
const interval = price.recurring?.interval || "one_time";
|
|
34
|
+
const key = `${productId}:${price.unit_amount}:${price.currency}:${interval}`;
|
|
35
|
+
if (!map[key]) {
|
|
36
|
+
map[key] = price;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Finds a matching product by name (case-insensitive)
|
|
44
|
+
* @param {Object} productsByName - Map of lowercase product names to products
|
|
45
|
+
* @param {string} planName - Name of the plan to match
|
|
46
|
+
* @returns {Object|null} Matching product or null
|
|
47
|
+
*/
|
|
48
|
+
function findMatchingProduct(productsByName, planName) {
|
|
49
|
+
const key = planName.toLowerCase().trim();
|
|
50
|
+
return productsByName[key] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates a price key for matching
|
|
55
|
+
* @param {string} productId - Stripe product ID
|
|
56
|
+
* @param {number} amount - Price amount in cents
|
|
57
|
+
* @param {string} currency - Currency code
|
|
58
|
+
* @param {string} interval - Price interval (month, year, one_time, etc.)
|
|
59
|
+
* @returns {string} Composite key
|
|
60
|
+
*/
|
|
61
|
+
function generatePriceKey(productId, amount, currency, interval) {
|
|
62
|
+
return `${productId}:${amount}:${currency.toLowerCase()}:${interval || "one_time"}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Finds a matching price by product, amount, currency, and interval
|
|
67
|
+
* @param {Object} pricesByKey - Map of composite keys to prices
|
|
68
|
+
* @param {string} productId - Stripe product ID
|
|
69
|
+
* @param {Object} localPrice - Local price object with amount, currency, interval
|
|
70
|
+
* @returns {Object|null} Matching price or null
|
|
71
|
+
*/
|
|
72
|
+
function findMatchingPrice(pricesByKey, productId, localPrice) {
|
|
73
|
+
const key = generatePriceKey(
|
|
74
|
+
productId,
|
|
75
|
+
localPrice.amount,
|
|
76
|
+
localPrice.currency,
|
|
77
|
+
localPrice.interval
|
|
78
|
+
);
|
|
79
|
+
return pricesByKey[key] || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Processes sync for a single plan
|
|
84
|
+
* @param {Object} plan - Local plan object
|
|
85
|
+
* @param {Object} productsByName - Map of Stripe products by name
|
|
86
|
+
* @param {Object} pricesByKey - Map of Stripe prices by key
|
|
87
|
+
* @param {Object} stripeApi - Stripe API interface with products.create and prices.create
|
|
88
|
+
* @returns {Object} Result with updated plan, counts, and any errors
|
|
89
|
+
*/
|
|
90
|
+
async function syncPlan(plan, productsByName, pricesByKey, stripeApi) {
|
|
91
|
+
const result = {
|
|
92
|
+
plan: { ...plan },
|
|
93
|
+
productMatched: false,
|
|
94
|
+
productCreated: false,
|
|
95
|
+
pricesMatched: 0,
|
|
96
|
+
pricesCreated: 0,
|
|
97
|
+
errors: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let productId = plan.id;
|
|
101
|
+
|
|
102
|
+
// Handle product sync
|
|
103
|
+
if (!productId) {
|
|
104
|
+
const existingProduct = findMatchingProduct(productsByName, plan.name);
|
|
105
|
+
|
|
106
|
+
if (existingProduct) {
|
|
107
|
+
productId = existingProduct.id;
|
|
108
|
+
result.plan.id = productId;
|
|
109
|
+
result.productMatched = true;
|
|
110
|
+
} else {
|
|
111
|
+
try {
|
|
112
|
+
const newProduct = await stripeApi.products.create({
|
|
113
|
+
name: plan.name,
|
|
114
|
+
description: plan.description || undefined,
|
|
115
|
+
});
|
|
116
|
+
productId = newProduct.id;
|
|
117
|
+
result.plan.id = productId;
|
|
118
|
+
result.productCreated = true;
|
|
119
|
+
// Add to map for price matching
|
|
120
|
+
const nameKey = plan.name.toLowerCase().trim();
|
|
121
|
+
productsByName[nameKey] = newProduct;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
result.errors.push(`Failed to create product "${plan.name}": ${error.message}`);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle price sync
|
|
130
|
+
if (plan.price && plan.price.length > 0) {
|
|
131
|
+
result.plan.price = [];
|
|
132
|
+
|
|
133
|
+
for (const price of plan.price) {
|
|
134
|
+
const updatedPrice = { ...price };
|
|
135
|
+
|
|
136
|
+
if (!price.id) {
|
|
137
|
+
const existingPrice = findMatchingPrice(pricesByKey, productId, price);
|
|
138
|
+
|
|
139
|
+
if (existingPrice) {
|
|
140
|
+
updatedPrice.id = existingPrice.id;
|
|
141
|
+
result.pricesMatched++;
|
|
142
|
+
} else {
|
|
143
|
+
try {
|
|
144
|
+
const priceParams = {
|
|
145
|
+
product: productId,
|
|
146
|
+
unit_amount: price.amount,
|
|
147
|
+
currency: price.currency.toLowerCase(),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (price.interval && price.interval !== "one_time") {
|
|
151
|
+
priceParams.recurring = {
|
|
152
|
+
interval: price.interval,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const newPrice = await stripeApi.prices.create(priceParams);
|
|
157
|
+
updatedPrice.id = newPrice.id;
|
|
158
|
+
result.pricesCreated++;
|
|
159
|
+
|
|
160
|
+
// Add to map
|
|
161
|
+
const priceKey = generatePriceKey(
|
|
162
|
+
productId,
|
|
163
|
+
price.amount,
|
|
164
|
+
price.currency,
|
|
165
|
+
price.interval
|
|
166
|
+
);
|
|
167
|
+
pricesByKey[priceKey] = newPrice;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
result.errors.push(
|
|
170
|
+
`Failed to create price ${price.amount} ${price.currency}/${price.interval}: ${error.message}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
result.plan.price.push(updatedPrice);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
buildProductsByNameMap,
|
|
185
|
+
buildPricesByKeyMap,
|
|
186
|
+
findMatchingProduct,
|
|
187
|
+
findMatchingPrice,
|
|
188
|
+
generatePriceKey,
|
|
189
|
+
syncPlan,
|
|
190
|
+
};
|