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,724 @@
1
+ /**
2
+ * Billing Module - Stripe integration for cycleCAD Pro/Enterprise
3
+ * Manages subscriptions, usage limits, feature gates, and trial periods
4
+ *
5
+ * Public API:
6
+ * window.cycleCAD.modules.billing.getCurrentTier() → {tier, features, limits, usage}
7
+ * window.cycleCAD.modules.billing.checkLimit(feature) → {allowed, current, limit, message}
8
+ * window.cycleCAD.modules.billing.upgrade() → redirect to Stripe Checkout
9
+ * window.cycleCAD.modules.billing.getUsage() → {projects, parts, storage, ...}
10
+ */
11
+
12
+ const BillingModule = {
13
+ id: 'billing',
14
+ version: '1.0.0',
15
+
16
+ // Stripe configuration (set via localStorage or env)
17
+ config: {
18
+ stripePublishableKey: localStorage.getItem('stripe_publishable_key') || 'pk_test_51234567890',
19
+ stripePriceIds: {
20
+ proMonthly: localStorage.getItem('stripe_price_pro_monthly') || 'price_1234_pro_monthly',
21
+ proYearly: localStorage.getItem('stripe_price_pro_yearly') || 'price_1234_pro_yearly',
22
+ enterpriseMonthly: localStorage.getItem('stripe_price_enterprise_monthly') || 'price_1234_ent_monthly',
23
+ enterpriseYearly: localStorage.getItem('stripe_price_enterprise_yearly') || 'price_1234_ent_yearly'
24
+ },
25
+ serverUrl: localStorage.getItem('billing_server_url') || 'http://localhost:3001',
26
+ trialDays: 14,
27
+ gracePeriodDays: 7
28
+ },
29
+
30
+ // Tier definitions with limits
31
+ tiers: {
32
+ free: {
33
+ id: 'free',
34
+ name: 'Free',
35
+ price: 0,
36
+ currency: 'EUR',
37
+ description: 'Perfect for getting started',
38
+ features: [
39
+ 'Up to 3 projects',
40
+ '100 parts per project',
41
+ '1 GB storage',
42
+ 'Basic 3D viewer',
43
+ 'STEP import (up to 30 MB)',
44
+ '20 AI requests per day',
45
+ 'Community support'
46
+ ],
47
+ limits: {
48
+ projects: 3,
49
+ partsPerProject: 100,
50
+ stepImportMB: 30,
51
+ collaborators: 0,
52
+ storageGB: 1,
53
+ aiRequestsPerDay: 20,
54
+ camOperations: false,
55
+ customMaterials: false,
56
+ apiAccess: false,
57
+ prioritySupport: false,
58
+ customBranding: false,
59
+ sso: false,
60
+ selfHosted: false
61
+ },
62
+ color: '#6B7280'
63
+ },
64
+
65
+ pro: {
66
+ id: 'pro',
67
+ name: 'Pro',
68
+ price: 4900,
69
+ priceYearly: 46800,
70
+ currency: 'EUR',
71
+ description: 'For professional designers',
72
+ features: [
73
+ 'Unlimited projects',
74
+ 'Unlimited parts',
75
+ '50 GB storage',
76
+ 'Full feature set',
77
+ 'STEP import (up to 500 MB)',
78
+ '500 AI requests per day',
79
+ 'CAM operations',
80
+ 'Custom materials',
81
+ 'API access',
82
+ 'Priority email support',
83
+ 'Monthly billing or yearly (save 20%)'
84
+ ],
85
+ limits: {
86
+ projects: Infinity,
87
+ partsPerProject: Infinity,
88
+ stepImportMB: 500,
89
+ collaborators: 10,
90
+ storageGB: 50,
91
+ aiRequestsPerDay: 500,
92
+ camOperations: true,
93
+ customMaterials: true,
94
+ apiAccess: true,
95
+ prioritySupport: true,
96
+ customBranding: false,
97
+ sso: false,
98
+ selfHosted: false
99
+ },
100
+ color: '#3B82F6'
101
+ },
102
+
103
+ enterprise: {
104
+ id: 'enterprise',
105
+ name: 'Enterprise',
106
+ price: 29900,
107
+ priceYearly: 286800,
108
+ currency: 'EUR',
109
+ description: 'For teams and manufacturers',
110
+ features: [
111
+ 'Everything in Pro',
112
+ 'Unlimited collaborators',
113
+ '500 GB storage',
114
+ 'Unlimited AI requests',
115
+ 'Unlimited STEP import',
116
+ 'CAM to real-time fab network',
117
+ 'Custom branding',
118
+ 'Single Sign-On (SSO)',
119
+ 'Self-hosted option',
120
+ '99.9% SLA',
121
+ 'Dedicated technical support',
122
+ 'Training and consulting included'
123
+ ],
124
+ limits: {
125
+ projects: Infinity,
126
+ partsPerProject: Infinity,
127
+ stepImportMB: Infinity,
128
+ collaborators: Infinity,
129
+ storageGB: 500,
130
+ aiRequestsPerDay: Infinity,
131
+ camOperations: true,
132
+ customMaterials: true,
133
+ apiAccess: true,
134
+ prioritySupport: true,
135
+ customBranding: true,
136
+ sso: true,
137
+ selfHosted: true,
138
+ sla: '99.9%'
139
+ },
140
+ color: '#8B5CF6'
141
+ }
142
+ },
143
+
144
+ // User state (cached in localStorage)
145
+ state: {
146
+ userId: null,
147
+ email: null,
148
+ tier: 'free',
149
+ status: 'active', // active, trialing, canceled, payment_failed
150
+ trialEndsAt: null,
151
+ currentPeriodEnd: null,
152
+ currentPeriodStart: null,
153
+ cancelAtPeriodEnd: false,
154
+ billingCycle: 'monthly', // monthly or yearly
155
+ stripeCustomerId: null,
156
+ usage: {
157
+ projects: 0,
158
+ partsInProject: {},
159
+ totalParts: 0,
160
+ storageGB: 0,
161
+ aiRequests: 0,
162
+ aiRequestsToday: 0,
163
+ lastAiRequestReset: Date.now(),
164
+ stepImportsThisMonth: 0,
165
+ stepImportBytesThisMonth: 0,
166
+ lastImportReset: Date.now()
167
+ },
168
+ lastSyncedAt: null,
169
+ offlineMode: false
170
+ },
171
+
172
+ /**
173
+ * Initialize billing module
174
+ * Load user state from localStorage or API
175
+ */
176
+ async init() {
177
+ console.log('[Billing] Initializing...');
178
+
179
+ // Load from localStorage
180
+ const saved = localStorage.getItem('billing_state');
181
+ if (saved) {
182
+ try {
183
+ this.state = { ...this.state, ...JSON.parse(saved) };
184
+ console.log('[Billing] Loaded cached state:', this.state.tier);
185
+ } catch (e) {
186
+ console.warn('[Billing] Failed to parse cached state:', e);
187
+ }
188
+ }
189
+
190
+ // Try to sync with server
191
+ try {
192
+ const response = await fetch(`${this.config.serverUrl}/billing/user`, {
193
+ method: 'GET',
194
+ credentials: 'include'
195
+ });
196
+
197
+ if (response.ok) {
198
+ const data = await response.json();
199
+ this.state = { ...this.state, ...data };
200
+ this.saveState();
201
+ console.log('[Billing] Synced with server');
202
+ this.state.offlineMode = false;
203
+ }
204
+ } catch (e) {
205
+ console.warn('[Billing] Server sync failed, using offline mode:', e.message);
206
+ this.state.offlineMode = true;
207
+ }
208
+
209
+ // Start daily AI request counter reset
210
+ this.startDailyReset();
211
+
212
+ // Load stripe.js
213
+ if (!window.Stripe) {
214
+ const script = document.createElement('script');
215
+ script.src = 'https://js.stripe.com/v3/';
216
+ script.async = true;
217
+ document.head.appendChild(script);
218
+ await new Promise(resolve => script.onload = resolve);
219
+ }
220
+
221
+ this.stripe = Stripe(this.config.stripePublishableKey);
222
+ console.log('[Billing] Module ready');
223
+ },
224
+
225
+ /**
226
+ * Get current tier configuration
227
+ */
228
+ getCurrentTier() {
229
+ const tier = this.tiers[this.state.tier] || this.tiers.free;
230
+ return {
231
+ tier: this.state.tier,
232
+ name: tier.name,
233
+ features: tier.features,
234
+ limits: tier.limits,
235
+ usage: this.state.usage,
236
+ status: this.state.status,
237
+ trialEndsAt: this.state.trialEndsAt,
238
+ currentPeriodEnd: this.state.currentPeriodEnd,
239
+ billingCycle: this.state.billingCycle,
240
+ daysUntilTrialExpires: this.state.trialEndsAt ?
241
+ Math.ceil((this.state.trialEndsAt - Date.now()) / (1000 * 60 * 60 * 24)) : null,
242
+ isTrialing: this.state.status === 'trialing',
243
+ isCanceled: this.state.status === 'canceled',
244
+ isPaymentFailed: this.state.status === 'payment_failed'
245
+ };
246
+ },
247
+
248
+ /**
249
+ * Check if a feature is allowed under current tier
250
+ * Returns {allowed, current, limit, message, upgradeRequired}
251
+ */
252
+ checkLimit(feature, count = 1) {
253
+ const tier = this.tiers[this.state.tier] || this.tiers.free;
254
+ const limits = tier.limits;
255
+ const usage = this.state.usage;
256
+
257
+ // Map feature names to limit keys and usage keys
258
+ const featureLimits = {
259
+ 'projects': { limit: limits.projects, usage: usage.projects, display: 'Projects' },
260
+ 'parts': { limit: limits.partsPerProject, usage: usage.totalParts, display: 'Parts' },
261
+ 'storage': { limit: limits.storageGB, usage: usage.storageGB, display: 'Storage (GB)' },
262
+ 'ai-requests': { limit: limits.aiRequestsPerDay, usage: usage.aiRequestsToday, display: 'AI requests today' },
263
+ 'step-import': { limit: limits.stepImportMB, usage: 0, display: 'STEP import size (MB)' },
264
+ 'collaborators': { limit: limits.collaborators, usage: 0, display: 'Collaborators' },
265
+ 'cam-operations': { limit: limits.camOperations ? 1 : 0, usage: 1, display: 'CAM operations' },
266
+ 'custom-materials': { limit: limits.customMaterials ? 1 : 0, usage: 1, display: 'Custom materials' },
267
+ 'api-access': { limit: limits.apiAccess ? 1 : 0, usage: 1, display: 'API access' }
268
+ };
269
+
270
+ const featureConfig = featureLimits[feature];
271
+ if (!featureConfig) {
272
+ return { allowed: true, message: 'Feature not tracked' };
273
+ }
274
+
275
+ const { limit, usage: currentUsage, display } = featureConfig;
276
+ const newTotal = currentUsage + count;
277
+ const allowed = limit === Infinity || newTotal <= limit;
278
+
279
+ return {
280
+ allowed,
281
+ current: currentUsage,
282
+ limit,
283
+ message: allowed ?
284
+ `${display}: ${currentUsage}/${limit === Infinity ? '∞' : limit}` :
285
+ `Upgrade to ${this.state.tier === 'free' ? 'Pro' : 'Enterprise'} to increase ${display} limit`,
286
+ upgradeRequired: !allowed,
287
+ percentUsed: limit === Infinity ? 0 : Math.round((currentUsage / limit) * 100)
288
+ };
289
+ },
290
+
291
+ /**
292
+ * Show upgrade prompt modal
293
+ */
294
+ showUpgradePrompt(feature, context = '') {
295
+ const message = this.checkLimit(feature).message;
296
+ const html = `
297
+ <div class="billing-upgrade-modal">
298
+ <div class="modal-backdrop"></div>
299
+ <div class="modal-content">
300
+ <h3>Upgrade Your Plan</h3>
301
+ <p>${message}</p>
302
+ ${context ? `<p class="context">${context}</p>` : ''}
303
+ <div class="tier-options">
304
+ <button class="tier-btn pro-btn" onclick="window.cycleCAD.modules.billing.startCheckout('pro', 'monthly')">
305
+ Upgrade to Pro<br><small>€49/month</small>
306
+ </button>
307
+ <button class="tier-btn enterprise-btn" onclick="window.cycleCAD.modules.billing.startCheckout('enterprise', 'monthly')">
308
+ Upgrade to Enterprise<br><small>€299/month</small>
309
+ </button>
310
+ </div>
311
+ <button class="cancel-btn" onclick="this.closest('.billing-upgrade-modal').remove()">Cancel</button>
312
+ </div>
313
+ </div>
314
+ `;
315
+
316
+ const container = document.createElement('div');
317
+ container.innerHTML = html;
318
+ document.body.appendChild(container);
319
+ },
320
+
321
+ /**
322
+ * Start Stripe Checkout for a tier
323
+ */
324
+ async startCheckout(tier, billingCycle = 'monthly') {
325
+ if (!['pro', 'enterprise'].includes(tier)) {
326
+ console.error('[Billing] Invalid tier:', tier);
327
+ return;
328
+ }
329
+
330
+ try {
331
+ const tierConfig = this.tiers[tier];
332
+ const priceId = billingCycle === 'yearly' ?
333
+ this.config.stripePriceIds[`${tier}Yearly`] :
334
+ this.config.stripePriceIds[`${tier}Monthly`];
335
+
336
+ console.log('[Billing] Starting checkout for', tier, billingCycle);
337
+
338
+ const response = await fetch(`${this.config.serverUrl}/billing/create-checkout`, {
339
+ method: 'POST',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ credentials: 'include',
342
+ body: JSON.stringify({
343
+ priceId,
344
+ tier,
345
+ billingCycle,
346
+ trialDays: this.state.tier === 'free' ? this.config.trialDays : 0
347
+ })
348
+ });
349
+
350
+ if (!response.ok) {
351
+ throw new Error(`HTTP ${response.status}`);
352
+ }
353
+
354
+ const { sessionId } = await response.json();
355
+ await this.stripe.redirectToCheckout({ sessionId });
356
+ } catch (e) {
357
+ console.error('[Billing] Checkout failed:', e);
358
+ alert('Failed to start checkout. Please try again.');
359
+ }
360
+ },
361
+
362
+ /**
363
+ * Open Stripe Customer Portal to manage subscription
364
+ */
365
+ async openCustomerPortal() {
366
+ try {
367
+ console.log('[Billing] Opening customer portal...');
368
+
369
+ const response = await fetch(`${this.config.serverUrl}/billing/create-portal`, {
370
+ method: 'POST',
371
+ credentials: 'include'
372
+ });
373
+
374
+ if (!response.ok) {
375
+ throw new Error(`HTTP ${response.status}`);
376
+ }
377
+
378
+ const { url } = await response.json();
379
+ window.open(url, '_blank');
380
+ } catch (e) {
381
+ console.error('[Billing] Failed to open portal:', e);
382
+ alert('Failed to open billing portal. Please try again.');
383
+ }
384
+ },
385
+
386
+ /**
387
+ * Track usage of a feature
388
+ */
389
+ trackUsage(feature, amount = 1) {
390
+ const now = Date.now();
391
+
392
+ // Reset daily AI counter if needed
393
+ if (now - this.state.usage.lastAiRequestReset > 24 * 60 * 60 * 1000) {
394
+ this.state.usage.aiRequestsToday = 0;
395
+ this.state.usage.lastAiRequestReset = now;
396
+ }
397
+
398
+ // Reset monthly STEP import if needed (simplified: monthly = 30 days)
399
+ if (now - this.state.usage.lastImportReset > 30 * 24 * 60 * 60 * 1000) {
400
+ this.state.usage.stepImportsThisMonth = 0;
401
+ this.state.usage.stepImportBytesThisMonth = 0;
402
+ this.state.usage.lastImportReset = now;
403
+ }
404
+
405
+ switch (feature) {
406
+ case 'ai-request':
407
+ this.state.usage.aiRequests++;
408
+ this.state.usage.aiRequestsToday++;
409
+ break;
410
+ case 'project-created':
411
+ this.state.usage.projects++;
412
+ break;
413
+ case 'part-added':
414
+ this.state.usage.totalParts++;
415
+ this.state.usage.partsInProject[this.getCurrentProjectId()] =
416
+ (this.state.usage.partsInProject[this.getCurrentProjectId()] || 0) + 1;
417
+ break;
418
+ case 'storage-added':
419
+ this.state.usage.storageGB += amount;
420
+ break;
421
+ case 'step-import':
422
+ this.state.usage.stepImportsThisMonth++;
423
+ this.state.usage.stepImportBytesThisMonth += amount;
424
+ break;
425
+ }
426
+
427
+ this.saveState();
428
+ this.dispatchUsageEvent(feature, amount);
429
+ },
430
+
431
+ /**
432
+ * Get current usage stats
433
+ */
434
+ getUsage() {
435
+ return { ...this.state.usage };
436
+ },
437
+
438
+ /**
439
+ * Check if user has a feature (returns boolean)
440
+ */
441
+ hasFeature(feature) {
442
+ const tier = this.tiers[this.state.tier] || this.tiers.free;
443
+ return tier.limits[feature] === true || tier.limits[feature] > 0;
444
+ },
445
+
446
+ /**
447
+ * Get remaining usage before hitting limit
448
+ */
449
+ getRemainingQuota(feature) {
450
+ const check = this.checkLimit(feature);
451
+ if (check.limit === Infinity) return Infinity;
452
+ return Math.max(0, check.limit - check.current);
453
+ },
454
+
455
+ /**
456
+ * Apply a promo code
457
+ */
458
+ async applyPromoCode(code) {
459
+ try {
460
+ const response = await fetch(`${this.config.serverUrl}/billing/apply-promo`, {
461
+ method: 'POST',
462
+ headers: { 'Content-Type': 'application/json' },
463
+ credentials: 'include',
464
+ body: JSON.stringify({ code })
465
+ });
466
+
467
+ const data = await response.json();
468
+ if (data.valid) {
469
+ console.log('[Billing] Promo code valid:', data.discount);
470
+ return { valid: true, discount: data.discount, message: data.message };
471
+ } else {
472
+ return { valid: false, message: data.message || 'Invalid promo code' };
473
+ }
474
+ } catch (e) {
475
+ console.error('[Billing] Promo validation failed:', e);
476
+ return { valid: false, message: 'Failed to validate promo code' };
477
+ }
478
+ },
479
+
480
+ /**
481
+ * Cancel subscription (redirects to portal)
482
+ */
483
+ async cancelSubscription() {
484
+ const confirmed = confirm(
485
+ 'Are you sure you want to cancel your subscription? ' +
486
+ 'You will lose access to premium features at the end of your billing cycle.'
487
+ );
488
+
489
+ if (confirmed) {
490
+ this.openCustomerPortal();
491
+ }
492
+ },
493
+
494
+ /**
495
+ * Change billing cycle (monthly ↔ yearly)
496
+ */
497
+ async changeBillingCycle(newCycle) {
498
+ if (!['monthly', 'yearly'].includes(newCycle)) {
499
+ console.error('[Billing] Invalid cycle:', newCycle);
500
+ return false;
501
+ }
502
+
503
+ try {
504
+ const response = await fetch(`${this.config.serverUrl}/billing/change-billing-cycle`, {
505
+ method: 'POST',
506
+ headers: { 'Content-Type': 'application/json' },
507
+ credentials: 'include',
508
+ body: JSON.stringify({ cycle: newCycle })
509
+ });
510
+
511
+ if (response.ok) {
512
+ this.state.billingCycle = newCycle;
513
+ this.saveState();
514
+ return true;
515
+ }
516
+ } catch (e) {
517
+ console.error('[Billing] Failed to change cycle:', e);
518
+ }
519
+ return false;
520
+ },
521
+
522
+ /**
523
+ * Get usage as percentage for progress indicators
524
+ */
525
+ getUsagePercentage(feature) {
526
+ const check = this.checkLimit(feature);
527
+ if (check.limit === Infinity) return 0;
528
+ return Math.round((check.current / check.limit) * 100);
529
+ },
530
+
531
+ /**
532
+ * Export usage data as CSV
533
+ */
534
+ exportUsageCSV() {
535
+ const lines = [
536
+ ['Metric', 'Current', 'Limit', 'Usage %'],
537
+ ['Projects', this.state.usage.projects, this.tiers[this.state.tier].limits.projects || '∞', this.getUsagePercentage('projects')],
538
+ ['Total Parts', this.state.usage.totalParts, this.tiers[this.state.tier].limits.partsPerProject || '∞', this.getUsagePercentage('parts')],
539
+ ['Storage (GB)', this.state.usage.storageGB.toFixed(2), this.tiers[this.state.tier].limits.storageGB, this.getUsagePercentage('storage')],
540
+ ['AI Requests (Today)', this.state.usage.aiRequestsToday, this.tiers[this.state.tier].limits.aiRequestsPerDay, this.getUsagePercentage('ai-requests')],
541
+ ['Total AI Requests', this.state.usage.aiRequests, '∞', 0],
542
+ ['STEP Imports (This Month)', this.state.usage.stepImportsThisMonth, '∞', 0],
543
+ ['STEP Import Data (MB)', (this.state.usage.stepImportBytesThisMonth / 1024 / 1024).toFixed(2), this.tiers[this.state.tier].limits.stepImportMB || '∞', this.getUsagePercentage('step-import')],
544
+ [''],
545
+ ['Generated at', new Date().toISOString()],
546
+ ['User Tier', this.state.tier],
547
+ ['Subscription Status', this.state.status]
548
+ ];
549
+
550
+ const csv = lines.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
551
+ const blob = new Blob([csv], { type: 'text/csv' });
552
+ const url = URL.createObjectURL(blob);
553
+ const a = document.createElement('a');
554
+ a.href = url;
555
+ a.download = `cyclecad-usage-${new Date().toISOString().split('T')[0]}.csv`;
556
+ a.click();
557
+ URL.revokeObjectURL(url);
558
+ },
559
+
560
+ /**
561
+ * Get list of past invoices
562
+ */
563
+ async getInvoices() {
564
+ try {
565
+ const response = await fetch(`${this.config.serverUrl}/billing/invoices`, {
566
+ method: 'GET',
567
+ credentials: 'include'
568
+ });
569
+
570
+ if (response.ok) {
571
+ return await response.json();
572
+ }
573
+ } catch (e) {
574
+ console.error('[Billing] Failed to fetch invoices:', e);
575
+ }
576
+ return [];
577
+ },
578
+
579
+ /**
580
+ * Internal: Save state to localStorage
581
+ */
582
+ saveState() {
583
+ localStorage.setItem('billing_state', JSON.stringify(this.state));
584
+ this.state.lastSyncedAt = Date.now();
585
+ },
586
+
587
+ /**
588
+ * Internal: Start daily AI request counter reset
589
+ */
590
+ startDailyReset() {
591
+ setInterval(() => {
592
+ const now = Date.now();
593
+ if (now - this.state.usage.lastAiRequestReset > 24 * 60 * 60 * 1000) {
594
+ this.state.usage.aiRequestsToday = 0;
595
+ this.state.usage.lastAiRequestReset = now;
596
+ this.saveState();
597
+ this.dispatchUsageEvent('daily-reset', 0);
598
+ }
599
+ }, 60000); // Check every minute
600
+ },
601
+
602
+ /**
603
+ * Internal: Get current project ID
604
+ */
605
+ getCurrentProjectId() {
606
+ return window.cycleCAD?.state?.currentProject || 'default';
607
+ },
608
+
609
+ /**
610
+ * Internal: Dispatch custom event
611
+ */
612
+ dispatchUsageEvent(feature, amount) {
613
+ window.dispatchEvent(new CustomEvent('billing-usage', {
614
+ detail: { feature, amount, usage: this.state.usage }
615
+ }));
616
+ },
617
+
618
+ /**
619
+ * Get UI component for pricing table
620
+ */
621
+ getUI() {
622
+ return {
623
+ id: 'billing-panel',
624
+ title: 'Billing & Pricing',
625
+ html: `
626
+ <div class="billing-panel">
627
+ <div class="current-plan">
628
+ <h4>Current Plan</h4>
629
+ <div class="plan-card ${this.state.tier}">
630
+ <div class="plan-name">${this.tiers[this.state.tier].name}</div>
631
+ <div class="plan-price">
632
+ €${this.state.tier === 'free' ? '0' :
633
+ this.state.billingCycle === 'yearly' ?
634
+ (this.tiers[this.state.tier].priceYearly / 100 / 12).toFixed(0) :
635
+ (this.tiers[this.state.tier].price / 100).toFixed(0)}/month
636
+ </div>
637
+ <div class="plan-status">${this.state.status}</div>
638
+ ${this.state.currentPeriodEnd ?
639
+ `<div class="next-billing">Next billing: ${new Date(this.state.currentPeriodEnd).toLocaleDateString()}</div>` : ''}
640
+ ${this.state.trialEndsAt ?
641
+ `<div class="trial-countdown">Trial expires in ${Math.ceil((this.state.trialEndsAt - Date.now()) / (1000 * 60 * 60 * 24))} days</div>` : ''}
642
+ </div>
643
+ </div>
644
+
645
+ <div class="usage-section">
646
+ <h4>Usage</h4>
647
+ ${this.getUsageBarHTML('Projects', this.getUsagePercentage('projects'),
648
+ `${this.state.usage.projects}/${this.tiers[this.state.tier].limits.projects}`)}
649
+ ${this.getUsageBarHTML('Storage', this.getUsagePercentage('storage'),
650
+ `${this.state.usage.storageGB.toFixed(1)} GB / ${this.tiers[this.state.tier].limits.storageGB} GB`)}
651
+ ${this.getUsageBarHTML('AI Requests (Today)', this.getUsagePercentage('ai-requests'),
652
+ `${this.state.usage.aiRequestsToday} / ${this.tiers[this.state.tier].limits.aiRequestsPerDay}`)}
653
+ </div>
654
+
655
+ <div class="billing-actions">
656
+ <button onclick="window.cycleCAD.modules.billing.openCustomerPortal()" class="btn btn-secondary">
657
+ Manage Subscription
658
+ </button>
659
+ <button onclick="window.cycleCAD.modules.billing.exportUsageCSV()" class="btn btn-secondary">
660
+ Export Usage
661
+ </button>
662
+ ${this.state.tier !== 'enterprise' ?
663
+ `<button onclick="window.cycleCAD.modules.billing.showUpgradePrompt('storage')" class="btn btn-primary">
664
+ Upgrade Plan
665
+ </button>` : ''}
666
+ </div>
667
+ </div>
668
+ `,
669
+ styles: `
670
+ .billing-panel {
671
+ padding: 16px;
672
+ display: flex;
673
+ flex-direction: column;
674
+ gap: 16px;
675
+ }
676
+ .current-plan h4, .usage-section h4 {
677
+ margin: 0 0 12px 0;
678
+ font-size: 14px;
679
+ font-weight: 600;
680
+ text-transform: uppercase;
681
+ color: #6B7280;
682
+ }
683
+ .plan-card {
684
+ border: 2px solid #E5E7EB;
685
+ border-radius: 8px;
686
+ padding: 16px;
687
+ background: #F9FAFB;
688
+ }
689
+ .plan-card.pro { border-color: #3B82F6; background: #EFF6FF; }
690
+ .plan-card.enterprise { border-color: #8B5CF6; background: #F5F3FF; }
691
+ .plan-name { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
692
+ .plan-price { font-size: 24px; font-weight: 700; color: #1F2937; margin-bottom: 8px; }
693
+ .plan-status { font-size: 12px; text-transform: uppercase; color: #6B7280; margin-bottom: 8px; }
694
+ .next-billing, .trial-countdown { font-size: 12px; color: #6B7280; }
695
+ .usage-section { display: flex; flex-direction: column; gap: 12px; }
696
+ .billing-actions { display: flex; gap: 8px; flex-wrap: wrap; }
697
+ `
698
+ };
699
+ },
700
+
701
+ /**
702
+ * Helper: Get HTML for usage progress bar
703
+ */
704
+ getUsageBarHTML(label, percentage, details) {
705
+ return `
706
+ <div class="usage-item">
707
+ <div class="usage-label">
708
+ <span>${label}</span>
709
+ <span class="usage-details">${details}</span>
710
+ </div>
711
+ <div class="usage-bar">
712
+ <div class="usage-fill" style="width: ${Math.min(percentage, 100)}%; background-color: ${
713
+ percentage > 90 ? '#EF4444' : percentage > 70 ? '#F59E0B' : '#10B981'
714
+ }"></div>
715
+ </div>
716
+ </div>
717
+ `;
718
+ }
719
+ };
720
+
721
+ // Export for use in cycleCAD
722
+ if (typeof window !== 'undefined') {
723
+ window.BillingModule = BillingModule;
724
+ }