From a699dd90737b27b457cc5b00c39faa03656b360a Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 15 Feb 2026 14:59:27 -0800 Subject: [PATCH] fix(platform-service): harden register/stripe flows for multi-product correctness - Make auth register provisioning truly best-effort (warn on failure, do not fail signup) - Process Stripe webhook events for all products (remove non-default skip) - Derive updated subscription plan from Stripe price IDs on subscription.updated - Sync derived plan to auth users and backend plan sync endpoint - Verified: tsc --noEmit clean, 20 test files / 183 tests passing --- .../src/modules/auth/routes.ts | 72 +++++++++++-------- .../src/modules/stripe/routes.ts | 19 +++-- 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/services/platform-service/src/modules/auth/routes.ts b/services/platform-service/src/modules/auth/routes.ts index 418c8345..1be2d78a 100644 --- a/services/platform-service/src/modules/auth/routes.ts +++ b/services/platform-service/src/modules/auth/routes.ts @@ -96,36 +96,50 @@ export async function authRoutes(app: FastifyInstance) { // Registration hook: initialize subscription + license using product defaults. // Best-effort during migration; auth account creation remains primary. - await subscriptionRepo.createSubscription({ - id: `sub_${user.id}_${Date.now()}`, - productId, - userId: user.id, - plan: initialPlan, - status: hasTrial ? 'trialing' : 'active', - currentPeriodStart: nowIso, - currentPeriodEnd: hasTrial ? trialEnd.toISOString() : nowIso, - cancelAtPeriodEnd: false, - monthlyPrice: 0, - tokensIncluded: 0, - tokensUsed: 0, - createdAt: nowIso, - updatedAt: nowIso, - }); + try { + await subscriptionRepo.createSubscription({ + id: `sub_${user.id}_${Date.now()}`, + productId, + userId: user.id, + plan: initialPlan, + status: hasTrial ? 'trialing' : 'active', + currentPeriodStart: nowIso, + currentPeriodEnd: hasTrial ? trialEnd.toISOString() : nowIso, + cancelAtPeriodEnd: false, + monthlyPrice: 0, + tokensIncluded: 0, + tokensUsed: 0, + createdAt: nowIso, + updatedAt: nowIso, + }); + } catch (err) { + req.log.warn( + { err, userId: user.id, productId }, + 'Subscription provisioning failed during register' + ); + } - await licenseRepo.create({ - id: `lic_${crypto.randomUUID()}`, - productId, - key: licenseRepo.generateKey(product.licensePrefix), - userId: user.id, - plan: initialPlan, - status: 'active', - activatedAt: null, - expiresAt: hasTrial ? trialEnd.toISOString() : null, - deviceIds: [], - maxDevices: product.deviceLimits[initialPlan], - createdAt: nowIso, - updatedAt: nowIso, - }); + try { + await licenseRepo.create({ + id: `lic_${crypto.randomUUID()}`, + productId, + key: licenseRepo.generateKey(product.licensePrefix), + userId: user.id, + plan: initialPlan, + status: 'active', + activatedAt: null, + expiresAt: hasTrial ? trialEnd.toISOString() : null, + deviceIds: [], + maxDevices: product.deviceLimits[initialPlan], + createdAt: nowIso, + updatedAt: nowIso, + }); + } catch (err) { + req.log.warn( + { err, userId: user.id, productId }, + 'License provisioning failed during register' + ); + } const accessToken = await jwt.createAccessToken({ sub: user.id, diff --git a/services/platform-service/src/modules/stripe/routes.ts b/services/platform-service/src/modules/stripe/routes.ts index 689273d7..22b77c32 100644 --- a/services/platform-service/src/modules/stripe/routes.ts +++ b/services/platform-service/src/modules/stripe/routes.ts @@ -110,10 +110,6 @@ export async function stripeRoutes(app: FastifyInstance) { // Route by productId in metadata (multi-tenant) const metadata = getEventMetadata(event); const eventProductId = metadata?.productId || DEFAULT_PRODUCT_ID; - if (eventProductId !== DEFAULT_PRODUCT_ID) { - app.log.info(`Ignoring event for product ${eventProductId}`); - return { received: true, skipped: true }; - } switch (event.type) { case 'checkout.session.completed': { @@ -183,19 +179,21 @@ export async function stripeRoutes(app: FastifyInstance) { const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer.id; const existing = await subRepo.getByStripeCustomerId(customerId, eventProductId); if (existing) { + const updatedPlan = getPlanFromSubscription(sub) ?? existing.plan; const newStatus = sub.cancel_at_period_end ? 'cancelled' : sub.status === 'active' ? 'active' : 'past_due'; await subRepo.updateSubscription(existing.id, existing.userId, { + plan: updatedPlan, status: newStatus, cancelAtPeriodEnd: sub.cancel_at_period_end, currentPeriodEnd: new Date(sub.current_period_end * 1000).toISOString(), }); - await authRepo.updatePlan(existing.userId, eventProductId, existing.plan); + await authRepo.updatePlan(existing.userId, eventProductId, updatedPlan); // Sync plan to backend users container - await syncUserPlan(existing.userId, existing.plan, app.log); + await syncUserPlan(existing.userId, updatedPlan, app.log); } break; } @@ -279,3 +277,12 @@ function getEventMetadata(event: Stripe.Event): Record | null { } return null; } + +function getPlanFromSubscription(sub: Stripe.Subscription): SubscriptionDoc['plan'] | null { + const priceId = sub.items.data[0]?.price?.id; + if (!priceId) return null; + const priceIds = getPriceIds(); + if (priceId === priceIds.enterprise) return 'enterprise'; + if (priceId === priceIds.pro) return 'pro'; + return null; +}