import { Router } from 'express' import { z } from 'zod' import { prisma } from '../lib/prisma' import { applyAdditionalDriversToReservation } from '../services/additionalDriverService' import * as amanpay from '../services/amanpayService' import { applyInsurancesToReservation } from '../services/insuranceService' import { applyPricingRules } from '../services/pricingRuleService' import { validateAndFlagLicense, validateLicense } from '../services/licenseValidationService' import * as paypal from '../services/paypalService' import { getMarketplaceHomepageContent } from '../services/platformContentService' import { getVehicleAvailabilitySummary } from '../services/vehicleAvailabilityService' const router = Router() router.get('/platform/homepage', async (_req, res, next) => { try { res.json({ data: await getMarketplaceHomepageContent() }) } catch (err) { next(err) } }) function isDatabaseUnavailableError(error: unknown): boolean { if (!error || typeof error !== 'object') return false const candidate = error as { code?: string; message?: string } return candidate.code === 'P1001' || candidate.message?.includes("Can't reach database server") === true } async function findCompanyBySlug(slug: string) { return prisma.company.findFirstOrThrow({ where: { slug, status: { in: ['ACTIVE', 'TRIALING', 'PAST_DUE', 'SUSPENDED'] } }, include: { brand: true, contractSettings: true }, }) } router.get('/:slug/brand', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) res.json({ data: { company: { id: company.id, slug: company.slug, name: company.name, phone: company.phone, }, brand: company.brand, }, }) } catch (err) { if (isDatabaseUnavailableError(err)) { return res.json({ data: { company: { id: 'demo', slug: req.params.slug, name: 'Demo Company', phone: null }, brand: null, }, }) } next(err) } }) router.get('/:slug/vehicles', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const vehicles = await prisma.vehicle.findMany({ where: { companyId: company.id, isPublished: true }, orderBy: { createdAt: 'desc' }, }) const enrichedVehicles = await Promise.all( vehicles.map(async (vehicle) => { const availability = await getVehicleAvailabilitySummary(vehicle.id) return { ...vehicle, availability: availability.available, availabilityStatus: availability.status, nextAvailableAt: availability.nextAvailableAt, } }), ) res.json({ data: enrichedVehicles }) } catch (err) { if (isDatabaseUnavailableError(err)) return res.json({ data: [] }) next(err) } }) router.get('/:slug/vehicles/:id', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const vehicle = await prisma.vehicle.findFirstOrThrow({ where: { id: req.params.id, companyId: company.id, isPublished: true }, }) const availability = await getVehicleAvailabilitySummary(vehicle.id) res.json({ data: { ...vehicle, availability: availability.available, availabilityStatus: availability.status, nextAvailableAt: availability.nextAvailableAt } }) } catch (err) { if (isDatabaseUnavailableError(err)) { return res.status(503).json({ error: 'database_unavailable', message: 'Vehicle details are temporarily unavailable', statusCode: 503 }) } next(err) } }) router.get('/:slug/offers', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const offers = await prisma.offer.findMany({ where: { companyId: company.id, isActive: true, validFrom: { lte: new Date() }, validUntil: { gte: new Date() }, }, orderBy: [{ isFeatured: 'desc' }, { createdAt: 'desc' }], }) res.json({ data: offers }) } catch (err) { if (isDatabaseUnavailableError(err)) return res.json({ data: [] }) next(err) } }) router.get('/:slug/booking-options', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const insurancePolicies = await prisma.insurancePolicy.findMany({ where: { companyId: company.id, isActive: true }, orderBy: [{ isRequired: 'desc' }, { sortOrder: 'asc' }, { name: 'asc' }], }) res.json({ data: { insurancePolicies, contractSettings: company.contractSettings, }, }) } catch (err) { if (isDatabaseUnavailableError(err)) return res.json({ data: { insurancePolicies: [], contractSettings: null } }) next(err) } }) router.post('/:slug/availability', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const { vehicleId, startDate, endDate } = z.object({ vehicleId: z.string().cuid(), startDate: z.string().datetime(), endDate: z.string().datetime(), }).parse(req.body) const availability = await getVehicleAvailabilitySummary(vehicleId, { range: { startDate: new Date(startDate), endDate: new Date(endDate) }, }) res.json({ data: { available: availability.available, nextAvailableAt: availability.nextAvailableAt } }) } catch (err) { if (isDatabaseUnavailableError(err)) { return res.status(503).json({ error: 'database_unavailable', message: 'Availability checks are temporarily unavailable', statusCode: 503 }) } next(err) } }) router.post('/:slug/book/validate-code', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const { code } = z.object({ code: z.string().min(1) }).parse(req.body) const offer = await prisma.offer.findFirst({ where: { companyId: company.id, promoCode: code, isActive: true, validFrom: { lte: new Date() }, validUntil: { gte: new Date() }, }, }) if (!offer) { return res.status(404).json({ error: 'invalid_code', message: 'Promo code not found or expired', statusCode: 404 }) } res.json({ data: offer }) } catch (err) { if (isDatabaseUnavailableError(err)) { return res.status(503).json({ error: 'database_unavailable', message: 'Promo code validation is temporarily unavailable', statusCode: 503 }) } next(err) } }) router.post('/:slug/book', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const body = z.object({ vehicleId: z.string().cuid(), startDate: z.string().datetime(), endDate: z.string().datetime(), firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), phone: z.string().optional(), driverLicense: z.string().optional(), dateOfBirth: z.string().datetime().optional(), licenseExpiry: z.string().datetime().optional(), licenseIssuedAt: z.string().datetime().optional(), nationality: z.string().optional(), offerId: z.string().cuid().optional(), promoCodeUsed: z.string().optional(), notes: z.string().optional(), source: z.enum(['PUBLIC_SITE', 'MARKETPLACE']).default('PUBLIC_SITE'), selectedInsurancePolicyIds: z.array(z.string()).default([]), additionalDrivers: z.array(z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email().optional(), phone: z.string().optional(), driverLicense: z.string().min(1), licenseExpiry: z.string().datetime().optional(), licenseIssuedAt: z.string().datetime().optional(), dateOfBirth: z.string().datetime().optional(), nationality: z.string().optional(), })).default([]), }).parse(req.body) const vehicle = await prisma.vehicle.findFirstOrThrow({ where: { id: body.vehicleId, companyId: company.id, isPublished: true }, }) const start = new Date(body.startDate) const end = new Date(body.endDate) if (end <= start) { return res.status(400).json({ error: 'invalid_dates', message: 'End date must be after start date', statusCode: 400 }) } const availability = await getVehicleAvailabilitySummary(vehicle.id, { range: { startDate: start, endDate: end }, }) if (!availability.available) { return res.status(409).json({ error: 'unavailable', message: 'Vehicle is not available for the selected dates', statusCode: 409, nextAvailableAt: availability.nextAvailableAt?.toISOString() ?? null, }) } const customer = await prisma.customer.upsert({ where: { companyId_email: { companyId: company.id, email: body.email } }, update: { firstName: body.firstName, lastName: body.lastName, phone: body.phone ?? null, driverLicense: body.driverLicense ?? null, dateOfBirth: body.dateOfBirth ? new Date(body.dateOfBirth) : null, licenseExpiry: body.licenseExpiry ? new Date(body.licenseExpiry) : null, licenseIssuedAt: body.licenseIssuedAt ? new Date(body.licenseIssuedAt) : null, nationality: body.nationality ?? null, }, create: { companyId: company.id, firstName: body.firstName, lastName: body.lastName, email: body.email, phone: body.phone ?? null, driverLicense: body.driverLicense ?? null, dateOfBirth: body.dateOfBirth ? new Date(body.dateOfBirth) : null, licenseExpiry: body.licenseExpiry ? new Date(body.licenseExpiry) : null, licenseIssuedAt: body.licenseIssuedAt ? new Date(body.licenseIssuedAt) : null, nationality: body.nationality ?? null, }, }) const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86400000)) const baseAmount = vehicle.dailyRate * totalDays let discountAmount = 0 if (body.promoCodeUsed) { const offer = await prisma.offer.findFirst({ where: { companyId: company.id, promoCode: body.promoCodeUsed, isActive: true, validFrom: { lte: new Date() }, validUntil: { gte: new Date() }, }, }) if (offer) { if (offer.type === 'PERCENTAGE') discountAmount = Math.round(baseAmount * offer.discountValue / 100) else if (offer.type === 'FIXED_AMOUNT') discountAmount = offer.discountValue else if (offer.type === 'FREE_DAY') discountAmount = vehicle.dailyRate * offer.discountValue } } const { applied, total: pricingRulesTotal } = await applyPricingRules( company.id, customer.id, body.additionalDrivers, vehicle.dailyRate, totalDays, ) const primaryLicenseResult = validateLicense(body.licenseExpiry ? new Date(body.licenseExpiry) : null) if (primaryLicenseResult.status === 'EXPIRED') { return res.status(400).json({ error: 'license_expired', message: 'The primary driver license is expired', statusCode: 400 }) } const reservation = await prisma.reservation.create({ data: { companyId: company.id, vehicleId: vehicle.id, customerId: customer.id, offerId: body.offerId ?? null, promoCodeUsed: body.promoCodeUsed ?? null, source: body.source, startDate: start, endDate: end, dailyRate: vehicle.dailyRate, totalDays, totalAmount: baseAmount - discountAmount + pricingRulesTotal, discountAmount, pricingRulesApplied: applied, pricingRulesTotal, notes: body.notes ?? null, status: 'DRAFT', }, }) if (body.selectedInsurancePolicyIds.length > 0) { await applyInsurancesToReservation(reservation.id, company.id, body.selectedInsurancePolicyIds, totalDays, baseAmount) } if (body.additionalDrivers.length > 0) { await applyAdditionalDriversToReservation(reservation.id, company.id, body.additionalDrivers, totalDays) } if (body.licenseExpiry) { await validateAndFlagLicense(customer.id).catch(() => null) } const refreshedReservation = await prisma.reservation.findUniqueOrThrow({ where: { id: reservation.id }, include: { insurances: true, additionalDrivers: true }, }) res.status(201).json({ data: { ...refreshedReservation, requiresManualApproval: primaryLicenseResult.requiresApproval || refreshedReservation.additionalDrivers.some((driver) => driver.requiresApproval), }, }) } catch (err) { if (isDatabaseUnavailableError(err)) { return res.status(503).json({ error: 'database_unavailable', message: 'Booking is temporarily unavailable', statusCode: 503 }) } next(err) } }) router.get('/:slug/booking/:id', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const reservation = await prisma.reservation.findFirstOrThrow({ where: { id: req.params.id, companyId: company.id }, include: { vehicle: true, customer: true }, }) res.json({ data: reservation }) } catch (err) { if (isDatabaseUnavailableError(err)) { return res.status(503).json({ error: 'database_unavailable', message: 'Booking details are temporarily unavailable', statusCode: 503 }) } next(err) } }) router.post('/:slug/booking/:id/pay', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const reservation = await prisma.reservation.findFirstOrThrow({ where: { id: req.params.id, companyId: company.id }, include: { vehicle: true, customer: true, additionalDrivers: true }, }) if (reservation.paymentStatus === 'PAID') { return res.status(409).json({ error: 'already_paid', message: 'This reservation is already paid', statusCode: 409 }) } const customerLicenseResult = validateLicense(reservation.customer.licenseExpiry) if ( reservation.customer.licenseValidationStatus === 'DENIED' || customerLicenseResult.status === 'EXPIRED' || (customerLicenseResult.requiresApproval && reservation.customer.licenseValidationStatus !== 'APPROVED') || reservation.additionalDrivers.some((driver) => driver.licenseExpired || (driver.requiresApproval && !driver.approvedAt)) ) { return res.status(409).json({ error: 'license_review_required', message: 'This reservation requires license review before payment can be processed', statusCode: 409, }) } const { provider, currency, successUrl, failureUrl } = z.object({ provider: z.enum(['AMANPAY', 'PAYPAL']), currency: z.enum(['MAD', 'USD', 'EUR']).default('MAD'), successUrl: z.string().url(), failureUrl: z.string().url(), }).parse(req.body) const amount = reservation.totalAmount const description = `Rental: ${reservation.vehicle.make} ${reservation.vehicle.model}` const orderId = `res-${reservation.id}-${Date.now()}` const webhookBase = process.env.API_URL ?? 'http://localhost:4000' let checkoutUrl: string let amanpayTransactionId: string | null = null let paypalCaptureId: string | null = null if (provider === 'AMANPAY') { if (!amanpay.isConfigured()) { return res.status(503).json({ error: 'provider_not_configured', message: 'Online payment is not available for this company', statusCode: 503 }) } const brand = company.brand as any const merchantId = brand?.amanpayMerchantId ?? process.env.AMANPAY_MERCHANT_ID ?? '' const secretKey = brand?.amanpaySecretKey ?? process.env.AMANPAY_SECRET_KEY ?? '' if (!merchantId || !secretKey) { return res.status(503).json({ error: 'provider_not_configured', message: 'AmanPay is not configured for this company', statusCode: 503 }) } const result = await amanpay.createCheckout({ amount, currency, orderId, description, customerEmail: reservation.customer.email, customerName: `${reservation.customer.firstName} ${reservation.customer.lastName}`, successUrl, failureUrl, webhookUrl: `${webhookBase}/api/v1/payments/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 available for this company', statusCode: 503 }) } const result = await paypal.createOrder({ amount, currency, orderId, description, returnUrl: successUrl, cancelUrl: failureUrl, }) checkoutUrl = result.approveUrl paypalCaptureId = result.orderId } await prisma.rentalPayment.create({ data: { companyId: company.id, reservationId: reservation.id, amount, currency, status: 'PENDING', type: 'CHARGE', paymentProvider: provider, amanpayTransactionId, paypalCaptureId, }, }) res.json({ data: { checkoutUrl } }) } catch (err) { if (isDatabaseUnavailableError(err)) { return res.status(503).json({ error: 'database_unavailable', message: 'Payment initiation is temporarily unavailable', statusCode: 503 }) } next(err) } }) router.post('/:slug/booking/:id/capture-paypal', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const { paypalOrderId } = z.object({ paypalOrderId: z.string() }).parse(req.body) const payment = await prisma.rentalPayment.findFirstOrThrow({ where: { paypalCaptureId: paypalOrderId, companyId: company.id }, }) const capture = await paypal.captureOrder(paypalOrderId) const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId await prisma.rentalPayment.update({ where: { id: payment.id }, data: { status: 'SUCCEEDED', paidAt: new Date(), paypalCaptureId: captureId }, }) await prisma.reservation.update({ where: { id: payment.reservationId }, data: { paymentStatus: 'PAID', paidAmount: { increment: payment.amount } }, }) res.json({ data: { success: true } }) } catch (err) { next(err) } }) router.post('/:slug/contact', async (req, res, next) => { try { const company = await findCompanyBySlug(req.params.slug) const body = z.object({ name: z.string().min(1), email: z.string().email(), message: z.string().min(1), }).parse(req.body) res.json({ data: { success: true, deliveredTo: company.brand?.publicEmail ?? company.email, preview: body, }, }) } catch (err) { next(err) } }) export default router