import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@rentaldrivego/types', () => ({ PLAN_FEATURES: { STARTER: ['fallback feature'] }, PLAN_PRICES: { STARTER: { MONTHLY: { MAD: 199 } } }, })) vi.mock('../../lib/prisma', () => ({ prisma: { pricingConfig: { findMany: vi.fn() }, planFeature: { findMany: vi.fn() }, }, })) vi.mock('../../services/platformContentService', () => ({ getStorefrontHomepageContent: vi.fn() })) vi.mock('../../services/vehicleAvailabilityService', () => ({ getVehicleAvailabilitySummary: vi.fn() })) vi.mock('../../services/insuranceService', () => ({ applyInsurancesToReservation: vi.fn() })) vi.mock('../../services/additionalDriverService', () => ({ applyAdditionalDriversToReservation: vi.fn() })) vi.mock('../../services/pricingRuleService', () => ({ applyPricingRules: vi.fn() })) vi.mock('../../services/licenseValidationService', () => ({ validateLicense: vi.fn(), validateAndFlagLicense: vi.fn().mockResolvedValue(undefined) })) vi.mock('../../services/amanpayService', () => ({ isConfigured: vi.fn(), createCheckout: vi.fn(), verifyWebhookSignature: vi.fn(), findTransactionFromWebhook: vi.fn() })) vi.mock('../../services/paypalService', () => ({ createOrder: vi.fn(), captureOrder: vi.fn() })) vi.mock('./site.presenter', () => ({ presentBrand: vi.fn((company: any) => ({ company: { id: company.id, slug: company.slug }, brand: company.brand ?? null })), presentPublicBooking: vi.fn((reservation: any) => ({ bookingReference: reservation.bookingReference ?? null, status: reservation.status === 'DRAFT' ? 'PENDING' : reservation.status, })), })) vi.mock('./site.repo', () => ({ findCompanyBySlug: vi.fn(), findPublishedVehicles: vi.fn(), findVehicleById: vi.fn(), findActiveOffers: vi.fn(), findInsurancePolicies: vi.fn(), findOfferByPromoCode: vi.fn(), upsertCustomer: vi.fn(), createReservation: vi.fn(), findReservationWithDetails: vi.fn(), findBooking: vi.fn(), findReservationForPayment: vi.fn(), createReservationPublicAccess: vi.fn(), findReservationPublicAccess: vi.fn(), markReservationPublicAccessUsed: vi.fn(), createRentalPayment: vi.fn(), findPaymentByPaypalOrderId: vi.fn(), capturePaypalPayment: vi.fn(), })) import { AppError } from '../../http/errors' import { prisma } from '../../lib/prisma' import { getVehicleAvailabilitySummary } from '../../services/vehicleAvailabilityService' import { applyPricingRules } from '../../services/pricingRuleService' import { applyInsurancesToReservation } from '../../services/insuranceService' import { applyAdditionalDriversToReservation } from '../../services/additionalDriverService' import { validateLicense, validateAndFlagLicense } from '../../services/licenseValidationService' import * as amanpay from '../../services/amanpayService' import * as paypal from '../../services/paypalService' import * as repo from './site.repo' import * as service from './site.service' const company = { id: 'company_1', slug: 'atlas', email: 'office@example.test', contractSettings: { depositRequired: true }, brand: { publicEmail: 'hello@example.test' } } function bookingBody(overrides: Record = {}) { return { vehicleId: 'vehicle_1', startDate: '2026-07-01T00:00:00.000Z', endDate: '2026-07-04T00:00:00.000Z', firstName: 'Nora', lastName: 'Renter', email: 'nora@example.test', phone: '+212600000000', pickupLocation: 'Casablanca', returnLocation: 'Casablanca', ...overrides, } as any } describe('site.service public booking/payment boundaries', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(repo.createReservationPublicAccess).mockResolvedValue({ id: 'access_1' } as never) vi.mocked(repo.findReservationPublicAccess).mockResolvedValue({ id: 'access_1' } as never) vi.mocked(repo.markReservationPublicAccessUsed).mockResolvedValue({ id: 'access_1' } as never) vi.mocked(repo.findCompanyBySlug).mockResolvedValue(company as never) vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null } as never) vi.mocked(applyPricingRules).mockResolvedValue({ applied: [{ code: 'WEEKEND' }], total: 90 } as never) vi.mocked(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false, expired: false, expiringSoon: false } as never) }) it('uses configured pricing and configured feature labels when platform pricing exists', async () => { vi.mocked(prisma.pricingConfig.findMany).mockResolvedValue([ { plan: 'STARTER', billingPeriod: 'MONTHLY', amount: 299 }, { plan: 'PRO', billingPeriod: 'YEARLY', amount: 2999 }, ] as never) vi.mocked(prisma.planFeature.findMany).mockResolvedValue([ { plan: 'STARTER', label: '3 vehicles' }, { plan: 'PRO', label: 'Priority support' }, ] as never) await expect(service.getPlatformPricing()).resolves.toEqual({ prices: { STARTER: { MONTHLY: { MAD: 299 } }, PRO: { YEARLY: { MAD: 2999 } } }, planFeatures: { STARTER: ['3 vehicles'] }, }) }) it('calculates booking totals from base rate, free-day promo, pricing rules, insurance and additional drivers', async () => { vi.mocked(repo.findVehicleById).mockResolvedValue({ id: 'vehicle_1', companyId: 'company_1', dailyRate: 500, category: 'SUV' } as never) vi.mocked(repo.upsertCustomer).mockResolvedValue({ id: 'customer_1' } as never) vi.mocked(repo.findOfferByPromoCode).mockResolvedValue({ id: 'offer_1', type: 'FREE_DAY', discountValue: 1 } as never) vi.mocked(repo.createReservation).mockResolvedValue({ id: 'reservation_1' } as never) vi.mocked(repo.findReservationWithDetails).mockResolvedValue({ id: 'reservation_1', bookingReference: 'BK-2026-AB12CD', status: 'DRAFT', startDate: new Date('2026-07-01T10:00:00.000Z'), endDate: new Date('2026-07-04T10:00:00.000Z'), pickupLocation: 'Casablanca', returnLocation: 'Casablanca', vehicle: { make: 'Dacia', model: 'Duster', year: 2024 }, customer: { firstName: 'Aya', lastName: 'Benali', email: 'aya@example.test', phone: '+212600000000' }, company: { name: 'Atlas Cars', brand: { displayName: 'Atlas Cars' } }, additionalDrivers: [], insurances: [], totalAmount: 1090, paidAmount: 0, paymentStatus: 'UNPAID', } as never) const result = await service.createBooking('atlas', bookingBody({ promoCodeUsed: 'FREEDAY', selectedInsurancePolicyIds: ['insurance_1'], additionalDrivers: [{ firstName: 'Second' }], })) expect(repo.createReservation).toHaveBeenCalledWith(expect.objectContaining({ totalDays: 3, dailyRate: 500, discountAmount: 500, pricingRulesTotal: 90, totalAmount: 1090, pickupLocation: 'Casablanca', returnLocation: 'Casablanca', status: 'DRAFT', source: 'PUBLIC_SITE', })) expect(applyInsurancesToReservation).not.toHaveBeenCalled() expect(applyAdditionalDriversToReservation).not.toHaveBeenCalled() expect(validateAndFlagLicense).not.toHaveBeenCalled() expect(result.status).toBe('PENDING') expect(result.bookingReference).toBe('BK-2026-AB12CD') }) it('rejects payment initialization for already-paid reservations before provider calls', async () => { vi.mocked(repo.findReservationForPayment).mockResolvedValue({ paymentStatus: 'PAID' } as never) await expect(service.initPayment('atlas', 'reservation_1', { provider: 'AMANPAY', accessToken: 'booking-access-token-123', successUrl: 'https://example.test/success', failureUrl: 'https://example.test/failure', })).rejects.toMatchObject({ statusCode: 409, error: 'already_paid' }) expect(amanpay.createCheckout).not.toHaveBeenCalled() }) it('blocks payment when the primary or additional driver license requires review', async () => { vi.mocked(validateLicense).mockReturnValue({ status: 'EXPIRING_SOON', requiresApproval: true } as never) vi.mocked(repo.findReservationForPayment).mockResolvedValue({ id: 'reservation_1', paymentStatus: 'UNPAID', totalAmount: 400, customer: { licenseExpiry: new Date('2026-07-01'), licenseValidationStatus: 'PENDING' }, additionalDrivers: [], vehicle: { make: 'Dacia', model: 'Duster' }, } as never) await expect(service.initPayment('atlas', 'reservation_1', { provider: 'PAYPAL', accessToken: 'booking-access-token-123', successUrl: 'https://example.test/success', failureUrl: 'https://example.test/failure', })).rejects.toMatchObject({ statusCode: 409, error: 'license_review_required' }) expect(paypal.createOrder).not.toHaveBeenCalled() }) it('routes public contact messages to the brand public email when configured', async () => { await expect(service.handleContact('atlas', { name: 'Visitor', email: 'visitor@example.test', message: 'Hi' })).resolves.toEqual({ success: true, deliveredTo: 'hello@example.test', preview: { name: 'Visitor', email: 'visitor@example.test', message: 'Hi' }, }) }) })