import { describe, it, expect, vi, beforeEach } from 'vitest' import { AppError, NotFoundError } from '../../http/errors' 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('./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(), })) vi.mock('../../services/vehicleAvailabilityService', () => ({ getVehicleAvailabilitySummary: vi.fn(), })) vi.mock('../../services/insuranceService', () => ({ applyInsurancesToReservation: vi.fn().mockResolvedValue(undefined), })) vi.mock('../../services/additionalDriverService', () => ({ applyAdditionalDriversToReservation: vi.fn().mockResolvedValue(undefined), })) 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(), })) vi.mock('../../services/paypalService', () => ({ isConfigured: vi.fn(), createOrder: vi.fn(), captureOrder: vi.fn(), })) vi.mock('../../services/platformContentService', () => ({ getMarketplaceHomepageContent: vi.fn(), })) import * as repo from './site.repo' import { getVehicleAvailabilitySummary } from '../../services/vehicleAvailabilityService' import { applyPricingRules } from '../../services/pricingRuleService' import { validateLicense } from '../../services/licenseValidationService' import * as amanpay from '../../services/amanpayService' import * as paypalSvc from '../../services/paypalService' import { getBrand, getPublicVehicles, checkAvailability, validatePromoCode, createBooking, initPayment, } from './site.service' const SLUG = 'test-company' function makeCompany(overrides: object = {}) { return { id: 'co-1', slug: SLUG, name: 'Test Co', phone: null, email: 'co@test.com', brand: { publicEmail: null }, contractSettings: null, ...overrides, } } function makeVehicle(overrides: object = {}) { return { id: 'v-1', dailyRate: 10000, year: 2022, make: 'Toyota', model: 'Camry', isPublished: true, status: 'AVAILABLE', companyId: 'co-1', category: 'SEDAN', ...overrides, } } 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) }) // ──────────────────────────────────────────────────────────────────────────── describe('getBrand', () => { it('returns company and public brand data only', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany({ brand: { displayName: 'Test Co', paymentMethodsEnabled: ['AMANPAY', 'PAYPAL'], amanpayMerchantId: 'merchant-id', amanpaySecretKey: 'top-secret', paypalMerchantId: 'paypal-merchant-id', }, }) as any) const result = await getBrand(SLUG) expect(result.company.id).toBe('co-1') expect(result.brand).toMatchObject({ displayName: 'Test Co', paymentMethodsEnabled: ['AMANPAY', 'PAYPAL'], paypalMerchantId: 'paypal-merchant-id', }) expect(result.brand).not.toHaveProperty('amanpayMerchantId') expect(result.brand).not.toHaveProperty('amanpaySecretKey') }) }) // ──────────────────────────────────────────────────────────────────────────── describe('getPublicVehicles', () => { it('returns vehicles enriched with availability', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findPublishedVehicles).mockResolvedValue([makeVehicle()] as any) vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null }) const result = await getPublicVehicles(SLUG) expect(result[0].availability).toBe(true) }) }) // ──────────────────────────────────────────────────────────────────────────── describe('checkAvailability', () => { it('returns availability result for given date range', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findVehicleById).mockResolvedValue(makeVehicle() as any) vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null }) const result = await checkAvailability(SLUG, 'v-1', '2025-06-01T00:00:00.000Z', '2025-06-04T00:00:00.000Z') expect(result.available).toBe(true) expect(getVehicleAvailabilitySummary).toHaveBeenCalledWith('v-1', { companyId: 'co-1', range: { startDate: new Date('2025-06-01T00:00:00.000Z'), endDate: new Date('2025-06-04T00:00:00.000Z') } }) }) }) // ──────────────────────────────────────────────────────────────────────────── describe('validatePromoCode', () => { it('returns offer when code is valid', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findOfferByPromoCode).mockResolvedValue({ id: 'o-1' } as any) const result = await validatePromoCode(SLUG, 'SUMMER10') expect((result as any).id).toBe('o-1') }) it('throws NotFoundError for unknown promo code', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findOfferByPromoCode).mockResolvedValue(null) await expect(validatePromoCode(SLUG, 'BADCODE')).rejects.toBeInstanceOf(NotFoundError) }) }) // ──────────────────────────────────────────────────────────────────────────── describe('createBooking', () => { const baseBody = { vehicleId: 'v-1', startDate: '2026-07-01T00:00:00.000Z', endDate: '2026-07-04T00:00:00.000Z', firstName: 'Ali', lastName: 'Ben', email: 'ali@test.com', phone: '+212600000000', pickupLocation: 'Casablanca', returnLocation: 'Casablanca', } beforeEach(() => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findVehicleById).mockResolvedValue(makeVehicle() as any) vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: true, status: 'AVAILABLE', nextAvailableAt: null, blockingReason: null }) vi.mocked(repo.upsertCustomer).mockResolvedValue({ id: 'c-1' } as any) vi.mocked(applyPricingRules).mockResolvedValue({ applied: [], total: 0 }) vi.mocked(repo.createReservation).mockResolvedValue({ id: 'r-1' } as any) vi.mocked(repo.findReservationWithDetails).mockResolvedValue({ id: 'r-1', bookingReference: 'BK-2026-ABC123', status: 'DRAFT', startDate: new Date('2026-07-01T00:00:00.000Z'), endDate: new Date('2026-07-04T00:00:00.000Z'), pickupLocation: 'Casablanca', returnLocation: 'Casablanca', vehicle: { make: 'Toyota', model: 'Camry', year: 2022 }, customer: { firstName: 'Ali', lastName: 'Ben', email: 'ali@test.com', phone: '+212600000000' }, company: { name: 'Test Co', brand: { displayName: 'Test Co' } }, additionalDrivers: [], insurances: [], totalAmount: 30000, paidAmount: 0, paymentStatus: 'UNPAID', } as any) }) it('creates a booking and returns a pending guest confirmation', async () => { const result = await createBooking(SLUG, baseBody) expect(repo.createReservation).toHaveBeenCalledOnce() expect((result as any).status).toBe('PENDING') expect((result as any).bookingReference).toBe('BK-2026-ABC123') }) it('throws AppError for invalid dates', async () => { await expect( createBooking(SLUG, { ...baseBody, startDate: '2026-07-04T00:00:00.000Z', endDate: '2026-07-01T00:00:00.000Z' }), ).rejects.toMatchObject({ error: 'invalid_dates' }) }) it('throws AppError when vehicle unavailable', async () => { vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: false, status: 'RESERVED', nextAvailableAt: null, blockingReason: 'RESERVATION' }) await expect(createBooking(SLUG, baseBody)).rejects.toMatchObject({ error: 'unavailable' }) }) }) // ──────────────────────────────────────────────────────────────────────────── describe('initPayment — payment guard paths', () => { const payBody = { provider: 'PAYPAL' as const, successUrl: 'http://localhost:3000/ok', failureUrl: 'http://localhost:3000/fail', accessToken: 'booking-access-token-123' } it('throws AppError when reservation is already paid', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findReservationForPayment).mockResolvedValue({ paymentStatus: 'PAID', totalAmount: 100, customer: { licenseExpiry: null, licenseValidationStatus: 'APPROVED', email: 'a@b.com', firstName: 'A', lastName: 'B' }, additionalDrivers: [], vehicle: { make: 'Toyota', model: 'Camry' }, } as any) vi.mocked(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false } as any) await expect(initPayment(SLUG, 'r-1', payBody)).rejects.toMatchObject({ error: 'already_paid' }) }) it('throws AppError when license review is required', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findReservationForPayment).mockResolvedValue({ paymentStatus: 'UNPAID', totalAmount: 100, customer: { licenseExpiry: null, licenseValidationStatus: 'DENIED', email: 'a@b.com', firstName: 'A', lastName: 'B' }, additionalDrivers: [], vehicle: { make: 'Toyota', model: 'Camry' }, } as any) vi.mocked(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false } as any) await expect(initPayment(SLUG, 'r-1', payBody)).rejects.toMatchObject({ error: 'license_review_required' }) }) it('throws AppError when PayPal is not configured', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findReservationForPayment).mockResolvedValue({ paymentStatus: 'UNPAID', totalAmount: 30000, customer: { licenseExpiry: null, licenseValidationStatus: 'APPROVED', email: 'a@b.com', firstName: 'Ali', lastName: 'Ben' }, additionalDrivers: [], vehicle: { make: 'Toyota', model: 'Camry' }, } as any) vi.mocked(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false } as any) vi.mocked(paypalSvc.isConfigured).mockReturnValue(false) await expect(initPayment(SLUG, 'r-1', payBody)).rejects.toMatchObject({ error: 'provider_not_configured' }) }) it('returns checkoutUrl when PayPal is configured', async () => { vi.mocked(repo.findCompanyBySlug).mockResolvedValue(makeCompany() as any) vi.mocked(repo.findReservationForPayment).mockResolvedValue({ id: 'r-1', paymentStatus: 'UNPAID', totalAmount: 30000, companyId: 'co-1', customer: { licenseExpiry: null, licenseValidationStatus: 'APPROVED', email: 'a@b.com', firstName: 'Ali', lastName: 'Ben' }, additionalDrivers: [], vehicle: { make: 'Toyota', model: 'Camry' }, } as any) vi.mocked(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false } as any) vi.mocked(paypalSvc.isConfigured).mockReturnValue(true) vi.mocked(paypalSvc.createOrder).mockResolvedValue({ approveUrl: 'https://paypal.com/approve', orderId: 'pp-1' } as any) vi.mocked(repo.createRentalPayment).mockResolvedValue(undefined as any) const result = await initPayment(SLUG, 'r-1', payBody) expect(result.checkoutUrl).toBe('https://paypal.com/approve') expect(repo.createRentalPayment).toHaveBeenCalledOnce() }) })