cyclecad 3.0.0 → 3.2.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/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
- package/BILLING-INDEX.md +293 -0
- package/BILLING-INTEGRATION-GUIDE.md +414 -0
- package/COLLABORATION-INDEX.md +440 -0
- package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
- package/DOCKER-BUILD-MANIFEST.txt +483 -0
- package/DOCKER-FILES-REFERENCE.md +440 -0
- package/DOCKER-INFRASTRUCTURE.md +475 -0
- package/DOCKER-README.md +435 -0
- package/Dockerfile +33 -55
- package/PWA-FILES-CREATED.txt +350 -0
- package/QUICK-START-TESTING.md +126 -0
- package/STEP-IMPORT-QUICKSTART.md +347 -0
- package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
- package/app/css/mobile.css +1074 -0
- package/app/icons/generate-icons.js +203 -0
- package/app/index.html +93 -0
- package/app/js/billing-ui.js +990 -0
- package/app/js/brep-kernel.js +933 -981
- package/app/js/collab-client.js +750 -0
- package/app/js/mobile-nav.js +623 -0
- package/app/js/mobile-toolbar.js +476 -0
- package/app/js/modules/billing-module.js +724 -0
- package/app/js/modules/step-module-enhanced.js +938 -0
- package/app/js/offline-manager.js +705 -0
- package/app/js/responsive-init.js +360 -0
- package/app/js/touch-handler.js +429 -0
- package/app/manifest.json +211 -0
- package/app/offline.html +508 -0
- package/app/sw.js +571 -0
- package/app/tests/billing-tests.html +779 -0
- package/app/tests/brep-tests.html +980 -0
- package/app/tests/collab-tests.html +743 -0
- package/app/tests/mobile-tests.html +1299 -0
- package/app/tests/pwa-tests.html +1134 -0
- package/app/tests/step-tests.html +1042 -0
- package/app/tests/test-agent-v3.html +719 -0
- package/docker-compose.yml +225 -0
- package/docs/BILLING-HELP.json +260 -0
- package/docs/BILLING-README.md +639 -0
- package/docs/BILLING-TUTORIAL.md +736 -0
- package/docs/BREP-HELP.json +326 -0
- package/docs/BREP-TUTORIAL.md +802 -0
- package/docs/COLLABORATION-HELP.json +228 -0
- package/docs/COLLABORATION-TUTORIAL.md +818 -0
- package/docs/DOCKER-HELP.json +224 -0
- package/docs/DOCKER-TUTORIAL.md +974 -0
- package/docs/MOBILE-HELP.json +243 -0
- package/docs/MOBILE-RESPONSIVE-README.md +378 -0
- package/docs/MOBILE-TUTORIAL.md +747 -0
- package/docs/PWA-HELP.json +228 -0
- package/docs/PWA-README.md +662 -0
- package/docs/PWA-TUTORIAL.md +757 -0
- package/docs/STEP-HELP.json +481 -0
- package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
- package/docs/TESTING-GUIDE.md +528 -0
- package/docs/TESTING-HELP.json +182 -0
- package/fusion-vs-cyclecad.html +1771 -0
- package/nginx.conf +237 -0
- package/package.json +1 -1
- package/server/Dockerfile.converter +51 -0
- package/server/Dockerfile.signaling +28 -0
- package/server/billing-server.js +487 -0
- package/server/converter-enhanced.py +528 -0
- package/server/requirements-converter.txt +29 -0
- package/server/signaling-server.js +801 -0
- package/tests/docker-tests.sh +389 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Server - Stripe integration for cycleCAD
|
|
3
|
+
* Express.js server for handling Stripe Checkout, webhooks, and subscription management
|
|
4
|
+
*
|
|
5
|
+
* Endpoints:
|
|
6
|
+
* POST /billing/create-checkout - Create Stripe Checkout session
|
|
7
|
+
* POST /billing/create-portal - Create Customer Portal session
|
|
8
|
+
* POST /billing/webhook - Handle Stripe webhooks
|
|
9
|
+
* GET /billing/user - Get current user subscription
|
|
10
|
+
* GET /billing/usage/:userId - Get usage stats
|
|
11
|
+
* POST /billing/apply-promo - Validate promo code
|
|
12
|
+
* GET /billing/invoices - List invoices for user
|
|
13
|
+
* POST /billing/change-billing-cycle - Switch monthly/yearly
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const express = require('express');
|
|
17
|
+
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY || 'sk_test_1234567890');
|
|
18
|
+
const router = express.Router();
|
|
19
|
+
|
|
20
|
+
// Middleware
|
|
21
|
+
const authMiddleware = (req, res, next) => {
|
|
22
|
+
// In production, verify JWT token from session/cookie
|
|
23
|
+
req.userId = req.session?.userId || req.user?.id || 'test-user-' + Date.now();
|
|
24
|
+
next();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
router.use(authMiddleware);
|
|
28
|
+
|
|
29
|
+
// Store for user subscriptions (in production, use database)
|
|
30
|
+
const userSubscriptions = new Map();
|
|
31
|
+
const userUsage = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* POST /billing/create-checkout
|
|
35
|
+
* Create a Stripe Checkout session
|
|
36
|
+
*/
|
|
37
|
+
router.post('/create-checkout', express.json(), async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { priceId, tier, billingCycle, trialDays } = req.body;
|
|
40
|
+
const userId = req.userId;
|
|
41
|
+
|
|
42
|
+
console.log(`[Billing] Creating checkout for user ${userId}, tier: ${tier}, cycle: ${billingCycle}`);
|
|
43
|
+
|
|
44
|
+
// Get or create Stripe customer
|
|
45
|
+
let customerId = userSubscriptions.get(userId)?.stripeCustomerId;
|
|
46
|
+
|
|
47
|
+
if (!customerId) {
|
|
48
|
+
const customer = await stripe.customers.create({
|
|
49
|
+
metadata: { userId, tier }
|
|
50
|
+
});
|
|
51
|
+
customerId = customer.id;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create checkout session
|
|
55
|
+
const sessionParams = {
|
|
56
|
+
customer: customerId,
|
|
57
|
+
payment_method_types: ['card'],
|
|
58
|
+
mode: 'subscription',
|
|
59
|
+
line_items: [
|
|
60
|
+
{
|
|
61
|
+
price: priceId,
|
|
62
|
+
quantity: 1
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
success_url: `${process.env.APP_URL || 'http://localhost:3000'}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
66
|
+
cancel_url: `${process.env.APP_URL || 'http://localhost:3000'}/billing/canceled`,
|
|
67
|
+
subscription_data: {
|
|
68
|
+
metadata: { userId, tier, billingCycle }
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Add trial period for free users upgrading
|
|
73
|
+
if (trialDays > 0) {
|
|
74
|
+
sessionParams.subscription_data.trial_period_days = trialDays;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const session = await stripe.checkout.sessions.create(sessionParams);
|
|
78
|
+
|
|
79
|
+
console.log(`[Billing] Checkout session created: ${session.id}`);
|
|
80
|
+
|
|
81
|
+
res.json({ sessionId: session.id });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('[Billing] Checkout error:', error);
|
|
84
|
+
res.status(500).json({ error: error.message });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* POST /billing/create-portal
|
|
90
|
+
* Create a Stripe Customer Portal session
|
|
91
|
+
*/
|
|
92
|
+
router.post('/create-portal', express.json(), async (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const userId = req.userId;
|
|
95
|
+
const subscription = userSubscriptions.get(userId);
|
|
96
|
+
|
|
97
|
+
if (!subscription || !subscription.stripeCustomerId) {
|
|
98
|
+
return res.status(400).json({ error: 'No active subscription' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
102
|
+
customer: subscription.stripeCustomerId,
|
|
103
|
+
return_url: process.env.APP_URL || 'http://localhost:3000'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(`[Billing] Portal session created: ${session.id}`);
|
|
107
|
+
|
|
108
|
+
res.json({ url: session.url });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('[Billing] Portal error:', error);
|
|
111
|
+
res.status(500).json({ error: error.message });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* POST /billing/webhook
|
|
117
|
+
* Handle Stripe webhook events
|
|
118
|
+
* Must verify webhook signature before processing
|
|
119
|
+
*/
|
|
120
|
+
router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
121
|
+
const sig = req.headers['stripe-signature'];
|
|
122
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_test_1234567890';
|
|
123
|
+
|
|
124
|
+
let event;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('[Billing] Webhook signature verification failed:', err.message);
|
|
130
|
+
return res.status(400).send(`Webhook Error: ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`[Billing] Webhook received: ${event.type}`);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
switch (event.type) {
|
|
137
|
+
case 'customer.subscription.created':
|
|
138
|
+
case 'customer.subscription.updated':
|
|
139
|
+
await handleSubscriptionChange(event.data.object);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case 'customer.subscription.deleted':
|
|
143
|
+
await handleSubscriptionCanceled(event.data.object);
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'invoice.payment_succeeded':
|
|
147
|
+
await handlePaymentSucceeded(event.data.object);
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case 'invoice.payment_failed':
|
|
151
|
+
await handlePaymentFailed(event.data.object);
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
default:
|
|
155
|
+
console.log(`[Billing] Unhandled event type: ${event.type}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
res.json({ received: true });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('[Billing] Webhook processing error:', error);
|
|
161
|
+
res.status(500).json({ error: error.message });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* GET /billing/user
|
|
167
|
+
* Get current user's subscription and usage
|
|
168
|
+
*/
|
|
169
|
+
router.get('/user', async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const userId = req.userId;
|
|
172
|
+
const subscription = userSubscriptions.get(userId) || createDefaultSubscription(userId);
|
|
173
|
+
|
|
174
|
+
res.json({
|
|
175
|
+
userId,
|
|
176
|
+
tier: subscription.tier,
|
|
177
|
+
status: subscription.status,
|
|
178
|
+
stripeCustomerId: subscription.stripeCustomerId,
|
|
179
|
+
currentPeriodStart: subscription.currentPeriodStart,
|
|
180
|
+
currentPeriodEnd: subscription.currentPeriodEnd,
|
|
181
|
+
trialEndsAt: subscription.trialEndsAt,
|
|
182
|
+
billingCycle: subscription.billingCycle,
|
|
183
|
+
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
|
|
184
|
+
usage: userUsage.get(userId) || {}
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('[Billing] Get user error:', error);
|
|
188
|
+
res.status(500).json({ error: error.message });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* GET /billing/usage/:userId
|
|
194
|
+
* Get usage stats for a user
|
|
195
|
+
*/
|
|
196
|
+
router.get('/usage/:userId', async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const userId = req.params.userId;
|
|
199
|
+
|
|
200
|
+
// Verify user is requesting their own data
|
|
201
|
+
if (userId !== req.userId && !req.user?.isAdmin) {
|
|
202
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const usage = userUsage.get(userId) || {
|
|
206
|
+
projects: 0,
|
|
207
|
+
totalParts: 0,
|
|
208
|
+
storageGB: 0,
|
|
209
|
+
aiRequests: 0,
|
|
210
|
+
aiRequestsToday: 0,
|
|
211
|
+
stepImportsThisMonth: 0,
|
|
212
|
+
stepImportBytesThisMonth: 0
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
res.json(usage);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('[Billing] Get usage error:', error);
|
|
218
|
+
res.status(500).json({ error: error.message });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* POST /billing/track-usage
|
|
224
|
+
* Track usage (called by client when features are used)
|
|
225
|
+
*/
|
|
226
|
+
router.post('/track-usage', express.json(), async (req, res) => {
|
|
227
|
+
try {
|
|
228
|
+
const userId = req.userId;
|
|
229
|
+
const { feature, amount = 1 } = req.body;
|
|
230
|
+
|
|
231
|
+
let usage = userUsage.get(userId) || {};
|
|
232
|
+
|
|
233
|
+
switch (feature) {
|
|
234
|
+
case 'ai-request':
|
|
235
|
+
usage.aiRequests = (usage.aiRequests || 0) + 1;
|
|
236
|
+
usage.aiRequestsToday = (usage.aiRequestsToday || 0) + 1;
|
|
237
|
+
break;
|
|
238
|
+
case 'project-created':
|
|
239
|
+
usage.projects = (usage.projects || 0) + 1;
|
|
240
|
+
break;
|
|
241
|
+
case 'part-added':
|
|
242
|
+
usage.totalParts = (usage.totalParts || 0) + 1;
|
|
243
|
+
break;
|
|
244
|
+
case 'storage-added':
|
|
245
|
+
usage.storageGB = (usage.storageGB || 0) + amount;
|
|
246
|
+
break;
|
|
247
|
+
case 'step-import':
|
|
248
|
+
usage.stepImportsThisMonth = (usage.stepImportsThisMonth || 0) + 1;
|
|
249
|
+
usage.stepImportBytesThisMonth = (usage.stepImportBytesThisMonth || 0) + amount;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
userUsage.set(userId, usage);
|
|
254
|
+
res.json({ success: true, usage });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('[Billing] Track usage error:', error);
|
|
257
|
+
res.status(500).json({ error: error.message });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* POST /billing/apply-promo
|
|
263
|
+
* Validate and apply promo code
|
|
264
|
+
*/
|
|
265
|
+
router.post('/apply-promo', express.json(), async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const { code } = req.body;
|
|
268
|
+
|
|
269
|
+
// Validate promo code with Stripe
|
|
270
|
+
const promoCodes = await stripe.promotionCodes.list({ code });
|
|
271
|
+
|
|
272
|
+
if (promoCodes.data.length === 0) {
|
|
273
|
+
return res.json({
|
|
274
|
+
valid: false,
|
|
275
|
+
message: 'Promo code not found'
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const promoCode = promoCodes.data[0];
|
|
280
|
+
|
|
281
|
+
if (!promoCode.active) {
|
|
282
|
+
return res.json({
|
|
283
|
+
valid: false,
|
|
284
|
+
message: 'Promo code is no longer active'
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check if code has max redemptions
|
|
289
|
+
if (promoCode.max_redemptions && promoCode.times_redeemed >= promoCode.max_redemptions) {
|
|
290
|
+
return res.json({
|
|
291
|
+
valid: false,
|
|
292
|
+
message: 'Promo code redemption limit reached'
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const coupon = promoCode.coupon;
|
|
297
|
+
const discount = coupon.amount_off ?
|
|
298
|
+
`€${coupon.amount_off / 100}` :
|
|
299
|
+
`${coupon.percent_off}%`;
|
|
300
|
+
|
|
301
|
+
res.json({
|
|
302
|
+
valid: true,
|
|
303
|
+
discount,
|
|
304
|
+
message: `Promo code applied: ${discount} off`,
|
|
305
|
+
couponId: coupon.id
|
|
306
|
+
});
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('[Billing] Promo validation error:', error);
|
|
309
|
+
res.status(500).json({ error: error.message });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* GET /billing/invoices
|
|
315
|
+
* Get list of invoices for current user
|
|
316
|
+
*/
|
|
317
|
+
router.get('/invoices', async (req, res) => {
|
|
318
|
+
try {
|
|
319
|
+
const userId = req.userId;
|
|
320
|
+
const subscription = userSubscriptions.get(userId);
|
|
321
|
+
|
|
322
|
+
if (!subscription || !subscription.stripeCustomerId) {
|
|
323
|
+
return res.json([]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const invoices = await stripe.invoices.list({
|
|
327
|
+
customer: subscription.stripeCustomerId,
|
|
328
|
+
limit: 20
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const formattedInvoices = invoices.data.map(invoice => ({
|
|
332
|
+
id: invoice.id,
|
|
333
|
+
number: invoice.number,
|
|
334
|
+
amount: invoice.total / 100,
|
|
335
|
+
currency: invoice.currency.toUpperCase(),
|
|
336
|
+
date: new Date(invoice.created * 1000).toISOString(),
|
|
337
|
+
status: invoice.status,
|
|
338
|
+
pdfUrl: invoice.invoice_pdf,
|
|
339
|
+
downloadUrl: invoice.invoice_pdf
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
res.json(formattedInvoices);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('[Billing] Get invoices error:', error);
|
|
345
|
+
res.status(500).json({ error: error.message });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* POST /billing/change-billing-cycle
|
|
351
|
+
* Switch between monthly and yearly billing
|
|
352
|
+
*/
|
|
353
|
+
router.post('/change-billing-cycle', express.json(), async (req, res) => {
|
|
354
|
+
try {
|
|
355
|
+
const userId = req.userId;
|
|
356
|
+
const { cycle } = req.body;
|
|
357
|
+
const subscription = userSubscriptions.get(userId);
|
|
358
|
+
|
|
359
|
+
if (!subscription || !subscription.stripeCustomriptionId) {
|
|
360
|
+
return res.status(400).json({ error: 'No active subscription' });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!['monthly', 'yearly'].includes(cycle)) {
|
|
364
|
+
return res.status(400).json({ error: 'Invalid billing cycle' });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Update subscription (in production, would update Stripe subscription)
|
|
368
|
+
subscription.billingCycle = cycle;
|
|
369
|
+
userSubscriptions.set(userId, subscription);
|
|
370
|
+
|
|
371
|
+
res.json({ success: true, billingCycle: cycle });
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error('[Billing] Change cycle error:', error);
|
|
374
|
+
res.status(500).json({ error: error.message });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// WEBHOOK HANDLERS
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Handle subscription created or updated
|
|
384
|
+
*/
|
|
385
|
+
async function handleSubscriptionChange(subscription) {
|
|
386
|
+
const userId = subscription.metadata?.userId;
|
|
387
|
+
if (!userId) return;
|
|
388
|
+
|
|
389
|
+
console.log(`[Billing] Subscription updated for user ${userId}`);
|
|
390
|
+
|
|
391
|
+
const tierMap = {
|
|
392
|
+
'price_pro_monthly': 'pro',
|
|
393
|
+
'price_pro_yearly': 'pro',
|
|
394
|
+
'price_enterprise_monthly': 'enterprise',
|
|
395
|
+
'price_enterprise_yearly': 'enterprise'
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
399
|
+
const tier = tierMap[priceId] || 'free';
|
|
400
|
+
const billingCycle = subscription.items.data[0]?.price.recurring?.interval === 'year' ?
|
|
401
|
+
'yearly' : 'monthly';
|
|
402
|
+
|
|
403
|
+
const updated = {
|
|
404
|
+
userId,
|
|
405
|
+
tier,
|
|
406
|
+
status: subscription.status,
|
|
407
|
+
stripeCustomerId: subscription.customer,
|
|
408
|
+
subscriptionId: subscription.id,
|
|
409
|
+
currentPeriodStart: subscription.current_period_start * 1000,
|
|
410
|
+
currentPeriodEnd: subscription.current_period_end * 1000,
|
|
411
|
+
trialEndsAt: subscription.trial_end ? subscription.trial_end * 1000 : null,
|
|
412
|
+
billingCycle,
|
|
413
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end || false
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
userSubscriptions.set(userId, updated);
|
|
417
|
+
console.log(`[Billing] Updated subscription:`, updated);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Handle subscription canceled
|
|
422
|
+
*/
|
|
423
|
+
async function handleSubscriptionCanceled(subscription) {
|
|
424
|
+
const userId = subscription.metadata?.userId;
|
|
425
|
+
if (!userId) return;
|
|
426
|
+
|
|
427
|
+
console.log(`[Billing] Subscription canceled for user ${userId}`);
|
|
428
|
+
|
|
429
|
+
const subscription_data = userSubscriptions.get(userId);
|
|
430
|
+
if (subscription_data) {
|
|
431
|
+
subscription_data.status = 'canceled';
|
|
432
|
+
subscription_data.tier = 'free';
|
|
433
|
+
userSubscriptions.set(userId, subscription_data);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Handle successful payment
|
|
439
|
+
*/
|
|
440
|
+
async function handlePaymentSucceeded(invoice) {
|
|
441
|
+
const userId = invoice.subscription_details?.metadata?.userId;
|
|
442
|
+
if (!userId) return;
|
|
443
|
+
|
|
444
|
+
console.log(`[Billing] Payment succeeded for user ${userId}`);
|
|
445
|
+
|
|
446
|
+
const subscription = userSubscriptions.get(userId);
|
|
447
|
+
if (subscription) {
|
|
448
|
+
subscription.status = 'active';
|
|
449
|
+
userSubscriptions.set(userId, subscription);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Handle failed payment
|
|
455
|
+
*/
|
|
456
|
+
async function handlePaymentFailed(invoice) {
|
|
457
|
+
const userId = invoice.subscription_details?.metadata?.userId;
|
|
458
|
+
if (!userId) return;
|
|
459
|
+
|
|
460
|
+
console.log(`[Billing] Payment failed for user ${userId}`);
|
|
461
|
+
|
|
462
|
+
const subscription = userSubscriptions.get(userId);
|
|
463
|
+
if (subscription) {
|
|
464
|
+
subscription.status = 'payment_failed';
|
|
465
|
+
subscription.gracePeriodEndsAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 day grace
|
|
466
|
+
userSubscriptions.set(userId, subscription);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Helper: Create default free subscription
|
|
472
|
+
*/
|
|
473
|
+
function createDefaultSubscription(userId) {
|
|
474
|
+
return {
|
|
475
|
+
userId,
|
|
476
|
+
tier: 'free',
|
|
477
|
+
status: 'active',
|
|
478
|
+
stripeCustomerId: null,
|
|
479
|
+
currentPeriodStart: Date.now(),
|
|
480
|
+
currentPeriodEnd: null,
|
|
481
|
+
trialEndsAt: null,
|
|
482
|
+
billingCycle: 'monthly',
|
|
483
|
+
cancelAtPeriodEnd: false
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
module.exports = router;
|