import { PLAN_FEATURES, PLAN_PRICES } from '@rentaldrivego/types' import { AppError, NotFoundError } from '../../http/errors' import { getVehicleAvailabilitySummary } from '../../services/vehicleAvailabilityService' import { applyInsurancesToReservation } from '../../services/insuranceService' import { applyAdditionalDriversToReservation } from '../../services/additionalDriverService' import { applyPricingRules } from '../../services/pricingRuleService' import { validateLicense, validateAndFlagLicense } from '../../services/licenseValidationService' import { getMarketplaceHomepageContent } from '../../services/platformContentService' import { prisma } from '../../lib/prisma' import * as amanpay from '../../services/amanpayService' import * as paypal from '../../services/paypalService' import * as repo from './site.repo' import { presentBrand } from './site.presenter' export async function getPlatformHomepage() { return getMarketplaceHomepageContent() } export async function getPlatformPricing() { const [configs, features] = await Promise.all([ prisma.pricingConfig.findMany(), prisma.planFeature.findMany({ orderBy: [{ plan: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }], }), ]) const prices = configs.length === 0 ? PLAN_PRICES : configs.reduce>>>((acc, config) => { if (!acc[config.plan]) acc[config.plan] = {} acc[config.plan]![config.billingPeriod] = { MAD: config.amount } return acc }, {}) const planFeatures = Object.fromEntries( Object.entries(PLAN_FEATURES).map(([plan, fallback]) => { const labels = features.filter((feature) => feature.plan === plan).map((feature) => feature.label) return [plan, labels.length > 0 ? labels : fallback] }), ) return { prices, planFeatures } } export async function getBrand(slug: string) { const company = await repo.findCompanyBySlug(slug) return presentBrand(company) } export async function getPublicVehicles(slug: string) { const company = await repo.findCompanyBySlug(slug) const vehicles = await repo.findPublishedVehicles(company.id) return Promise.all( vehicles.map(async (v) => { const a = await getVehicleAvailabilitySummary(v.id) return { ...v, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt } }), ) } export async function getVehicleDetail(slug: string, vehicleId: string) { const company = await repo.findCompanyBySlug(slug) const vehicle = await repo.findVehicleById(vehicleId, company.id) const a = await getVehicleAvailabilitySummary(vehicle.id) return { ...vehicle, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt } } export async function getOffers(slug: string) { const company = await repo.findCompanyBySlug(slug) return repo.findActiveOffers(company.id) } export async function getBookingOptions(slug: string) { const company = await repo.findCompanyBySlug(slug) const insurancePolicies = await repo.findInsurancePolicies(company.id) return { insurancePolicies, contractSettings: company.contractSettings } } export async function checkAvailability(slug: string, vehicleId: string, startDate: string, endDate: string) { await repo.findCompanyBySlug(slug) const availability = await getVehicleAvailabilitySummary(vehicleId, { range: { startDate: new Date(startDate), endDate: new Date(endDate) }, }) return { available: availability.available, nextAvailableAt: availability.nextAvailableAt } } export async function validatePromoCode(slug: string, code: string) { const company = await repo.findCompanyBySlug(slug) const offer = await repo.findOfferByPromoCode(company.id, code) if (!offer) throw new NotFoundError('Promo code not found or expired') return offer } export async function createBooking(slug: string, body: { vehicleId: string; startDate: string; endDate: string firstName: string; lastName: string; email: string; phone?: string driverLicense?: string; dateOfBirth?: string; licenseExpiry?: string; licenseIssuedAt?: string; nationality?: string offerId?: string; promoCodeUsed?: string; notes?: string; source?: string selectedInsurancePolicyIds?: string[]; additionalDrivers?: any[] }) { const company = await repo.findCompanyBySlug(slug) const vehicle = await repo.findVehicleById(body.vehicleId, company.id) const start = new Date(body.startDate) const end = new Date(body.endDate) if (end <= start) throw new AppError('End date must be after start date', 400, 'invalid_dates') const availability = await getVehicleAvailabilitySummary(vehicle.id, { range: { startDate: start, endDate: end } }) if (!availability.available) { throw new AppError('Vehicle is not available for the selected dates', 409, 'unavailable', { nextAvailableAt: availability.nextAvailableAt?.toISOString() ?? null, }) } const customer = await repo.upsertCustomer(company.id, { email: body.email, firstName: body.firstName, lastName: body.lastName, phone: body.phone, driverLicense: body.driverLicense, dateOfBirth: body.dateOfBirth, licenseExpiry: body.licenseExpiry, licenseIssuedAt: body.licenseIssuedAt, nationality: body.nationality, }) const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86_400_000)) const baseAmount = vehicle.dailyRate * totalDays let discountAmount = 0 if (body.promoCodeUsed) { const offer = await repo.findOfferByPromoCode(company.id, body.promoCodeUsed) 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 additionalDriversList = body.additionalDrivers ?? [] const { applied, total: pricingRulesTotal } = await applyPricingRules( company.id, customer.id, additionalDriversList as any[], vehicle.dailyRate, totalDays, ) const primaryLicenseResult = validateLicense(body.licenseExpiry ? new Date(body.licenseExpiry) : null) if (primaryLicenseResult.status === 'EXPIRED') { throw new AppError('The primary driver license is expired', 400, 'license_expired') } const reservation = await repo.createReservation({ companyId: company.id, vehicleId: vehicle.id, customerId: customer.id, offerId: body.offerId ?? null, promoCodeUsed: body.promoCodeUsed ?? null, vehicleCategory: vehicle.category, source: body.source ?? 'PUBLIC_SITE', startDate: start, endDate: end, dailyRate: vehicle.dailyRate, totalDays, totalAmount: baseAmount - discountAmount + pricingRulesTotal, discountAmount, pricingRulesApplied: applied, pricingRulesTotal, notes: body.notes ?? null, status: 'DRAFT', }) const insuranceIds = body.selectedInsurancePolicyIds ?? [] if (insuranceIds.length > 0) { await applyInsurancesToReservation(reservation.id, company.id, insuranceIds, totalDays, baseAmount) } if (additionalDriversList.length > 0) { await applyAdditionalDriversToReservation(reservation.id, company.id, additionalDriversList, totalDays) } if (body.licenseExpiry) { await validateAndFlagLicense(customer.id).catch(() => null) } const refreshed = await repo.findReservationWithDetails(reservation.id) return { ...refreshed, requiresManualApproval: primaryLicenseResult.requiresApproval || refreshed.additionalDrivers.some((d) => d.requiresApproval), } } export async function getBooking(slug: string, reservationId: string) { const company = await repo.findCompanyBySlug(slug) return repo.findBooking(reservationId, company.id) } export async function initPayment(slug: string, reservationId: string, body: { provider: 'AMANPAY' | 'PAYPAL'; currency?: 'MAD'; successUrl: string; failureUrl: string }) { const company = await repo.findCompanyBySlug(slug) const reservation = await repo.findReservationForPayment(reservationId, company.id) if (reservation.paymentStatus === 'PAID') { throw new AppError('This reservation is already paid', 409, 'already_paid') } const customerLicenseResult = validateLicense(reservation.customer.licenseExpiry) const licenseBlocked = reservation.customer.licenseValidationStatus === 'DENIED' || customerLicenseResult.status === 'EXPIRED' || (customerLicenseResult.requiresApproval && reservation.customer.licenseValidationStatus !== 'APPROVED') || reservation.additionalDrivers.some((d) => d.licenseExpired || (d.requiresApproval && !d.approvedAt)) if (licenseBlocked) { throw new AppError('This reservation requires license review before payment can be processed', 409, 'license_review_required') } const currency = body.currency ?? 'MAD' 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 (body.provider === 'AMANPAY') { if (!amanpay.isConfigured()) { throw new AppError('Online payment is not available for this company', 503, 'provider_not_configured') } 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) { throw new AppError('AmanPay is not configured for this company', 503, 'provider_not_configured') } const result = await amanpay.createCheckout({ amount, currency, orderId, description, customerEmail: reservation.customer.email, customerName: `${reservation.customer.firstName} ${reservation.customer.lastName}`, successUrl: body.successUrl, failureUrl: body.failureUrl, webhookUrl: `${webhookBase}/api/v1/payments/webhooks/amanpay`, }) checkoutUrl = result.checkoutUrl amanpayTransactionId = result.transactionId } else { if (!paypal.isConfigured()) { throw new AppError('PayPal is not available for this company', 503, 'provider_not_configured') } const result = await paypal.createOrder({ amount, currency, orderId, description, returnUrl: body.successUrl, cancelUrl: body.failureUrl, }) checkoutUrl = result.approveUrl paypalCaptureId = result.orderId } await repo.createRentalPayment({ companyId: company.id, reservationId: reservation.id, amount, currency, paymentProvider: body.provider, amanpayTransactionId, paypalCaptureId, }) return { checkoutUrl } } export async function capturePaypal(slug: string, paypalOrderId: string) { const company = await repo.findCompanyBySlug(slug) const payment = await repo.findPaymentByPaypalOrderId(paypalOrderId, company.id) const capture = await paypal.captureOrder(paypalOrderId) as Record const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId await repo.capturePaypalPayment(payment.id, captureId, payment.reservationId, payment.amount) return { success: true } } export async function handleContact(slug: string, body: { name: string; email: string; message: string }) { const company = await repo.findCompanyBySlug(slug) return { success: true, deliveredTo: company.brand?.publicEmail ?? company.email, preview: body, } }