fraim 2.0.177 → 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.
- package/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/ai-hub/server.js +50 -1
- package/dist/src/api/admin/payments.js +33 -0
- package/dist/src/api/admin/sales-leads.js +21 -0
- package/dist/src/api/payment/create-session.js +338 -0
- package/dist/src/api/payment/dashboard-link.js +149 -0
- package/dist/src/api/payment/session-details.js +31 -0
- package/dist/src/api/payment/webhook.js +587 -0
- package/dist/src/api/personas/me.js +29 -0
- package/dist/src/api/pricing/get-config.js +25 -0
- package/dist/src/api/sales/contact.js +44 -0
- package/dist/src/cli/commands/add-provider.js +74 -61
- package/dist/src/cli/commands/add-surface.js +128 -0
- package/dist/src/cli/commands/login.js +5 -69
- package/dist/src/cli/commands/setup.js +27 -347
- package/dist/src/cli/distribution/marketplace-bundles.js +580 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +5 -3
- package/dist/src/cli/mcp/mcp-server-registry.js +10 -3
- package/dist/src/cli/providers/local-provider-registry.js +2 -3
- package/dist/src/cli/setup/auto-mcp-setup.js +9 -32
- package/dist/src/cli/setup/ide-detector.js +34 -14
- package/dist/src/config/persona-capability-bundles.js +17 -13
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/first-run/session-service.js +2 -2
- package/dist/src/fraim/config-loader.js +11 -0
- package/dist/src/fraim/db-service.js +2387 -0
- package/dist/src/fraim/issues.js +152 -0
- package/dist/src/fraim/template-processor.js +184 -0
- package/dist/src/fraim/utils/request-utils.js +23 -0
- package/dist/src/local-mcp-server/stdio-server.js +28 -4
- package/dist/src/local-mcp-server/usage-collector.js +24 -0
- package/dist/src/middleware/auth.js +266 -0
- package/dist/src/middleware/cors-config.js +111 -0
- package/dist/src/middleware/logger.js +116 -0
- package/dist/src/middleware/rate-limit.js +110 -0
- package/dist/src/middleware/reject-query-api-key.js +45 -0
- package/dist/src/middleware/security-headers.js +41 -0
- package/dist/src/middleware/telemetry.js +134 -0
- package/dist/src/models/payment.js +2 -0
- package/dist/src/routes/analytics.js +1447 -0
- package/dist/src/routes/app-routes.js +32 -0
- package/dist/src/routes/auth-routes.js +505 -0
- package/dist/src/routes/oauth-routes.js +325 -0
- package/dist/src/routes/payment-routes.js +186 -0
- package/dist/src/routes/persona-catalog-routes.js +84 -0
- package/dist/src/services/admin-service.js +229 -0
- package/dist/src/services/audit-log-persistence.js +60 -0
- package/dist/src/services/audit-log.js +69 -0
- package/dist/src/services/cookie-service.js +129 -0
- package/dist/src/services/dashboard-access.js +27 -0
- package/dist/src/services/demo-seed-service.js +139 -0
- package/dist/src/services/email-code.js +23 -0
- package/dist/src/services/email-service-clean.js +782 -0
- package/dist/src/services/email-service.js +951 -0
- package/dist/src/services/installer-service.js +131 -0
- package/dist/src/services/mcp-oauth-store.js +33 -0
- package/dist/src/services/mcp-service.js +823 -0
- package/dist/src/services/oauth-helpers.js +127 -0
- package/dist/src/services/org-service.js +89 -0
- package/dist/src/services/persona-entitlement-service.js +288 -0
- package/dist/src/services/provider-service.js +215 -0
- package/dist/src/services/registry-service.js +628 -0
- package/dist/src/services/session-service.js +86 -0
- package/dist/src/services/trial-reminder-service.js +120 -0
- package/dist/src/services/usage-analytics-service.js +419 -0
- package/dist/src/services/workspace-identity.js +21 -0
- package/dist/src/types/analytics.js +2 -0
- package/dist/src/utils/payment-calculator.js +52 -0
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +33 -0
- package/extensions/office-word/taskpane.html +242 -0
- package/package.json +14 -3
- package/public/ai-hub/index.html +14 -2
- package/public/ai-hub/script.js +340 -66
- package/public/ai-hub/styles.css +83 -0
|
@@ -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
|
+
}
|