fraim-framework 2.0.179 → 2.0.180

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 (59) hide show
  1. package/dist/src/ai-hub/desktop-main.js +2 -2
  2. package/dist/src/api/admin/payments.js +33 -0
  3. package/dist/src/api/admin/sales-leads.js +21 -0
  4. package/dist/src/api/payment/create-session.js +338 -0
  5. package/dist/src/api/payment/dashboard-link.js +149 -0
  6. package/dist/src/api/payment/session-details.js +31 -0
  7. package/dist/src/api/payment/webhook.js +587 -0
  8. package/dist/src/api/personas/me.js +29 -0
  9. package/dist/src/api/pricing/get-config.js +25 -0
  10. package/dist/src/api/sales/contact.js +44 -0
  11. package/dist/src/cli/distribution/marketplace-bundles.js +5 -1
  12. package/dist/src/db/payment-repository.js +61 -0
  13. package/dist/src/fraim/config-loader.js +11 -0
  14. package/dist/src/fraim/db-service.js +2387 -0
  15. package/dist/src/fraim/issues.js +152 -0
  16. package/dist/src/fraim/template-processor.js +184 -0
  17. package/dist/src/fraim/utils/request-utils.js +23 -0
  18. package/dist/src/middleware/auth.js +266 -0
  19. package/dist/src/middleware/cors-config.js +111 -0
  20. package/dist/src/middleware/logger.js +116 -0
  21. package/dist/src/middleware/rate-limit.js +110 -0
  22. package/dist/src/middleware/reject-query-api-key.js +45 -0
  23. package/dist/src/middleware/security-headers.js +41 -0
  24. package/dist/src/middleware/telemetry.js +134 -0
  25. package/dist/src/models/payment.js +2 -0
  26. package/dist/src/routes/analytics.js +1447 -0
  27. package/dist/src/routes/app-routes.js +32 -0
  28. package/dist/src/routes/auth-routes.js +505 -0
  29. package/dist/src/routes/oauth-routes.js +325 -0
  30. package/dist/src/routes/payment-routes.js +186 -0
  31. package/dist/src/routes/persona-catalog-routes.js +84 -0
  32. package/dist/src/services/admin-service.js +229 -0
  33. package/dist/src/services/audit-log-persistence.js +60 -0
  34. package/dist/src/services/audit-log.js +69 -0
  35. package/dist/src/services/cookie-service.js +129 -0
  36. package/dist/src/services/dashboard-access.js +27 -0
  37. package/dist/src/services/demo-seed-service.js +139 -0
  38. package/dist/src/services/email-code.js +23 -0
  39. package/dist/src/services/email-service-clean.js +782 -0
  40. package/dist/src/services/email-service.js +951 -0
  41. package/dist/src/services/installer-service.js +131 -0
  42. package/dist/src/services/mcp-oauth-store.js +33 -0
  43. package/dist/src/services/mcp-service.js +823 -0
  44. package/dist/src/services/oauth-helpers.js +127 -0
  45. package/dist/src/services/org-service.js +89 -0
  46. package/dist/src/services/persona-entitlement-service.js +288 -0
  47. package/dist/src/services/provider-service.js +215 -0
  48. package/dist/src/services/registry-service.js +628 -0
  49. package/dist/src/services/session-service.js +86 -0
  50. package/dist/src/services/trial-reminder-service.js +120 -0
  51. package/dist/src/services/usage-analytics-service.js +419 -0
  52. package/dist/src/services/workspace-identity.js +21 -0
  53. package/dist/src/types/analytics.js +2 -0
  54. package/dist/src/utils/payment-calculator.js +52 -0
  55. package/extensions/office-word/favicon.ico +0 -0
  56. package/extensions/office-word/icon-64.png +0 -0
  57. package/extensions/office-word/manifest.xml +33 -0
  58. package/extensions/office-word/taskpane.html +242 -0
  59. package/package.json +12 -2
