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
This commit is contained in:
saravanakumardb1 2026-02-15 14:59:27 -08:00
parent 0c3c109bf1
commit a699dd9073
2 changed files with 56 additions and 35 deletions

View File

@ -96,36 +96,50 @@ export async function authRoutes(app: FastifyInstance) {
// Registration hook: initialize subscription + license using product defaults. // Registration hook: initialize subscription + license using product defaults.
// Best-effort during migration; auth account creation remains primary. // Best-effort during migration; auth account creation remains primary.
await subscriptionRepo.createSubscription({ try {
id: `sub_${user.id}_${Date.now()}`, await subscriptionRepo.createSubscription({
productId, id: `sub_${user.id}_${Date.now()}`,
userId: user.id, productId,
plan: initialPlan, userId: user.id,
status: hasTrial ? 'trialing' : 'active', plan: initialPlan,
currentPeriodStart: nowIso, status: hasTrial ? 'trialing' : 'active',
currentPeriodEnd: hasTrial ? trialEnd.toISOString() : nowIso, currentPeriodStart: nowIso,
cancelAtPeriodEnd: false, currentPeriodEnd: hasTrial ? trialEnd.toISOString() : nowIso,
monthlyPrice: 0, cancelAtPeriodEnd: false,
tokensIncluded: 0, monthlyPrice: 0,
tokensUsed: 0, tokensIncluded: 0,
createdAt: nowIso, tokensUsed: 0,
updatedAt: nowIso, createdAt: nowIso,
}); updatedAt: nowIso,
});
} catch (err) {
req.log.warn(
{ err, userId: user.id, productId },
'Subscription provisioning failed during register'
);
}
await licenseRepo.create({ try {
id: `lic_${crypto.randomUUID()}`, await licenseRepo.create({
productId, id: `lic_${crypto.randomUUID()}`,
key: licenseRepo.generateKey(product.licensePrefix), productId,
userId: user.id, key: licenseRepo.generateKey(product.licensePrefix),
plan: initialPlan, userId: user.id,
status: 'active', plan: initialPlan,
activatedAt: null, status: 'active',
expiresAt: hasTrial ? trialEnd.toISOString() : null, activatedAt: null,
deviceIds: [], expiresAt: hasTrial ? trialEnd.toISOString() : null,
maxDevices: product.deviceLimits[initialPlan], deviceIds: [],
createdAt: nowIso, maxDevices: product.deviceLimits[initialPlan],
updatedAt: nowIso, createdAt: nowIso,
}); updatedAt: nowIso,
});
} catch (err) {
req.log.warn(
{ err, userId: user.id, productId },
'License provisioning failed during register'
);
}
const accessToken = await jwt.createAccessToken({ const accessToken = await jwt.createAccessToken({
sub: user.id, sub: user.id,

View File

@ -110,10 +110,6 @@ export async function stripeRoutes(app: FastifyInstance) {
// Route by productId in metadata (multi-tenant) // Route by productId in metadata (multi-tenant)
const metadata = getEventMetadata(event); const metadata = getEventMetadata(event);
const eventProductId = metadata?.productId || DEFAULT_PRODUCT_ID; 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) { switch (event.type) {
case 'checkout.session.completed': { 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 customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer.id;
const existing = await subRepo.getByStripeCustomerId(customerId, eventProductId); const existing = await subRepo.getByStripeCustomerId(customerId, eventProductId);
if (existing) { if (existing) {
const updatedPlan = getPlanFromSubscription(sub) ?? existing.plan;
const newStatus = sub.cancel_at_period_end const newStatus = sub.cancel_at_period_end
? 'cancelled' ? 'cancelled'
: sub.status === 'active' : sub.status === 'active'
? 'active' ? 'active'
: 'past_due'; : 'past_due';
await subRepo.updateSubscription(existing.id, existing.userId, { await subRepo.updateSubscription(existing.id, existing.userId, {
plan: updatedPlan,
status: newStatus, status: newStatus,
cancelAtPeriodEnd: sub.cancel_at_period_end, cancelAtPeriodEnd: sub.cancel_at_period_end,
currentPeriodEnd: new Date(sub.current_period_end * 1000).toISOString(), 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 // Sync plan to backend users container
await syncUserPlan(existing.userId, existing.plan, app.log); await syncUserPlan(existing.userId, updatedPlan, app.log);
} }
break; break;
} }
@ -279,3 +277,12 @@ function getEventMetadata(event: Stripe.Event): Record<string, string> | null {
} }
return 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;
}