import { Router } from 'express' import { z } from 'zod' import { PLAN_PRICES } from '@rentaldrivego/types' import { prisma } from '../lib/prisma' import { requireCompanyAuth } from '../middleware/requireCompanyAuth' import { requireTenant } from '../middleware/requireTenant' import * as amanpay from '../services/amanpayService' import * as paypal from '../services/paypalService' const router = Router() router.get('/plans', (_req, res) => { res.json({ data: PLAN_PRICES }) }) // ─── AmanPay subscription webhook (no auth) ────────────────────────────────── router.post('/webhooks/amanpay', async (req, res, next) => { try { const rawBody = JSON.stringify(req.body) const signature = req.headers['x-amanpay-signature'] as string ?? '' if (amanpay.isConfigured() && !amanpay.verifyWebhookSignature(rawBody, signature)) { return res.status(401).json({ error: 'invalid_signature' }) } const event = req.body const transactionId = event.transaction_id ?? event.id const status = event.status?.toUpperCase() if (status === 'PAID' || status === 'SUCCEEDED') { const invoice = await prisma.subscriptionInvoice.findFirst({ where: { amanpayTransactionId: transactionId }, include: { subscription: true }, }) if (invoice) { await prisma.subscriptionInvoice.update({ where: { id: invoice.id }, data: { status: 'PAID', paidAt: new Date() }, }) await prisma.subscription.update({ where: { id: invoice.subscriptionId }, data: { status: 'ACTIVE', currentPeriodStart: new Date(), currentPeriodEnd: addPeriod(new Date(), invoice.subscription.billingPeriod), }, }) } } res.json({ received: true }) } catch (err) { next(err) } }) // ─── PayPal subscription webhook (no auth) ─────────────────────────────────── router.post('/webhooks/paypal', async (req, res, next) => { try { const rawBody = JSON.stringify(req.body) const isValid = await paypal.verifyWebhookEvent(req.headers as Record, rawBody) if (paypal.isConfigured() && !isValid) { return res.status(401).json({ error: 'invalid_signature' }) } const event = req.body if (event.event_type === 'PAYMENT.CAPTURE.COMPLETED') { const captureId = event.resource?.id as string const invoice = await prisma.subscriptionInvoice.findFirst({ where: { paypalCaptureId: captureId }, include: { subscription: true }, }) if (invoice) { await prisma.subscriptionInvoice.update({ where: { id: invoice.id }, data: { status: 'PAID', paidAt: new Date() }, }) await prisma.subscription.update({ where: { id: invoice.subscriptionId }, data: { status: 'ACTIVE', currentPeriodStart: new Date(), currentPeriodEnd: addPeriod(new Date(), invoice.subscription.billingPeriod), }, }) } } res.json({ received: true }) } catch (err) { next(err) } }) // ─── PayPal capture redirect (no full auth needed — just valid session) ─────── router.post('/capture-paypal', requireCompanyAuth, requireTenant, async (req, res, next) => { try { const { paypalOrderId } = z.object({ paypalOrderId: z.string() }).parse(req.body) const invoice = await prisma.subscriptionInvoice.findFirstOrThrow({ where: { paypalCaptureId: paypalOrderId, companyId: req.companyId }, include: { subscription: true }, }) const capture = await paypal.captureOrder(paypalOrderId) as Record const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId await prisma.subscriptionInvoice.update({ where: { id: invoice.id }, data: { status: 'PAID', paidAt: new Date(), paypalCaptureId: captureId }, }) await prisma.subscription.update({ where: { id: invoice.subscriptionId }, data: { status: 'ACTIVE', currentPeriodStart: new Date(), currentPeriodEnd: addPeriod(new Date(), invoice.subscription.billingPeriod), }, }) res.json({ data: { success: true } }) } catch (err) { next(err) } }) // ─── Authenticated subscription routes ─────────────────────────────────────── router.use(requireCompanyAuth, requireTenant) router.get('/me', async (req, res, next) => { try { const subscription = await prisma.subscription.findUnique({ where: { companyId: req.companyId }, include: { invoices: { orderBy: { createdAt: 'desc' }, take: 12 } }, }) res.json({ data: subscription }) } catch (err) { next(err) } }) router.get('/invoices', async (req, res, next) => { try { const invoices = await prisma.subscriptionInvoice.findMany({ where: { companyId: req.companyId }, orderBy: { createdAt: 'desc' }, take: 50, }) res.json({ data: invoices }) } catch (err) { next(err) } }) const checkoutSchema = z.object({ plan: z.enum(['STARTER', 'GROWTH', 'PRO']), billingPeriod: z.enum(['MONTHLY', 'ANNUAL']), currency: z.enum(['MAD', 'USD', 'EUR']), provider: z.enum(['AMANPAY', 'PAYPAL']), successUrl: z.string().url(), failureUrl: z.string().url(), }) router.post('/checkout', async (req, res, next) => { try { const body = checkoutSchema.parse(req.body) const prices = PLAN_PRICES[body.plan]?.[body.billingPeriod] if (!prices) return res.status(400).json({ error: 'invalid_plan', message: 'Invalid plan or billing period', statusCode: 400 }) const amount = prices[body.currency] if (!amount) return res.status(400).json({ error: 'invalid_currency', message: 'Currency not supported for this plan', statusCode: 400 }) const company = await prisma.company.findUniqueOrThrow({ where: { id: req.companyId } }) let subscription = await prisma.subscription.findUnique({ where: { companyId: req.companyId } }) if (!subscription) { subscription = await prisma.subscription.create({ data: { companyId: req.companyId, plan: body.plan, billingPeriod: body.billingPeriod, currency: body.currency, status: 'PENDING' as any, }, }) } const orderId = `sub-${req.companyId}-${Date.now()}` const description = `${body.plan} plan — ${body.billingPeriod}` const webhookBase = process.env.API_URL ?? 'http://localhost:4000' let checkoutUrl: string let amanpayTransactionId: string | null = null let paypalCaptureId: string | null = null if (body.provider === 'AMANPAY') { if (!amanpay.isConfigured()) { return res.status(503).json({ error: 'provider_not_configured', message: 'AmanPay is not configured on this platform', statusCode: 503 }) } const result = await amanpay.createCheckout({ amount, currency: body.currency, orderId, description, customerEmail: company.email, customerName: company.name, successUrl: body.successUrl, failureUrl: body.failureUrl, webhookUrl: `${webhookBase}/api/v1/subscriptions/webhooks/amanpay`, }) checkoutUrl = result.checkoutUrl amanpayTransactionId = result.transactionId } else { if (!paypal.isConfigured()) { return res.status(503).json({ error: 'provider_not_configured', message: 'PayPal is not configured on this platform', statusCode: 503 }) } const result = await paypal.createOrder({ amount, currency: body.currency, orderId, description, returnUrl: body.successUrl, cancelUrl: body.failureUrl, }) checkoutUrl = result.approveUrl paypalCaptureId = result.orderId } const invoice = await prisma.subscriptionInvoice.create({ data: { companyId: req.companyId, subscriptionId: subscription.id, amount, currency: body.currency, status: 'PENDING', paymentProvider: body.provider, amanpayTransactionId, paypalCaptureId, }, }) res.json({ data: { invoice, checkoutUrl } }) } catch (err) { next(err) } }) router.post('/change-plan', async (req, res, next) => { try { const { plan, billingPeriod, currency } = z.object({ plan: z.enum(['STARTER', 'GROWTH', 'PRO']), billingPeriod: z.enum(['MONTHLY', 'ANNUAL']), currency: z.enum(['MAD', 'USD', 'EUR']), }).parse(req.body) const updated = await prisma.subscription.update({ where: { companyId: req.companyId }, data: { plan, billingPeriod, currency }, }) res.json({ data: updated }) } catch (err) { next(err) } }) router.post('/cancel', async (req, res, next) => { try { const updated = await prisma.subscription.update({ where: { companyId: req.companyId }, data: { cancelAtPeriodEnd: true }, }) res.json({ data: updated }) } catch (err) { next(err) } }) router.post('/resume', async (req, res, next) => { try { const updated = await prisma.subscription.update({ where: { companyId: req.companyId }, data: { cancelAtPeriodEnd: false }, }) res.json({ data: updated }) } catch (err) { next(err) } }) // ─── Helpers ───────────────────────────────────────────────────────────────── function addPeriod(date: Date, period: string): Date { const d = new Date(date) if (period === 'ANNUAL') { d.setFullYear(d.getFullYear() + 1) } else { d.setMonth(d.getMonth() + 1) } return d } export default router