@@ -0,0 +1,587 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDashboardAccessUrl = void 0;
4
+ exports.handleStripeWebhook = handleStripeWebhook;
5
+ exports.resolveSubscriptionNextBillingDate = resolveSubscriptionNextBillingDate;
6
+ exports.isInitialSubscriptionInvoice = isInitialSubscriptionInvoice;
7
+ exports.getInvoiceAmountPaidCents = getInvoiceAmountPaidCents;
8
+ exports.extractSubscriptionPeriodEndDate = extractSubscriptionPeriodEndDate;
9
+ exports.extractInvoicePeriodEndDate = extractInvoicePeriodEndDate;
10
+ exports.resolveSubscriptionNextBillingDateWithInvoiceFallback = resolveSubscriptionNextBillingDateWithInvoiceFallback;
11
+ exports.buildCurrentPeriodEndUpdate = buildCurrentPeriodEndUpdate;
12
+ exports.resolveInvoicePaidNextBillingDate = resolveInvoicePaidNextBillingDate;
13
+ const stripe_1 = require("../../config/stripe");
14
+ const feature_flags_1 = require("../../config/feature-flags");
15
+ const email_service_1 = require("../../services/email-service");
16
+ const dashboard_access_1 = require("../../services/dashboard-access");
17
+ Object.defineProperty(exports, "createDashboardAccessUrl", { enumerable: true, get: function () { return dashboard_access_1.createDashboardAccessUrl; } });
18
+ const persona_entitlement_service_1 = require("../../services/persona-entitlement-service");
19
+ async function handleStripeWebhook(req, res, paymentRepo, dbService) {
20
+ const sig = req.headers['stripe-signature'];
21
+ if (!sig) {
22
+ return res.status(400).json({ error: 'Missing stripe-signature header' });
23
+ }
24
+ let event;
25
+ try {
26
+ // When using express.raw(), req.body is a Buffer
27
+ // When using express.json(), req.body is an object
28
+ const rawBody = Buffer.isBuffer(req.body)
29
+ ? req.body
30
+ : (typeof req.body === 'string' ? req.body : JSON.stringify(req.body));
31
+ // Verify webhook signature
32
+ event = stripe_1.stripe.webhooks.constructEvent(rawBody, sig, stripe_1.STRIPE_CONFIG.webhookSecret);
33
+ }
34
+ catch (err) {
35
+ console.error('❌ Webhook signature verification failed:', err.message);
36
+ return res.status(400).json({ error: `Webhook Error: ${err.message}` });
37
+ }
38
+ console.log(`✅ Webhook received: ${event.type} (id: ${event.id})`);
39
+ // Check for idempotency if dbService available
40
+ if (dbService) {
41
+ try {
42
+ const alreadyProcessed = await dbService.isWebhookEventProcessed(event.id);
43
+ if (alreadyProcessed) {
44
+ console.log(`ℹ️ Webhook ${event.id} already processed, skipping`);
45
+ return res.json({ received: true, skipped: true });
46
+ }
47
+ // Record webhook event
48
+ await dbService.recordWebhookEvent({
49
+ eventId: event.id,
50
+ type: event.type,
51
+ processed: false,
52
+ processedAt: null,
53
+ receivedAt: new Date(),
54
+ retryCount: 0,
55
+ lastError: null
56
+ });
57
+ }
58
+ catch (error) {
59
+ if (error.message === 'Webhook event already processed') {
60
+ console.log(`ℹ️ Webhook ${event.id} already processed (race condition), skipping`);
61
+ return res.json({ received: true, skipped: true });
62
+ }
63
+ console.error('❌ Error recording webhook event:', error);
64
+ // Continue processing even if recording fails
65
+ }
66
+ }
67
+ try {
68
+ switch (event.type) {
69
+ // Subscription lifecycle events
70
+ case 'customer.subscription.created':
71
+ case 'customer.subscription.updated': {
72
+ const subscription = event.data.object;
73
+ if (dbService) {
74
+ await handleSubscriptionCreatedOrUpdated(subscription, dbService);
75
+ }
76
+ break;
77
+ }
78
+ case 'customer.subscription.deleted': {
79
+ const subscription = event.data.object;
80
+ if (dbService) {
81
+ await handleSubscriptionDeleted(subscription, dbService);
82
+ }
83
+ break;
84
+ }
85
+ case 'invoice.paid': {
86
+ const invoice = event.data.object;
87
+ if (dbService) {
88
+ await handleInvoicePaid(invoice, dbService);
89
+ }
90
+ break;
91
+ }
92
+ case 'invoice.payment_failed': {
93
+ const invoice = event.data.object;
94
+ if (dbService) {
95
+ await handleInvoicePaymentFailed(invoice, dbService);
96
+ }
97
+ break;
98
+ }
99
+ // Legacy payment events (for one-time payments)
100
+ case 'checkout.session.completed': {
101
+ const session = event.data.object;
102
+ await handleCheckoutSessionCompleted(session, paymentRepo, dbService);
103
+ break;
104
+ }
105
+ case 'payment_intent.succeeded': {
106
+ const paymentIntent = event.data.object;
107
+ await handlePaymentSuccess(paymentIntent, paymentRepo);
108
+ break;
109
+ }
110
+ case 'payment_intent.payment_failed': {
111
+ const paymentIntent = event.data.object;
112
+ await handlePaymentFailure(paymentIntent, paymentRepo);
113
+ break;
114
+ }
115
+ default:
116
+ console.log(`ℹ️ Unhandled event type: ${event.type}`);
117
+ }
118
+ // Mark event as processed
119
+ if (dbService) {
120
+ await dbService.markWebhookEventProcessed(event.id);
121
+ }
122
+ res.json({ received: true });
123
+ }
124
+ catch (error) {
125
+ console.error('❌ Error processing webhook:', error);
126
+ // Mark event as failed
127
+ if (dbService) {
128
+ await dbService.markWebhookEventFailed(event.id, error.message);
129
+ }
130
+ res.status(500).json({ error: 'Webhook processing failed', details: error.message });
131
+ }
132
+ }
133
+ // ========== Subscription Lifecycle Handlers ==========
134
+ async function resolveSubscriptionNextBillingDate(subscription, subscriptionRetriever = (subscriptionId) => stripe_1.stripe.subscriptions.retrieve(subscriptionId)) {
135
+ const directPeriodEnd = extractSubscriptionPeriodEndDate(subscription);
136
+ if (directPeriodEnd) {
137
+ return directPeriodEnd;
138
+ }
139
+ if (!subscription.id) {
140
+ return null;
141
+ }
142
+ try {
143
+ const fetchedSubscription = await subscriptionRetriever(subscription.id);
144
+ const fetchedPeriodEnd = extractSubscriptionPeriodEndDate(fetchedSubscription);
145
+ if (fetchedPeriodEnd) {
146
+ return fetchedPeriodEnd;
147
+ }
148
+ }
149
+ catch (error) {
150
+ console.warn(`⚠️ Failed to retrieve subscription ${subscription.id} for period end: ${error?.message || error}`);
151
+ }
152
+ return null;
153
+ }
154
+ function isInitialSubscriptionInvoice(invoice) {
155
+ return invoice.billing_reason === 'subscription_create';
156
+ }
157
+ function getInvoiceAmountPaidCents(invoice) {
158
+ return typeof invoice.amount_paid === 'number' ? Math.max(0, invoice.amount_paid) : null;
159
+ }
160
+ function extractSubscriptionPeriodEndDate(subscription) {
161
+ const rootPeriodEnd = subscription.current_period_end;
162
+ if (typeof rootPeriodEnd === 'number' && rootPeriodEnd > 0) {
163
+ return new Date(rootPeriodEnd * 1000);
164
+ }
165
+ const itemEnds = (subscription.items?.data || [])
166
+ .map((item) => item?.current_period_end)
167
+ .filter((value) => typeof value === 'number' && value > 0);
168
+ if (itemEnds.length > 0) {
169
+ // Use the earliest upcoming period boundary across items.
170
+ return new Date(Math.min(...itemEnds) * 1000);
171
+ }
172
+ return null;
173
+ }
174
+ function extractInvoicePeriodEndDate(invoice) {
175
+ const rootPeriodEnd = invoice.period_end;
176
+ if (typeof rootPeriodEnd === 'number' && rootPeriodEnd > 0) {
177
+ return new Date(rootPeriodEnd * 1000);
178
+ }
179
+ const linePeriodEnds = (invoice.lines?.data || [])
180
+ .map((line) => line?.period?.end)
181
+ .filter((value) => typeof value === 'number' && value > 0);
182
+ if (linePeriodEnds.length > 0) {
183
+ // Use the furthest period end across recurring/proration lines.
184
+ return new Date(Math.max(...linePeriodEnds) * 1000);
185
+ }
186
+ return null;
187
+ }
188
+ async function resolveSubscriptionNextBillingDateWithInvoiceFallback(subscription, subscriptionRetriever = (subscriptionId) => stripe_1.stripe.subscriptions.retrieve(subscriptionId), invoiceRetriever = (invoiceId) => stripe_1.stripe.invoices.retrieve(invoiceId)) {
189
+ const subscriptionDate = await resolveSubscriptionNextBillingDate(subscription, subscriptionRetriever);
190
+ if (subscriptionDate)
191
+ return subscriptionDate;
192
+ const latestInvoiceField = subscription.latest_invoice;
193
+ if (latestInvoiceField && typeof latestInvoiceField === 'object') {
194
+ const invoiceDate = extractInvoicePeriodEndDate(latestInvoiceField);
195
+ if (invoiceDate)
196
+ return invoiceDate;
197
+ }
198
+ if (typeof latestInvoiceField === 'string' && latestInvoiceField) {
199
+ try {
200
+ const latestInvoice = await invoiceRetriever(latestInvoiceField);
201
+ const invoiceDate = extractInvoicePeriodEndDate(latestInvoice);
202
+ if (invoiceDate)
203
+ return invoiceDate;
204
+ }
205
+ catch (error) {
206
+ console.warn(`⚠️ Failed to retrieve latest invoice ${latestInvoiceField} for subscription ${subscription.id}: ${error?.message || error}`);
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ function buildCurrentPeriodEndUpdate(nextBillingDate) {
212
+ return nextBillingDate ? { currentPeriodEnd: nextBillingDate } : {};
213
+ }
214
+ async function resolveInvoicePaidNextBillingDate(invoice, subscription, subscriptionDateResolver = resolveSubscriptionNextBillingDate) {
215
+ const subscriptionDate = subscription ? await subscriptionDateResolver(subscription) : null;
216
+ return subscriptionDate || extractInvoicePeriodEndDate(invoice);
217
+ }
218
+ function extractFounderDiscountFlagFromSubscription(subscription) {
219
+ return subscription.metadata?.founderDiscount === 'true';
220
+ }
221
+ async function handleSubscriptionCreatedOrUpdated(subscription, dbService) {
222
+ console.log(`✅ Subscription ${subscription.status}: ${subscription.id}`);
223
+ if (subscription.metadata?.plan === 'persona-hire') {
224
+ if ((0, feature_flags_1.isPersonaEntitlementsEnabled)()) {
225
+ const customerId = typeof subscription.customer === 'string'
226
+ ? subscription.customer
227
+ : subscription.customer.id;
228
+ const customer = await stripe_1.stripe.customers.retrieve(customerId);
229
+ const email = !('deleted' in customer) ? customer.email : null;
230
+ if (email && subscription.metadata.personaKey && (subscription.metadata.hireMode === 'job' || subscription.metadata.hireMode === 'fulltime')) {
231
+ const nextBillingDate = await resolveSubscriptionNextBillingDateWithInvoiceFallback(subscription);
232
+ await (0, persona_entitlement_service_1.syncPersonaEntitlementPurchase)(dbService, {
233
+ userId: email,
234
+ stripeCustomerId: customerId,
235
+ stripeSubscriptionId: subscription.id,
236
+ stripeCheckoutSessionId: null,
237
+ personaKey: subscription.metadata.personaKey,
238
+ hireMode: subscription.metadata.hireMode,
239
+ purchaseSource: 'stripe-webhook',
240
+ status: subscription.status === 'active' ? 'active' : 'suspended',
241
+ expiresAt: nextBillingDate,
242
+ metadata: subscription.metadata
243
+ });
244
+ }
245
+ }
246
+ console.log(`ℹ️ Skipping API-key provisioning for persona hire subscription: ${subscription.id}`);
247
+ return;
248
+ }
249
+ const customerId = typeof subscription.customer === 'string'
250
+ ? subscription.customer
251
+ : subscription.customer.id;
252
+ // Get or create API key for this customer
253
+ let apiKey = await dbService.getApiKeyByStripeCustomerId(customerId);
254
+ if (!apiKey) {
255
+ // Check if subscription has customer email in metadata
256
+ const customer = await stripe_1.stripe.customers.retrieve(customerId);
257
+ const email = customer.email;
258
+ if (!email) {
259
+ console.error(`❌ No email found for customer ${customerId}`);
260
+ return;
261
+ }
262
+ // Check if user already has an API key (trial user upgrading)
263
+ apiKey = await dbService.getApiKeyByUserId(email);
264
+ const nextBillingDate = await resolveSubscriptionNextBillingDateWithInvoiceFallback(subscription);
265
+ if (apiKey) {
266
+ // Upgrade trial key to paid subscription
267
+ await dbService.updateApiKey(apiKey.key, {
268
+ tier: 'paid-subscription',
269
+ status: subscription.status === 'active' ? 'active' : 'suspended',
270
+ stripeCustomerId: customerId,
271
+ stripeSubscriptionId: subscription.id,
272
+ ...buildCurrentPeriodEndUpdate(nextBillingDate),
273
+ expiresAt: null, // Subscriptions don't have fixed expiration
274
+ cancelAt: subscription.cancel_at ? new Date(subscription.cancel_at * 1000) : null
275
+ });
276
+ console.log(`✅ Upgraded trial key to paid subscription: ${apiKey.key}`);
277
+ }
278
+ else {
279
+ // Create new API key for subscription (shouldn't happen in normal flow)
280
+ const newKey = dbService.generateApiKey(email, 'default');
281
+ await dbService.createApiKey({
282
+ key: newKey,
283
+ userId: email,
284
+ orgId: 'default',
285
+ tier: 'paid-subscription',
286
+ status: subscription.status === 'active' ? 'active' : 'suspended',
287
+ expiresAt: null,
288
+ stripeCustomerId: customerId,
289
+ stripeSubscriptionId: subscription.id,
290
+ currentPeriodEnd: nextBillingDate,
291
+ cancelAt: subscription.cancel_at ? new Date(subscription.cancel_at * 1000) : null,
292
+ suspendedAt: null,
293
+ suspensionReason: null,
294
+ lastUsedAt: null,
295
+ apiCallCount: 0,
296
+ personaSystemActive: (0, feature_flags_1.isPersonaEntitlementsEnabled)() || undefined
297
+ });
298
+ console.log(`✅ Created new API key for subscription: ${newKey}`);
299
+ apiKey = { key: newKey, userId: email };
300
+ }
301
+ }
302
+ else {
303
+ // Update existing subscription key
304
+ const nextBillingDate = await resolveSubscriptionNextBillingDateWithInvoiceFallback(subscription);
305
+ await dbService.updateApiKey(apiKey.key, {
306
+ tier: 'paid-subscription',
307
+ status: subscription.status === 'active' ? 'active' : 'suspended',
308
+ stripeCustomerId: customerId,
309
+ stripeSubscriptionId: subscription.id,
310
+ ...buildCurrentPeriodEndUpdate(nextBillingDate),
311
+ expiresAt: null,
312
+ cancelAt: subscription.cancel_at ? new Date(subscription.cancel_at * 1000) : null,
313
+ suspendedAt: subscription.status !== 'active' ? new Date() : null,
314
+ suspensionReason: subscription.status !== 'active' ? subscription.status : null
315
+ });
316
+ }
317
+ // Intentionally no activation email here.
318
+ // Billing-confirmed messaging is sent from invoice.paid for Stripe source-of-truth amounts.
319
+ }
320
+ async function handleSubscriptionDeleted(subscription, dbService) {
321
+ if (subscription.metadata?.plan === 'persona-hire' && (0, feature_flags_1.isPersonaEntitlementsEnabled)()) {
322
+ const customerId = typeof subscription.customer === 'string'
323
+ ? subscription.customer
324
+ : subscription.customer.id;
325
+ const customer = await stripe_1.stripe.customers.retrieve(customerId);
326
+ const email = !('deleted' in customer) ? customer.email : null;
327
+ if (email && subscription.metadata.personaKey && subscription.metadata.hireMode === 'fulltime') {
328
+ await (0, persona_entitlement_service_1.syncPersonaEntitlementPurchase)(dbService, {
329
+ userId: email,
330
+ stripeCustomerId: customerId,
331
+ stripeSubscriptionId: subscription.id,
332
+ stripeCheckoutSessionId: null,
333
+ personaKey: subscription.metadata.personaKey,
334
+ hireMode: 'fulltime',
335
+ purchaseSource: 'stripe-webhook',
336
+ status: 'expired',
337
+ expiresAt: new Date(),
338
+ metadata: subscription.metadata
339
+ });
340
+ }
341
+ }
342
+ console.log(`❌ Subscription cancelled: ${subscription.id}`);
343
+ if (subscription.metadata?.plan === 'persona-hire') {
344
+ console.log(`ℹ️ Skipping persona hire cancellation handling for subscription: ${subscription.id}`);
345
+ return;
346
+ }
347
+ const apiKey = await dbService.getApiKeyByStripeSubscriptionId(subscription.id);
348
+ if (!apiKey) {
349
+ console.warn(`⚠️ No API key found for subscription: ${subscription.id}`);
350
+ return;
351
+ }
352
+ // Mark key as expired
353
+ await dbService.updateApiKey(apiKey.key, {
354
+ status: 'expired',
355
+ expiresAt: new Date(), // Expired now
356
+ cancelAt: null
357
+ });
358
+ // Send cancellation email
359
+ const emailService = new email_service_1.EmailService();
360
+ await emailService.sendSubscriptionCancelled(apiKey.userId, new Date());
361
+ console.log(`✅ API key expired due to subscription cancellation: ${apiKey.key}`);
362
+ }
363
+ async function handleInvoicePaid(invoice, dbService) {
364
+ console.log(`✅ Invoice paid: ${invoice.id}`);
365
+ const subscriptionField = invoice.subscription;
366
+ if (!subscriptionField) {
367
+ console.log(`ℹ️ Invoice is not for a subscription, skipping`);
368
+ return;
369
+ }
370
+ const subscriptionId = typeof subscriptionField === 'string'
371
+ ? subscriptionField
372
+ : subscriptionField.id;
373
+ const customerId = typeof invoice.customer === 'string'
374
+ ? invoice.customer
375
+ : invoice.customer?.id;
376
+ let apiKey = await dbService.getApiKeyByStripeSubscriptionId(subscriptionId);
377
+ if (!apiKey && customerId) {
378
+ apiKey = await dbService.getApiKeyByStripeCustomerId(customerId);
379
+ if (apiKey) {
380
+ await dbService.updateApiKey(apiKey.key, { stripeSubscriptionId: subscriptionId });
381
+ }
382
+ }
383
+ if (!apiKey && (0, feature_flags_1.isPersonaEntitlementsEnabled)() && invoice.customer_email) {
384
+ try {
385
+ const subscription = await stripe_1.stripe.subscriptions.retrieve(subscriptionId);
386
+ if (subscription.metadata?.plan === 'persona-hire') {
387
+ apiKey = { userId: invoice.customer_email };
388
+ }
389
+ }
390
+ catch (error) {
391
+ console.warn(`⚠️ Failed persona entitlement prefetch for invoice ${invoice.id}: ${error?.message || error}`);
392
+ }
393
+ }
394
+ if (!apiKey) {
395
+ console.warn(`⚠️ No API key found for subscription: ${subscriptionId}`);
396
+ return;
397
+ }
398
+ const isInitialSubscriptionCharge = isInitialSubscriptionInvoice(invoice);
399
+ const amountPaidCents = getInvoiceAmountPaidCents(invoice);
400
+ let subscriptionForInvoice = null;
401
+ try {
402
+ subscriptionForInvoice = await stripe_1.stripe.subscriptions.retrieve(subscriptionId);
403
+ }
404
+ catch (error) {
405
+ console.warn(`⚠️ Failed to retrieve subscription ${subscriptionId} during invoice.paid: ${error?.message || error}`);
406
+ }
407
+ if (subscriptionForInvoice?.metadata?.plan === 'persona-hire') {
408
+ if ((0, feature_flags_1.isPersonaEntitlementsEnabled)() && subscriptionForInvoice.metadata.personaKey && subscriptionForInvoice.metadata.hireMode === 'fulltime') {
409
+ const email = apiKey?.userId || invoice.customer_email;
410
+ if (email) {
411
+ await (0, persona_entitlement_service_1.syncPersonaEntitlementPurchase)(dbService, {
412
+ userId: email,
413
+ stripeCustomerId: customerId || null,
414
+ stripeSubscriptionId: subscriptionId,
415
+ stripeCheckoutSessionId: null,
416
+ personaKey: subscriptionForInvoice.metadata.personaKey,
417
+ hireMode: 'fulltime',
418
+ purchaseSource: 'stripe-webhook',
419
+ status: 'active',
420
+ expiresAt: await resolveInvoicePaidNextBillingDate(invoice, subscriptionForInvoice),
421
+ metadata: {
422
+ ...subscriptionForInvoice.metadata,
423
+ latestInvoiceId: invoice.id
424
+ }
425
+ });
426
+ }
427
+ }
428
+ console.log(`ℹ️ Skipping API-key activation emails for persona hire subscription: ${subscriptionId}`);
429
+ return;
430
+ }
431
+ const nextBillingDate = await resolveInvoicePaidNextBillingDate(invoice, subscriptionForInvoice);
432
+ const currentPeriodPatch = buildCurrentPeriodEndUpdate(nextBillingDate);
433
+ // Restore key if it was suspended due to payment failure
434
+ if (apiKey.status === 'suspended') {
435
+ await dbService.updateApiKey(apiKey.key, {
436
+ status: 'active',
437
+ ...currentPeriodPatch,
438
+ suspendedAt: null,
439
+ suspensionReason: null
440
+ });
441
+ console.log(`✅ API key restored after payment: ${apiKey.key}`);
442
+ // Send restoration email
443
+ const emailService = new email_service_1.EmailService();
444
+ const restorationBillingDate = nextBillingDate || new Date();
445
+ await emailService.sendPaymentRestored(apiKey.userId, restorationBillingDate);
446
+ return;
447
+ }
448
+ if (isInitialSubscriptionCharge) {
449
+ // First paid subscription charge: send activation using Stripe invoice + subscription data
450
+ const emailService = new email_service_1.EmailService();
451
+ const subscription = subscriptionForInvoice || await stripe_1.stripe.subscriptions.retrieve(subscriptionId);
452
+ const activationBillingDate = nextBillingDate || await resolveSubscriptionNextBillingDate(subscription);
453
+ const billingCycle = subscription.items.data[0]?.price?.recurring?.interval === 'year' ? 'annual' : 'monthly';
454
+ const plan = subscription.items.data[0]?.price?.nickname ||
455
+ (subscription.metadata?.plan === 'managed' ? 'FRAIM Managed' : 'FRAIM Self-Serve');
456
+ const founderDiscount = extractFounderDiscountFlagFromSubscription(subscription);
457
+ if (activationBillingDate) {
458
+ await dbService.updateApiKey(apiKey.key, buildCurrentPeriodEndUpdate(activationBillingDate));
459
+ }
460
+ if (amountPaidCents === null || !activationBillingDate) {
461
+ console.warn(`⚠️ Skipping subscription activation email due to missing Stripe invoice/subscription data for ${subscriptionId}`);
462
+ return;
463
+ }
464
+ const dashboardUrl = await (0, dashboard_access_1.createDashboardAccessUrl)(dbService, apiKey.userId, apiKey.key);
465
+ await emailService.sendSubscriptionActivated(apiKey.userId, plan, amountPaidCents, billingCycle, activationBillingDate, dashboardUrl, founderDiscount);
466
+ return;
467
+ }
468
+ // Regular renewal
469
+ {
470
+ console.log(`✅ Subscription renewed: ${apiKey.key}`);
471
+ if (nextBillingDate) {
472
+ await dbService.updateApiKey(apiKey.key, buildCurrentPeriodEndUpdate(nextBillingDate));
473
+ }
474
+ // Send renewal receipt
475
+ const emailService = new email_service_1.EmailService();
476
+ const amount = (invoice.amount_paid || 0) / 100;
477
+ const renewalBillingDate = nextBillingDate || new Date();
478
+ await emailService.sendRenewalReceipt(apiKey.userId, amount, renewalBillingDate, invoice.hosted_invoice_url || undefined);
479
+ }
480
+ }
481
+ async function handleInvoicePaymentFailed(invoice, dbService) {
482
+ console.log(`❌ Invoice payment failed: ${invoice.id}`);
483
+ const subscriptionField = invoice.subscription;
484
+ if (!subscriptionField) {
485
+ console.log(`ℹ️ Invoice is not for a subscription, skipping`);
486
+ return;
487
+ }
488
+ const subscriptionId = typeof subscriptionField === 'string'
489
+ ? subscriptionField
490
+ : subscriptionField.id;
491
+ try {
492
+ const subscription = await stripe_1.stripe.subscriptions.retrieve(subscriptionId);
493
+ if (subscription.metadata?.plan === 'persona-hire') {
494
+ if ((0, feature_flags_1.isPersonaEntitlementsEnabled)()) {
495
+ const customerId = typeof subscription.customer === 'string'
496
+ ? subscription.customer
497
+ : subscription.customer.id;
498
+ const customer = await stripe_1.stripe.customers.retrieve(customerId);
499
+ const email = !('deleted' in customer) ? customer.email : null;
500
+ if (email && subscription.metadata.personaKey && subscription.metadata.hireMode === 'fulltime') {
501
+ await (0, persona_entitlement_service_1.syncPersonaEntitlementPurchase)(dbService, {
502
+ userId: email,
503
+ stripeCustomerId: customerId,
504
+ stripeSubscriptionId: subscriptionId,
505
+ stripeCheckoutSessionId: null,
506
+ personaKey: subscription.metadata.personaKey,
507
+ hireMode: 'fulltime',
508
+ purchaseSource: 'stripe-webhook',
509
+ status: 'suspended',
510
+ metadata: {
511
+ ...subscription.metadata,
512
+ latestFailedInvoiceId: invoice.id
513
+ }
514
+ });
515
+ }
516
+ }
517
+ console.log(`ℹ️ Skipping persona hire payment-failure handling for subscription: ${subscriptionId}`);
518
+ return;
519
+ }
520
+ }
521
+ catch (error) {
522
+ console.warn(`⚠️ Failed to retrieve subscription ${subscriptionId} during invoice.payment_failed: ${error?.message || error}`);
523
+ }
524
+ const apiKey = await dbService.getApiKeyByStripeSubscriptionId(subscriptionId);
525
+ if (!apiKey) {
526
+ console.warn(`⚠️ No API key found for subscription: ${subscriptionId}`);
527
+ return;
528
+ }
529
+ // Suspend API key
530
+ await dbService.updateApiKey(apiKey.key, {
531
+ status: 'suspended',
532
+ suspendedAt: new Date(),
533
+ suspensionReason: 'payment_failed'
534
+ });
535
+ console.log(`✅ API key suspended due to payment failure: ${apiKey.key}`);
536
+ // Send payment failure email with 7-day grace period
537
+ const emailService = new email_service_1.EmailService();
538
+ const gracePeriodEndsAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
539
+ const billingPortalUrl = process.env.STRIPE_BILLING_PORTAL_URL || 'https://fraimworks.ai/billing';
540
+ await emailService.sendPaymentFailed(apiKey.userId, billingPortalUrl, gracePeriodEndsAt);
541
+ }
542
+ // ========== Legacy Payment Handlers ==========
543
+ async function handleCheckoutSessionCompleted(session, paymentRepo, dbService) {
544
+ if (dbService && (0, feature_flags_1.isPersonaEntitlementsEnabled)() && session.metadata?.plan === 'persona-hire') {
545
+ await (0, persona_entitlement_service_1.syncPersonaEntitlementFromCheckoutSession)(dbService, session, 'stripe-checkout');
546
+ }
547
+ console.log(`✅ Checkout session completed: ${session.id}`);
548
+ // Update payment record with payment intent ID
549
+ if (session.payment_intent) {
550
+ const paymentIntentId = typeof session.payment_intent === 'string'
551
+ ? session.payment_intent
552
+ : session.payment_intent.id;
553
+ // Find payment by checkout session ID and update with payment intent ID
554
+ const payment = await paymentRepo.getPaymentByStripePaymentIntent('');
555
+ // Note: This is a simplified approach. In production, you'd query by checkout session ID
556
+ // For now, we'll update in the payment_intent.succeeded handler
557
+ }
558
+ }
559
+ async function handlePaymentSuccess(paymentIntent, paymentRepo) {
560
+ console.log(`✅ Payment succeeded: ${paymentIntent.id}`);
561
+ // Update payment status
562
+ const updated = await paymentRepo.updatePaymentStatus(paymentIntent.id, 'succeeded', {
563
+ stripePaymentIntentId: paymentIntent.id,
564
+ completedAt: new Date(),
565
+ });
566
+ if (updated) {
567
+ console.log(`✅ Payment record updated: ${paymentIntent.id}`);
568
+ // TODO: Send confirmation email
569
+ }
570
+ else {
571
+ console.warn(`⚠️ Payment record not found for payment intent: ${paymentIntent.id}`);
572
+ }
573
+ }
574
+ async function handlePaymentFailure(paymentIntent, paymentRepo) {
575
+ console.log(`❌ Payment failed: ${paymentIntent.id}`);
576
+ // Update payment status
577
+ const updated = await paymentRepo.updatePaymentStatus(paymentIntent.id, 'failed', {
578
+ failureReason: paymentIntent.last_payment_error?.message || 'Unknown error',
579
+ failedAt: new Date(),
580
+ });
581
+ if (updated) {
582
+ console.log(`✅ Payment failure recorded: ${paymentIntent.id}`);
583
+ }
584
+ else {
585
+ console.warn(`⚠️ Payment record not found for payment intent: ${paymentIntent.id}`);
586
+ }
587
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMyPersonas = getMyPersonas;
4
+ const feature_flags_1 = require("../../config/feature-flags");
5
+ const persona_entitlement_service_1 = require("../../services/persona-entitlement-service");
6
+ async function getMyPersonas(req, res, dbService) {
7
+ try {
8
+ const apiKeyData = req.apiKeyData;
9
+ if (!apiKeyData?.userId) {
10
+ res.status(401).json({ error: 'Authentication required' });
11
+ return;
12
+ }
13
+ if (!(0, feature_flags_1.isPersonaEntitlementsEnabled)()) {
14
+ res.status(404).json({
15
+ error: 'Not found',
16
+ featureFlags: {
17
+ personaEntitlements: false
18
+ }
19
+ });
20
+ return;
21
+ }
22
+ const state = await (0, persona_entitlement_service_1.getWorkspacePersonaState)(dbService, apiKeyData.userId, apiKeyData.key);
23
+ res.json(state);
24
+ }
25
+ catch (error) {
26
+ console.error('Error getting persona entitlements:', error);
27
+ res.status(500).json({ error: 'Failed to load persona entitlements', details: error?.message || String(error) });
28
+ }
29
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPricingConfig = getPricingConfig;
4
+ const pricing_1 = require("../../config/pricing");
5
+ const feature_flags_1 = require("../../config/feature-flags");
6
+ /**
7
+ * GET /api/pricing/config
8
+ * Returns pricing configuration for frontend
9
+ */
10
+ async function getPricingConfig(req, res) {
11
+ try {
12
+ res.json({
13
+ pricing: pricing_1.PRICING,
14
+ fixedFees: pricing_1.FIXED_FEES,
15
+ managedPricing: pricing_1.MANAGED_PRICING,
16
+ founderDiscountRate: pricing_1.FOUNDER_DISCOUNT_RATE,
17
+ consumerEmailDomains: pricing_1.CONSUMER_EMAIL_DOMAINS,
18
+ featureFlags: (0, feature_flags_1.getPublicFeatureFlags)(),
19
+ });
20
+ }
21
+ catch (error) {
22
+ console.error('Error getting pricing config:', error);
23
+ res.status(500).json({ error: 'Failed to get pricing configuration' });
24
+ }
25
+ }