cyclecad 3.0.0 → 3.1.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.
Files changed (66) hide show
  1. package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  7. package/DOCKER-FILES-REFERENCE.md +440 -0
  8. package/DOCKER-INFRASTRUCTURE.md +475 -0
  9. package/DOCKER-README.md +435 -0
  10. package/Dockerfile +33 -55
  11. package/PWA-FILES-CREATED.txt +350 -0
  12. package/QUICK-START-TESTING.md +126 -0
  13. package/STEP-IMPORT-QUICKSTART.md +347 -0
  14. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  15. package/app/css/mobile.css +1074 -0
  16. package/app/icons/generate-icons.js +203 -0
  17. package/app/js/billing-ui.js +990 -0
  18. package/app/js/brep-kernel.js +933 -981
  19. package/app/js/collab-client.js +750 -0
  20. package/app/js/mobile-nav.js +623 -0
  21. package/app/js/mobile-toolbar.js +476 -0
  22. package/app/js/modules/billing-module.js +724 -0
  23. package/app/js/modules/step-module-enhanced.js +938 -0
  24. package/app/js/offline-manager.js +705 -0
  25. package/app/js/responsive-init.js +360 -0
  26. package/app/js/touch-handler.js +429 -0
  27. package/app/manifest.json +211 -0
  28. package/app/offline.html +508 -0
  29. package/app/sw.js +571 -0
  30. package/app/tests/billing-tests.html +779 -0
  31. package/app/tests/brep-tests.html +980 -0
  32. package/app/tests/collab-tests.html +743 -0
  33. package/app/tests/mobile-tests.html +1299 -0
  34. package/app/tests/pwa-tests.html +1134 -0
  35. package/app/tests/step-tests.html +1042 -0
  36. package/app/tests/test-agent-v3.html +719 -0
  37. package/docker-compose.yml +225 -0
  38. package/docs/BILLING-HELP.json +260 -0
  39. package/docs/BILLING-README.md +639 -0
  40. package/docs/BILLING-TUTORIAL.md +736 -0
  41. package/docs/BREP-HELP.json +326 -0
  42. package/docs/BREP-TUTORIAL.md +802 -0
  43. package/docs/COLLABORATION-HELP.json +228 -0
  44. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  45. package/docs/DOCKER-HELP.json +224 -0
  46. package/docs/DOCKER-TUTORIAL.md +974 -0
  47. package/docs/MOBILE-HELP.json +243 -0
  48. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  49. package/docs/MOBILE-TUTORIAL.md +747 -0
  50. package/docs/PWA-HELP.json +228 -0
  51. package/docs/PWA-README.md +662 -0
  52. package/docs/PWA-TUTORIAL.md +757 -0
  53. package/docs/STEP-HELP.json +481 -0
  54. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  55. package/docs/TESTING-GUIDE.md +528 -0
  56. package/docs/TESTING-HELP.json +182 -0
  57. package/fusion-vs-cyclecad.html +1771 -0
  58. package/nginx.conf +237 -0
  59. package/package.json +1 -1
  60. package/server/Dockerfile.converter +51 -0
  61. package/server/Dockerfile.signaling +28 -0
  62. package/server/billing-server.js +487 -0
  63. package/server/converter-enhanced.py +528 -0
  64. package/server/requirements-converter.txt +29 -0
  65. package/server/signaling-server.js +801 -0
  66. 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;