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.
@@ -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
+ };