fix architecture and write new tests
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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', () => ({ getMarketplaceHomepageContent: 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 })) }))
|
||||
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<string, unknown> = {}) {
|
||||
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',
|
||||
driverLicense: 'DL-1',
|
||||
dateOfBirth: '1992-01-01',
|
||||
licenseExpiry: '2027-01-01',
|
||||
licenseIssuedAt: '2022-01-01',
|
||||
nationality: 'MA',
|
||||
identityDocumentNumber: 'ID-1',
|
||||
fullAddress: 'Casablanca',
|
||||
licenseCountry: 'MA',
|
||||
licenseCategory: 'B',
|
||||
...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', additionalDrivers: [{ requiresApproval: true }] } 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,
|
||||
status: 'DRAFT',
|
||||
source: 'PUBLIC_SITE',
|
||||
}))
|
||||
expect(applyInsurancesToReservation).toHaveBeenCalledWith('reservation_1', 'company_1', ['insurance_1'], 3, 1500)
|
||||
expect(applyAdditionalDriversToReservation).toHaveBeenCalledWith('reservation_1', 'company_1', [{ firstName: 'Second' }], 3)
|
||||
expect(validateAndFlagLicense).toHaveBeenCalledWith('customer_1')
|
||||
expect(result.requiresManualApproval).toBe(true)
|
||||
})
|
||||
|
||||
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' },
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user