CognxSafeTrack
chore: execute Sprint 38 technical debt resolution (Type Safety, Zod validation, Vitest, Mock LLM extracted)
d9879cf | import { FastifyInstance } from 'fastify'; | |
| import { stripeService } from '../services/stripe'; | |
| import { prisma } from '../services/prisma'; | |
| import { z } from 'zod'; | |
| // βββ Shared Zod schemas ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const checkoutSchema = z.object({ | |
| userId: z.string().uuid(), | |
| trackId: z.string().uuid(), | |
| }); | |
| // βββ Private routes (require ADMIN_API_KEY) βββββββββββββββββββββββββββββββββββ | |
| export async function paymentRoutes(fastify: FastifyInstance) { | |
| // Create a Checkout Session | |
| fastify.post('/checkout', async (request, reply) => { | |
| const parseResult = checkoutSchema.safeParse(request.body); | |
| if (!parseResult.success) { | |
| return reply.status(400).send({ error: 'Invalid request body', details: parseResult.error.flatten() }); | |
| } | |
| const { userId, trackId } = parseResult.data; | |
| try { | |
| // Validate the track exists and is premium | |
| const track = await prisma.track.findUnique({ where: { id: trackId } }); | |
| if (!track || !track.isPremium || !track.stripePriceId) { | |
| return reply.status(400).send({ error: 'Invalid or non-premium track' }); | |
| } | |
| const user = await prisma.user.findUnique({ where: { id: userId } }); | |
| if (!user) { | |
| return reply.status(404).send({ error: 'User not found' }); | |
| } | |
| const checkoutUrl = await stripeService.createCheckoutSession( | |
| user.id, | |
| track.id, | |
| track.stripePriceId, | |
| user.phone | |
| ); | |
| return { success: true, url: checkoutUrl }; | |
| } catch (error) { | |
| fastify.log.error(error); | |
| return reply.status(500).send({ error: 'Failed to create checkout session' }); | |
| } | |
| }); | |
| } | |
| // βββ Public Stripe webhook (no API key auth β secured by Stripe signature) ββββ | |
| export async function stripeWebhookRoute(fastify: FastifyInstance) { | |
| // Capture raw body buffer for Stripe signature verification | |
| fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => { | |
| (req as any).rawBody = body; | |
| try { | |
| done(null, JSON.parse(body.toString('utf8'))); | |
| } catch (err: unknown) { | |
| done(err as Error, undefined as unknown as Buffer); | |
| } | |
| }); | |
| fastify.post('/webhook', async (request, reply) => { | |
| const sig = request.headers['stripe-signature']; | |
| if (!sig || typeof sig !== 'string') { | |
| return reply.status(400).send({ error: 'Missing stripe-signature header' }); | |
| } | |
| let event; | |
| try { | |
| const rawBody = (request as any).rawBody as Buffer; | |
| event = stripeService.verifyWebhookSignature(rawBody, sig); | |
| } catch (err: unknown) { | |
| fastify.log.warn(`[Stripe Webhook] Signature verification failed: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`); | |
| return reply.status(400).send(`Webhook Error: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`); | |
| } | |
| // Handle the checkout.session.completed event | |
| if (event.type === 'checkout.session.completed') { | |
| const session = event.data.object as any; | |
| const userId = session.metadata?.userId; | |
| const trackId = session.metadata?.trackId; | |
| const amountTotal = session.amount_total; | |
| if (userId && trackId) { | |
| try { | |
| await prisma.$transaction(async (tx: any) => { | |
| // 1. Record the payment | |
| await tx.payment.create({ | |
| data: { | |
| userId, | |
| trackId, | |
| amount: amountTotal, | |
| status: 'COMPLETED', | |
| stripeSessionId: session.id, | |
| currency: session.currency || 'XOF' | |
| } | |
| }); | |
| // 2. Create the Enrollment (dedup) | |
| const existingEnrollment = await tx.enrollment.findFirst({ | |
| where: { userId, trackId } | |
| }); | |
| if (!existingEnrollment) { | |
| await tx.enrollment.create({ | |
| data: { | |
| userId, | |
| trackId, | |
| status: 'ACTIVE', | |
| currentDay: 1 | |
| } | |
| }); | |
| fastify.log.info(`[Stripe Webhook] Enrollment created for User ${userId}, Track ${trackId}`); | |
| } | |
| }); | |
| } catch (dbError) { | |
| fastify.log.error(dbError, '[Stripe Webhook] Database error during webhook processing'); | |
| // Still return 200 so Stripe doesn't retry endlessly β but alert monitoring. | |
| } | |
| } | |
| } | |
| reply.send({ received: true }); | |
| }); | |
| } | |