spaps 0.5.49 ā 0.7.0
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/AI_TOOLS.json +1 -1
- package/README.md +17 -17
- package/client.js +1 -1
- package/package.json +3 -8
- package/src/ai-helper.js +2 -2
- package/src/cli-dispatcher.js +32 -18
- package/src/config.js +1 -1
- package/src/docs-html.js +1 -1
- package/src/docs-system.js +8 -8
- package/src/doctor.js +4 -4
- package/src/error-handler.js +6 -6
- package/src/handlers.js +33 -24
- package/src/help-system.js +6 -6
- package/src/local-server.js +268 -1603
- package/src/admin-local.js +0 -408
- package/src/stripe-local.js +0 -263
package/src/admin-local.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local Admin Management for SPAPS
|
|
3
|
-
* Provides product, customer, and order management in local mode
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const { v4: uuidv4 } = require('uuid');
|
|
9
|
-
|
|
10
|
-
class LocalAdminManager {
|
|
11
|
-
constructor(dataDir = './.spaps') {
|
|
12
|
-
this.dataDir = dataDir;
|
|
13
|
-
this.ensureDataDir();
|
|
14
|
-
this.loadData();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
ensureDataDir() {
|
|
18
|
-
if (!fs.existsSync(this.dataDir)) {
|
|
19
|
-
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
loadData() {
|
|
24
|
-
// Load or create products
|
|
25
|
-
this.productsFile = path.join(this.dataDir, 'products.json');
|
|
26
|
-
if (fs.existsSync(this.productsFile)) {
|
|
27
|
-
this.products = JSON.parse(fs.readFileSync(this.productsFile, 'utf8'));
|
|
28
|
-
} else {
|
|
29
|
-
this.products = this.getDefaultProducts();
|
|
30
|
-
this.saveProducts();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Load or create orders
|
|
34
|
-
this.ordersFile = path.join(this.dataDir, 'orders.json');
|
|
35
|
-
if (fs.existsSync(this.ordersFile)) {
|
|
36
|
-
this.orders = JSON.parse(fs.readFileSync(this.ordersFile, 'utf8'));
|
|
37
|
-
} else {
|
|
38
|
-
this.orders = [];
|
|
39
|
-
this.saveOrders();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Load or create customers
|
|
43
|
-
this.customersFile = path.join(this.dataDir, 'customers.json');
|
|
44
|
-
if (fs.existsSync(this.customersFile)) {
|
|
45
|
-
this.customers = JSON.parse(fs.readFileSync(this.customersFile, 'utf8'));
|
|
46
|
-
} else {
|
|
47
|
-
this.customers = [];
|
|
48
|
-
this.saveCustomers();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
getDefaultProducts() {
|
|
53
|
-
return [
|
|
54
|
-
{
|
|
55
|
-
id: 'prod_local_validate',
|
|
56
|
-
price_id: 'price_local_validate',
|
|
57
|
-
name: 'Validate Tier',
|
|
58
|
-
description: 'Landing page with data capture and analytics',
|
|
59
|
-
price: 50000,
|
|
60
|
-
currency: 'usd',
|
|
61
|
-
features: [
|
|
62
|
-
'Landing page',
|
|
63
|
-
'Data capture setup',
|
|
64
|
-
'Analytics integration',
|
|
65
|
-
'Deployment'
|
|
66
|
-
],
|
|
67
|
-
active: true,
|
|
68
|
-
created_at: new Date().toISOString()
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'prod_local_prototype',
|
|
72
|
-
price_id: 'price_local_prototype',
|
|
73
|
-
name: 'Prototype Tier',
|
|
74
|
-
description: 'Clickable prototype with core user flows',
|
|
75
|
-
price: 250000,
|
|
76
|
-
currency: 'usd',
|
|
77
|
-
features: [
|
|
78
|
-
'Clickable interface',
|
|
79
|
-
'Core user flows',
|
|
80
|
-
'Demo environment',
|
|
81
|
-
'Shareable prototype link'
|
|
82
|
-
],
|
|
83
|
-
active: true,
|
|
84
|
-
created_at: new Date().toISOString()
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: 'prod_local_strategy',
|
|
88
|
-
price_id: 'price_local_strategy',
|
|
89
|
-
name: 'Strategy Tier',
|
|
90
|
-
description: 'Technical architecture and implementation roadmap',
|
|
91
|
-
price: 1000000,
|
|
92
|
-
currency: 'usd',
|
|
93
|
-
features: [
|
|
94
|
-
'Technical architecture design',
|
|
95
|
-
'Development roadmap',
|
|
96
|
-
'Tech stack decisions',
|
|
97
|
-
'Implementation timeline'
|
|
98
|
-
],
|
|
99
|
-
active: true,
|
|
100
|
-
created_at: new Date().toISOString()
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 'prod_local_build',
|
|
104
|
-
price_id: 'price_local_build',
|
|
105
|
-
name: 'Build Tier',
|
|
106
|
-
description: 'Full application development and deployment',
|
|
107
|
-
price: 2500000,
|
|
108
|
-
currency: 'usd',
|
|
109
|
-
features: [
|
|
110
|
-
'Full application development',
|
|
111
|
-
'Database & backend',
|
|
112
|
-
'Production deployment',
|
|
113
|
-
'Technical handoff'
|
|
114
|
-
],
|
|
115
|
-
active: true,
|
|
116
|
-
created_at: new Date().toISOString()
|
|
117
|
-
}
|
|
118
|
-
];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Product Management
|
|
122
|
-
|
|
123
|
-
saveProducts() {
|
|
124
|
-
fs.writeFileSync(this.productsFile, JSON.stringify(this.products, null, 2));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
listProducts(filter = {}) {
|
|
128
|
-
let filtered = [...this.products];
|
|
129
|
-
|
|
130
|
-
if (filter.active !== undefined) {
|
|
131
|
-
filtered = filtered.filter(p => p.active === filter.active);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return filtered;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
getProduct(id) {
|
|
138
|
-
return this.products.find(p => p.id === id || p.price_id === id);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
createProduct(data) {
|
|
142
|
-
const product = {
|
|
143
|
-
id: `prod_local_${uuidv4().substring(0, 8)}`,
|
|
144
|
-
price_id: `price_local_${uuidv4().substring(0, 8)}`,
|
|
145
|
-
name: data.name,
|
|
146
|
-
description: data.description,
|
|
147
|
-
price: data.price,
|
|
148
|
-
currency: data.currency || 'usd',
|
|
149
|
-
features: data.features || [],
|
|
150
|
-
active: true,
|
|
151
|
-
created_at: new Date().toISOString(),
|
|
152
|
-
updated_at: new Date().toISOString()
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
this.products.push(product);
|
|
156
|
-
this.saveProducts();
|
|
157
|
-
|
|
158
|
-
return product;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
updateProduct(id, updates) {
|
|
162
|
-
const index = this.products.findIndex(p => p.id === id || p.price_id === id);
|
|
163
|
-
if (index === -1) {
|
|
164
|
-
throw new Error('Product not found');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
this.products[index] = {
|
|
168
|
-
...this.products[index],
|
|
169
|
-
...updates,
|
|
170
|
-
updated_at: new Date().toISOString()
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
this.saveProducts();
|
|
174
|
-
return this.products[index];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
deleteProduct(id) {
|
|
178
|
-
const index = this.products.findIndex(p => p.id === id || p.price_id === id);
|
|
179
|
-
if (index === -1) {
|
|
180
|
-
throw new Error('Product not found');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Soft delete - just mark as inactive
|
|
184
|
-
this.products[index].active = false;
|
|
185
|
-
this.products[index].deleted_at = new Date().toISOString();
|
|
186
|
-
|
|
187
|
-
this.saveProducts();
|
|
188
|
-
return { success: true };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Order Management
|
|
192
|
-
|
|
193
|
-
saveOrders() {
|
|
194
|
-
fs.writeFileSync(this.ordersFile, JSON.stringify(this.orders, null, 2));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
listOrders(filter = {}) {
|
|
198
|
-
let filtered = [...this.orders];
|
|
199
|
-
|
|
200
|
-
if (filter.status) {
|
|
201
|
-
filtered = filtered.filter(o => o.status === filter.status);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (filter.customer_id) {
|
|
205
|
-
filtered = filtered.filter(o => o.customer_id === filter.customer_id);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Sort by created_at descending
|
|
209
|
-
filtered.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
210
|
-
|
|
211
|
-
return filtered;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
createOrder(data) {
|
|
215
|
-
const order = {
|
|
216
|
-
id: `order_local_${uuidv4().substring(0, 8)}`,
|
|
217
|
-
customer_id: data.customer_id || `cus_local_${uuidv4().substring(0, 8)}`,
|
|
218
|
-
customer_email: data.customer_email,
|
|
219
|
-
product_id: data.product_id,
|
|
220
|
-
price_id: data.price_id,
|
|
221
|
-
amount: data.amount,
|
|
222
|
-
currency: data.currency || 'usd',
|
|
223
|
-
status: 'pending',
|
|
224
|
-
payment_intent_id: `pi_local_${uuidv4().substring(0, 8)}`,
|
|
225
|
-
metadata: data.metadata || {},
|
|
226
|
-
created_at: new Date().toISOString(),
|
|
227
|
-
updated_at: new Date().toISOString()
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
this.orders.push(order);
|
|
231
|
-
this.saveOrders();
|
|
232
|
-
|
|
233
|
-
return order;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
updateOrderStatus(orderId, status) {
|
|
237
|
-
const order = this.orders.find(o => o.id === orderId);
|
|
238
|
-
if (!order) {
|
|
239
|
-
throw new Error('Order not found');
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
order.status = status;
|
|
243
|
-
order.updated_at = new Date().toISOString();
|
|
244
|
-
|
|
245
|
-
if (status === 'completed') {
|
|
246
|
-
order.completed_at = new Date().toISOString();
|
|
247
|
-
} else if (status === 'cancelled') {
|
|
248
|
-
order.cancelled_at = new Date().toISOString();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
this.saveOrders();
|
|
252
|
-
return order;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Customer Management
|
|
256
|
-
|
|
257
|
-
saveCustomers() {
|
|
258
|
-
fs.writeFileSync(this.customersFile, JSON.stringify(this.customers, null, 2));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
listCustomers() {
|
|
262
|
-
return this.customers;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
createCustomer(data) {
|
|
266
|
-
const customer = {
|
|
267
|
-
id: `cus_local_${uuidv4().substring(0, 8)}`,
|
|
268
|
-
email: data.email,
|
|
269
|
-
name: data.name,
|
|
270
|
-
metadata: data.metadata || {},
|
|
271
|
-
created_at: new Date().toISOString(),
|
|
272
|
-
updated_at: new Date().toISOString()
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
this.customers.push(customer);
|
|
276
|
-
this.saveCustomers();
|
|
277
|
-
|
|
278
|
-
return customer;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Analytics
|
|
282
|
-
|
|
283
|
-
getAnalytics() {
|
|
284
|
-
const now = new Date();
|
|
285
|
-
const thirtyDaysAgo = new Date(now - 30 * 24 * 60 * 60 * 1000);
|
|
286
|
-
|
|
287
|
-
const recentOrders = this.orders.filter(o =>
|
|
288
|
-
new Date(o.created_at) > thirtyDaysAgo
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
const completedOrders = recentOrders.filter(o => o.status === 'completed');
|
|
292
|
-
const totalRevenue = completedOrders.reduce((sum, o) => sum + o.amount, 0);
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
total_orders: this.orders.length,
|
|
296
|
-
recent_orders: recentOrders.length,
|
|
297
|
-
completed_orders: completedOrders.length,
|
|
298
|
-
total_revenue: totalRevenue,
|
|
299
|
-
total_customers: this.customers.length,
|
|
300
|
-
total_products: this.products.filter(p => p.active).length,
|
|
301
|
-
revenue_by_product: this.getRevenueByProduct(),
|
|
302
|
-
orders_by_status: this.getOrdersByStatus(),
|
|
303
|
-
recent_activity: this.getRecentActivity()
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
getRevenueByProduct() {
|
|
308
|
-
const revenue = {};
|
|
309
|
-
|
|
310
|
-
this.orders
|
|
311
|
-
.filter(o => o.status === 'completed')
|
|
312
|
-
.forEach(order => {
|
|
313
|
-
const product = this.getProduct(order.product_id);
|
|
314
|
-
if (product) {
|
|
315
|
-
if (!revenue[product.name]) {
|
|
316
|
-
revenue[product.name] = {
|
|
317
|
-
total: 0,
|
|
318
|
-
count: 0
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
revenue[product.name].total += order.amount;
|
|
322
|
-
revenue[product.name].count += 1;
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
return revenue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
getOrdersByStatus() {
|
|
330
|
-
const statuses = {};
|
|
331
|
-
|
|
332
|
-
this.orders.forEach(order => {
|
|
333
|
-
if (!statuses[order.status]) {
|
|
334
|
-
statuses[order.status] = 0;
|
|
335
|
-
}
|
|
336
|
-
statuses[order.status]++;
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
return statuses;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
getRecentActivity() {
|
|
343
|
-
const activities = [];
|
|
344
|
-
|
|
345
|
-
// Add recent orders
|
|
346
|
-
this.orders.slice(-5).forEach(order => {
|
|
347
|
-
activities.push({
|
|
348
|
-
type: 'order',
|
|
349
|
-
message: `New order #${order.id.substring(12)} - $${order.amount / 100}`,
|
|
350
|
-
timestamp: order.created_at
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Add recent customers
|
|
355
|
-
this.customers.slice(-3).forEach(customer => {
|
|
356
|
-
activities.push({
|
|
357
|
-
type: 'customer',
|
|
358
|
-
message: `New customer: ${customer.email}`,
|
|
359
|
-
timestamp: customer.created_at
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// Sort by timestamp
|
|
364
|
-
activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
365
|
-
|
|
366
|
-
return activities.slice(0, 10);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Export/Import for migration to production
|
|
370
|
-
|
|
371
|
-
exportData() {
|
|
372
|
-
return {
|
|
373
|
-
products: this.products,
|
|
374
|
-
orders: this.orders,
|
|
375
|
-
customers: this.customers,
|
|
376
|
-
exported_at: new Date().toISOString(),
|
|
377
|
-
version: '1.0.0'
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
importData(data) {
|
|
382
|
-
if (data.products) {
|
|
383
|
-
this.products = data.products;
|
|
384
|
-
this.saveProducts();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (data.orders) {
|
|
388
|
-
this.orders = data.orders;
|
|
389
|
-
this.saveOrders();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (data.customers) {
|
|
393
|
-
this.customers = data.customers;
|
|
394
|
-
this.saveCustomers();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return {
|
|
398
|
-
success: true,
|
|
399
|
-
imported: {
|
|
400
|
-
products: data.products?.length || 0,
|
|
401
|
-
orders: data.orders?.length || 0,
|
|
402
|
-
customers: data.customers?.length || 0
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
module.exports = LocalAdminManager;
|
package/src/stripe-local.js
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local Stripe Integration for SPAPS CLI
|
|
3
|
-
* Handles webhook forwarding and testing without Stripe CLI
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const { spawn } = require('child_process');
|
|
7
|
-
const chalk = require('chalk');
|
|
8
|
-
const ora = require('ora');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
|
|
12
|
-
class StripeLocalManager {
|
|
13
|
-
constructor(options = {}) {
|
|
14
|
-
this.port = options.port || 3300;
|
|
15
|
-
this.stripeCliProcess = null;
|
|
16
|
-
this.useBuiltInSimulator = options.simulator !== false;
|
|
17
|
-
this.webhookSecret = 'whsec_local_development_secret';
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Check if Stripe CLI is installed
|
|
22
|
-
*/
|
|
23
|
-
async checkStripeCLI() {
|
|
24
|
-
try {
|
|
25
|
-
const { execSync } = require('child_process');
|
|
26
|
-
execSync('stripe --version', { stdio: 'ignore' });
|
|
27
|
-
return true;
|
|
28
|
-
} catch (error) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Start Stripe webhook forwarding
|
|
35
|
-
*/
|
|
36
|
-
async startWebhookForwarding() {
|
|
37
|
-
const hasStripeCLI = await this.checkStripeCLI();
|
|
38
|
-
|
|
39
|
-
if (hasStripeCLI && !this.useBuiltInSimulator) {
|
|
40
|
-
return this.startStripeCLI();
|
|
41
|
-
} else {
|
|
42
|
-
return this.startBuiltInSimulator();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Start Stripe CLI webhook forwarding
|
|
48
|
-
*/
|
|
49
|
-
async startStripeCLI() {
|
|
50
|
-
console.log(chalk.blue('\nš” Starting Stripe CLI webhook forwarding...'));
|
|
51
|
-
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
this.stripeCliProcess = spawn('stripe', [
|
|
54
|
-
'listen',
|
|
55
|
-
'--forward-to',
|
|
56
|
-
`localhost:${this.port}/api/stripe/webhooks`,
|
|
57
|
-
'--print-json'
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
let webhookSecret = null;
|
|
61
|
-
|
|
62
|
-
this.stripeCliProcess.stdout.on('data', (data) => {
|
|
63
|
-
const output = data.toString();
|
|
64
|
-
|
|
65
|
-
// Parse webhook secret from output
|
|
66
|
-
if (!webhookSecret && output.includes('whsec_')) {
|
|
67
|
-
const match = output.match(/whsec_[a-zA-Z0-9]+/);
|
|
68
|
-
if (match) {
|
|
69
|
-
webhookSecret = match[0];
|
|
70
|
-
console.log(chalk.green(`ā
Stripe webhooks connected!`));
|
|
71
|
-
console.log(chalk.gray(` Secret: ${webhookSecret}`));
|
|
72
|
-
console.log(chalk.gray(` Forwarding to: http://localhost:${this.port}/api/stripe/webhooks`));
|
|
73
|
-
|
|
74
|
-
// Save webhook secret to env file
|
|
75
|
-
this.saveWebhookSecret(webhookSecret);
|
|
76
|
-
|
|
77
|
-
resolve({
|
|
78
|
-
type: 'stripe-cli',
|
|
79
|
-
secret: webhookSecret,
|
|
80
|
-
url: `http://localhost:${this.port}/api/stripe/webhooks`
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Log webhook events
|
|
86
|
-
try {
|
|
87
|
-
const json = JSON.parse(output);
|
|
88
|
-
if (json.type) {
|
|
89
|
-
console.log(chalk.blue(`ā” Webhook: ${json.type}`));
|
|
90
|
-
}
|
|
91
|
-
} catch (e) {
|
|
92
|
-
// Not JSON, ignore
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
this.stripeCliProcess.stderr.on('data', (data) => {
|
|
97
|
-
const error = data.toString();
|
|
98
|
-
if (error.includes('login')) {
|
|
99
|
-
console.log(chalk.yellow('\nā ļø Stripe CLI not logged in'));
|
|
100
|
-
console.log(chalk.cyan(' Run: stripe login'));
|
|
101
|
-
reject(new Error('Stripe CLI not authenticated'));
|
|
102
|
-
} else if (error.includes('Error')) {
|
|
103
|
-
console.error(chalk.red(`Stripe CLI error: ${error}`));
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
this.stripeCliProcess.on('close', (code) => {
|
|
108
|
-
if (code !== 0 && code !== null) {
|
|
109
|
-
reject(new Error(`Stripe CLI exited with code ${code}`));
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Start built-in webhook simulator
|
|
117
|
-
*/
|
|
118
|
-
async startBuiltInSimulator() {
|
|
119
|
-
console.log(chalk.blue('\nš Starting built-in webhook simulator...'));
|
|
120
|
-
console.log(chalk.gray(' (Stripe CLI not found or simulator mode enabled)'));
|
|
121
|
-
|
|
122
|
-
// The local server will handle webhook simulation
|
|
123
|
-
console.log(chalk.green(`ā
Webhook simulator ready!`));
|
|
124
|
-
console.log(chalk.gray(` Test UI: http://localhost:${this.port}/api/stripe/webhooks/test`));
|
|
125
|
-
console.log(chalk.gray(` Endpoint: http://localhost:${this.port}/api/stripe/webhooks`));
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
type: 'simulator',
|
|
129
|
-
secret: this.webhookSecret,
|
|
130
|
-
url: `http://localhost:${this.port}/api/stripe/webhooks`,
|
|
131
|
-
testUI: `http://localhost:${this.port}/api/stripe/webhooks/test`
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Save webhook secret to local env file
|
|
137
|
-
*/
|
|
138
|
-
saveWebhookSecret(secret) {
|
|
139
|
-
const envPath = path.join(process.cwd(), '.env.local');
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
let envContent = '';
|
|
143
|
-
if (fs.existsSync(envPath)) {
|
|
144
|
-
envContent = fs.readFileSync(envPath, 'utf8');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Update or add webhook secret
|
|
148
|
-
if (envContent.includes('STRIPE_WEBHOOK_SECRET=')) {
|
|
149
|
-
envContent = envContent.replace(
|
|
150
|
-
/STRIPE_WEBHOOK_SECRET=.*/,
|
|
151
|
-
`STRIPE_WEBHOOK_SECRET=${secret}`
|
|
152
|
-
);
|
|
153
|
-
} else {
|
|
154
|
-
envContent += `\n# Auto-generated by SPAPS\nSTRIPE_WEBHOOK_SECRET=${secret}\n`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
fs.writeFileSync(envPath, envContent);
|
|
158
|
-
console.log(chalk.gray(` Secret saved to .env.local`));
|
|
159
|
-
} catch (error) {
|
|
160
|
-
console.error(chalk.yellow(` Could not save webhook secret: ${error.message}`));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Stop webhook forwarding
|
|
166
|
-
*/
|
|
167
|
-
stop() {
|
|
168
|
-
if (this.stripeCliProcess) {
|
|
169
|
-
console.log(chalk.yellow('\nš Stopping Stripe webhook forwarding...'));
|
|
170
|
-
this.stripeCliProcess.kill();
|
|
171
|
-
this.stripeCliProcess = null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Create test products for local development
|
|
177
|
-
*/
|
|
178
|
-
async createTestProducts() {
|
|
179
|
-
console.log(chalk.blue('\nš¦ Creating test Stripe products...'));
|
|
180
|
-
|
|
181
|
-
const products = [
|
|
182
|
-
{
|
|
183
|
-
id: 'prod_local_validate',
|
|
184
|
-
name: 'Validate Tier',
|
|
185
|
-
description: 'Landing page with data capture',
|
|
186
|
-
price: 50000, // $500
|
|
187
|
-
price_id: 'price_local_validate'
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
id: 'prod_local_prototype',
|
|
191
|
-
name: 'Prototype Tier',
|
|
192
|
-
description: 'Clickable prototype with core flows',
|
|
193
|
-
price: 250000, // $2,500
|
|
194
|
-
price_id: 'price_local_prototype'
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
id: 'prod_local_strategy',
|
|
198
|
-
name: 'Strategy Tier',
|
|
199
|
-
description: 'Technical architecture and roadmap',
|
|
200
|
-
price: 1000000, // $10,000
|
|
201
|
-
price_id: 'price_local_strategy'
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
id: 'prod_local_build',
|
|
205
|
-
name: 'Build Tier',
|
|
206
|
-
description: 'Full application development',
|
|
207
|
-
price: 2500000, // $25,000
|
|
208
|
-
price_id: 'price_local_build'
|
|
209
|
-
}
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
// Store products in local config
|
|
213
|
-
const configPath = path.join(process.cwd(), '.spaps', 'stripe-products.json');
|
|
214
|
-
const configDir = path.dirname(configPath);
|
|
215
|
-
|
|
216
|
-
if (!fs.existsSync(configDir)) {
|
|
217
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
fs.writeFileSync(configPath, JSON.stringify(products, null, 2));
|
|
221
|
-
|
|
222
|
-
console.log(chalk.green('ā
Test products created:'));
|
|
223
|
-
products.forEach(p => {
|
|
224
|
-
console.log(chalk.gray(` - ${p.name}: $${p.price / 100}`));
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
return products;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Show webhook testing instructions
|
|
232
|
-
*/
|
|
233
|
-
showInstructions() {
|
|
234
|
-
console.log(chalk.yellow('\nš Webhook Testing Guide:'));
|
|
235
|
-
console.log();
|
|
236
|
-
|
|
237
|
-
if (this.useBuiltInSimulator) {
|
|
238
|
-
console.log('1. Open webhook tester UI:');
|
|
239
|
-
console.log(chalk.cyan(` http://localhost:${this.port}/api/stripe/webhooks/test`));
|
|
240
|
-
console.log();
|
|
241
|
-
console.log('2. Or trigger via code:');
|
|
242
|
-
console.log(chalk.gray(' ```javascript'));
|
|
243
|
-
console.log(chalk.gray(' // Your app code'));
|
|
244
|
-
console.log(chalk.gray(' const result = await spaps.createCheckoutSession(...);'));
|
|
245
|
-
console.log(chalk.gray(' // Webhook fires automatically after 1 second'));
|
|
246
|
-
console.log(chalk.gray(' ```'));
|
|
247
|
-
} else {
|
|
248
|
-
console.log('1. Trigger test events:');
|
|
249
|
-
console.log(chalk.cyan(' stripe trigger payment_intent.succeeded'));
|
|
250
|
-
console.log();
|
|
251
|
-
console.log('2. Or use the Stripe Dashboard:');
|
|
252
|
-
console.log(chalk.cyan(' https://dashboard.stripe.com/test/webhooks'));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
console.log();
|
|
256
|
-
console.log(chalk.blue('š” Tips:'));
|
|
257
|
-
console.log(' - Webhooks auto-retry on failure');
|
|
258
|
-
console.log(' - Check logs for webhook events');
|
|
259
|
-
console.log(' - Use webhook secret in your app');
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
module.exports = StripeLocalManager;
|