refractor code,
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { AppError } from '../../http/errors'
|
||||
|
||||
// ── repo mock ──────────────────────────────────────────────────────────────
|
||||
vi.mock('./reservation.repo', () => ({
|
||||
findMany: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
findByIdSimple: vi.fn(),
|
||||
findByIdWithRelations: vi.fn(),
|
||||
findByIdForCheckout: vi.fn(),
|
||||
findForLicenseCheck: vi.fn(),
|
||||
findForClose: vi.fn(),
|
||||
findForInspection: vi.fn(),
|
||||
findConflict: vi.fn(),
|
||||
create: vi.fn(),
|
||||
updateById: vi.fn(),
|
||||
findActiveOffer: vi.fn(),
|
||||
incrementOfferRedemption: vi.fn(),
|
||||
findVehicle: vi.fn(),
|
||||
findCustomer: vi.fn(),
|
||||
findCustomerById: vi.fn(),
|
||||
updateVehicleStatus: vi.fn(),
|
||||
findCompanyWithBrand: vi.fn(),
|
||||
findBrandLocale: vi.fn(),
|
||||
findInspections: vi.fn(),
|
||||
findAdditionalDriver: vi.fn(),
|
||||
updateAdditionalDriver: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('./reservation.pricing.service', () => ({
|
||||
applyPricingRules: vi.fn(),
|
||||
calculateUpdatedInsuranceCharge: vi.fn(),
|
||||
calculateUpdatedAdditionalDriverCharge: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('./reservation.insurance.service', () => ({
|
||||
applyInsurancesToReservation: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('./reservation.additional-driver.service', () => ({
|
||||
applyAdditionalDriversToReservation: vi.fn(),
|
||||
approveAdditionalDriver: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../services/licenseValidationService', () => ({
|
||||
validateAndFlagLicense: vi.fn().mockResolvedValue(undefined),
|
||||
validateLicense: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../services/notificationService', () => ({
|
||||
sendNotification: vi.fn().mockResolvedValue(undefined),
|
||||
sendTransactionalEmail: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock('../../lib/prisma', () => ({
|
||||
prisma: {
|
||||
reservation: { update: vi.fn() },
|
||||
damageInspection: { upsert: vi.fn() },
|
||||
damageReport: { upsert: vi.fn() },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./reservation.presenter', () => ({
|
||||
serializeReservationForDashboard: vi.fn((r) => r),
|
||||
parseReservationExtras: vi.fn(() => ({})),
|
||||
buildReservationWorkflow: vi.fn(),
|
||||
normalizeOptionalString: vi.fn((s) => s),
|
||||
}))
|
||||
|
||||
import * as repo from './reservation.repo'
|
||||
import * as pricingService from './reservation.pricing.service'
|
||||
import * as insuranceService from './reservation.insurance.service'
|
||||
import * as additionalDriverService from './reservation.additional-driver.service'
|
||||
import { validateLicense } from '../../services/licenseValidationService'
|
||||
import { buildReservationWorkflow } from './reservation.presenter'
|
||||
import { createReservation } from './reservation.service'
|
||||
import { confirmReservation, checkinReservation, checkoutReservation, closeReservation } from './reservation.lifecycle.service'
|
||||
import { approveAdditionalDriver } from './reservation.additional-driver.service'
|
||||
|
||||
const COMPANY = 'company-1'
|
||||
const RES_ID = 'reservation-1'
|
||||
|
||||
function makeVehicle(overrides: object = {}) {
|
||||
return { id: 'vehicle-1', dailyRate: 100, status: 'AVAILABLE', isPublished: true, mileage: 50000, ...overrides }
|
||||
}
|
||||
|
||||
function makeReservation(overrides: object = {}) {
|
||||
return {
|
||||
id: RES_ID,
|
||||
companyId: COMPANY,
|
||||
vehicleId: 'vehicle-1',
|
||||
customerId: 'customer-1',
|
||||
status: 'DRAFT',
|
||||
startDate: new Date('2025-06-01'),
|
||||
endDate: new Date('2025-06-04'),
|
||||
totalDays: 3,
|
||||
dailyRate: 100,
|
||||
depositAmount: 0,
|
||||
discountAmount: 0,
|
||||
totalAmount: 300,
|
||||
additionalDrivers: [],
|
||||
insurances: [],
|
||||
customer: { firstName: 'Ali', lastName: 'Ben', email: 'ali@test.com', licenseExpiry: null, licenseValidationStatus: 'APPROVED' },
|
||||
vehicle: { year: 2022, make: 'Toyota', model: 'Camry' },
|
||||
extras: {},
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('createReservation', () => {
|
||||
it('creates a reservation and returns it', async () => {
|
||||
vi.mocked(repo.findVehicle).mockResolvedValue(makeVehicle())
|
||||
vi.mocked(repo.findCustomer).mockResolvedValue({ id: 'customer-1' } as any)
|
||||
vi.mocked(repo.findConflict).mockResolvedValue(null)
|
||||
vi.mocked(repo.findActiveOffer).mockResolvedValue(null)
|
||||
vi.mocked(pricingService.applyPricingRules).mockResolvedValue({ applied: [], total: 0 })
|
||||
vi.mocked(repo.create).mockResolvedValue(makeReservation() as any)
|
||||
|
||||
const result = await createReservation(COMPANY, {
|
||||
vehicleId: 'vehicle-1',
|
||||
customerId: 'customer-1',
|
||||
startDate: '2025-06-01T00:00:00.000Z',
|
||||
endDate: '2025-06-04T00:00:00.000Z',
|
||||
depositAmount: 0,
|
||||
selectedInsurancePolicyIds: [],
|
||||
additionalDrivers: [],
|
||||
})
|
||||
|
||||
expect(repo.create).toHaveBeenCalledOnce()
|
||||
expect(result.id).toBe(RES_ID)
|
||||
})
|
||||
|
||||
it('throws conflict error when vehicle is unavailable', async () => {
|
||||
vi.mocked(repo.findVehicle).mockResolvedValue(makeVehicle())
|
||||
vi.mocked(repo.findCustomer).mockResolvedValue({ id: 'customer-1' } as any)
|
||||
vi.mocked(repo.findConflict).mockResolvedValue({ id: 'other-res' } as any)
|
||||
|
||||
await expect(
|
||||
createReservation(COMPANY, {
|
||||
vehicleId: 'vehicle-1',
|
||||
customerId: 'customer-1',
|
||||
startDate: '2025-06-01T00:00:00.000Z',
|
||||
endDate: '2025-06-04T00:00:00.000Z',
|
||||
selectedInsurancePolicyIds: [],
|
||||
additionalDrivers: [],
|
||||
}),
|
||||
).rejects.toThrow('Vehicle is not available for the selected dates')
|
||||
|
||||
expect(repo.create).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('confirmReservation', () => {
|
||||
it('confirms a DRAFT reservation', async () => {
|
||||
const res = makeReservation({ status: 'DRAFT' })
|
||||
vi.mocked(repo.findByIdSimple).mockResolvedValue(res as any)
|
||||
vi.mocked(repo.findForLicenseCheck).mockResolvedValue({
|
||||
...res,
|
||||
customer: { ...res.customer, licenseExpiry: null, licenseValidationStatus: 'APPROVED' },
|
||||
additionalDrivers: [],
|
||||
} as any)
|
||||
vi.mocked(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false } as any)
|
||||
vi.mocked(repo.updateById).mockResolvedValue({ ...res, status: 'CONFIRMED' } as any)
|
||||
vi.mocked(repo.findCustomerById).mockResolvedValue({ email: 'ali@test.com', firstName: 'Ali' } as any)
|
||||
vi.mocked(repo.findBrandLocale).mockResolvedValue({ defaultLocale: 'fr' } as any)
|
||||
|
||||
const result = await confirmReservation(RES_ID, COMPANY)
|
||||
|
||||
expect(repo.updateById).toHaveBeenCalledWith(RES_ID, { status: 'CONFIRMED' })
|
||||
expect((result as any).status).toBe('CONFIRMED')
|
||||
})
|
||||
|
||||
it('rejects non-DRAFT reservation', async () => {
|
||||
vi.mocked(repo.findByIdSimple).mockResolvedValue(makeReservation({ status: 'CONFIRMED' }) as any)
|
||||
|
||||
await expect(confirmReservation(RES_ID, COMPANY)).rejects.toThrow('Only DRAFT reservations can be confirmed')
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('checkinReservation', () => {
|
||||
it('transitions CONFIRMED → ACTIVE and marks vehicle RENTED', async () => {
|
||||
const res = makeReservation({ status: 'CONFIRMED' })
|
||||
vi.mocked(repo.findByIdSimple).mockResolvedValue(res as any)
|
||||
vi.mocked(repo.findForLicenseCheck).mockResolvedValue({
|
||||
...res,
|
||||
customer: { ...res.customer, licenseExpiry: null, licenseValidationStatus: 'APPROVED' },
|
||||
additionalDrivers: [],
|
||||
} as any)
|
||||
vi.mocked(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false } as any)
|
||||
vi.mocked(repo.updateById).mockResolvedValue({ ...res, status: 'ACTIVE' } as any)
|
||||
vi.mocked(repo.updateVehicleStatus).mockResolvedValue(undefined as any)
|
||||
|
||||
await checkinReservation(RES_ID, COMPANY, 55000)
|
||||
|
||||
expect(repo.updateById).toHaveBeenCalledWith(RES_ID, expect.objectContaining({ status: 'ACTIVE', checkInMileage: 55000 }))
|
||||
expect(repo.updateVehicleStatus).toHaveBeenCalledWith('vehicle-1', 'RENTED')
|
||||
})
|
||||
|
||||
it('rejects non-CONFIRMED reservation', async () => {
|
||||
vi.mocked(repo.findByIdSimple).mockResolvedValue(makeReservation({ status: 'ACTIVE' }) as any)
|
||||
|
||||
await expect(checkinReservation(RES_ID, COMPANY)).rejects.toThrow('Only CONFIRMED reservations can be checked in')
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('checkoutReservation', () => {
|
||||
it('transitions ACTIVE → COMPLETED and marks vehicle AVAILABLE', async () => {
|
||||
const res = makeReservation({ status: 'ACTIVE' })
|
||||
vi.mocked(repo.findByIdForCheckout).mockResolvedValue(res as any)
|
||||
vi.mocked(repo.updateById).mockResolvedValue({ ...res, status: 'COMPLETED' } as any)
|
||||
vi.mocked(repo.updateVehicleStatus).mockResolvedValue(undefined as any)
|
||||
vi.mocked(repo.findCustomerById).mockResolvedValue({ email: 'ali@test.com', firstName: 'Ali' } as any)
|
||||
vi.mocked(repo.findCompanyWithBrand).mockResolvedValue({ name: 'TestCo', brand: { displayName: 'TestCo', defaultLocale: 'fr' } } as any)
|
||||
|
||||
await checkoutReservation(RES_ID, COMPANY, 58000)
|
||||
|
||||
expect(repo.updateById).toHaveBeenCalledWith(RES_ID, expect.objectContaining({ status: 'COMPLETED', checkOutMileage: 58000 }))
|
||||
expect(repo.updateVehicleStatus).toHaveBeenCalledWith('vehicle-1', 'AVAILABLE', 58000)
|
||||
})
|
||||
|
||||
it('rejects non-ACTIVE reservation', async () => {
|
||||
vi.mocked(repo.findByIdForCheckout).mockResolvedValue(makeReservation({ status: 'CONFIRMED' }) as any)
|
||||
|
||||
await expect(checkoutReservation(RES_ID, COMPANY)).rejects.toThrow('Only ACTIVE reservations can be checked out')
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('closeReservation', () => {
|
||||
it('closes a COMPLETED reservation', async () => {
|
||||
const res = makeReservation({ status: 'COMPLETED' })
|
||||
vi.mocked(repo.findForClose).mockResolvedValue(res as any)
|
||||
vi.mocked(buildReservationWorkflow).mockReturnValue({ closed: false } as any)
|
||||
const { prisma } = await import('../../lib/prisma')
|
||||
vi.mocked(prisma.reservation.update).mockResolvedValue({ ...res, extras: { reservationClosedBy: 'Admin User' } } as any)
|
||||
|
||||
await closeReservation(RES_ID, COMPANY, 'Admin User')
|
||||
|
||||
expect(prisma.reservation.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { id: RES_ID } }),
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects already closed reservation', async () => {
|
||||
vi.mocked(repo.findForClose).mockResolvedValue(makeReservation({ status: 'COMPLETED' }) as any)
|
||||
vi.mocked(buildReservationWorkflow).mockReturnValue({ closed: true } as any)
|
||||
|
||||
await expect(closeReservation(RES_ID, COMPANY, 'Admin')).rejects.toThrow('already closed')
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('approveAdditionalDriver', () => {
|
||||
it('calls through to the additional driver service', async () => {
|
||||
vi.mocked(additionalDriverService.approveAdditionalDriver).mockResolvedValue({ id: 'driver-1', approvedAt: new Date() } as any)
|
||||
|
||||
const result = await approveAdditionalDriver(RES_ID, 'driver-1', COMPANY, true, 'Looks good', 'Manager')
|
||||
|
||||
expect(additionalDriverService.approveAdditionalDriver).toHaveBeenCalledWith(RES_ID, 'driver-1', COMPANY, true, 'Looks good', 'Manager')
|
||||
expect((result as any).id).toBe('driver-1')
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('pricing rule application', () => {
|
||||
it('applies pricing rules and adjusts totalAmount', async () => {
|
||||
vi.mocked(repo.findVehicle).mockResolvedValue(makeVehicle({ dailyRate: 100 }))
|
||||
vi.mocked(repo.findCustomer).mockResolvedValue({ id: 'customer-1' } as any)
|
||||
vi.mocked(repo.findConflict).mockResolvedValue(null)
|
||||
vi.mocked(repo.findActiveOffer).mockResolvedValue(null)
|
||||
vi.mocked(pricingService.applyPricingRules).mockResolvedValue({ applied: [{ id: 'rule-1' }], total: 50 })
|
||||
vi.mocked(repo.create).mockResolvedValue(makeReservation({ totalAmount: 350 }) as any)
|
||||
|
||||
await createReservation(COMPANY, {
|
||||
vehicleId: 'vehicle-1',
|
||||
customerId: 'customer-1',
|
||||
startDate: '2025-06-01T00:00:00.000Z',
|
||||
endDate: '2025-06-04T00:00:00.000Z',
|
||||
selectedInsurancePolicyIds: [],
|
||||
additionalDrivers: [],
|
||||
})
|
||||
|
||||
expect(pricingService.applyPricingRules).toHaveBeenCalledOnce()
|
||||
const createCall = vi.mocked(repo.create).mock.calls[0][0] as any
|
||||
expect(createCall.pricingRulesApplied).toEqual([{ id: 'rule-1' }])
|
||||
expect(createCall.totalAmount).toBe(350)
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
describe('insurance application', () => {
|
||||
it('calls applyInsurancesToReservation when insurance IDs are provided', async () => {
|
||||
vi.mocked(repo.findVehicle).mockResolvedValue(makeVehicle())
|
||||
vi.mocked(repo.findCustomer).mockResolvedValue({ id: 'customer-1' } as any)
|
||||
vi.mocked(repo.findConflict).mockResolvedValue(null)
|
||||
vi.mocked(repo.findActiveOffer).mockResolvedValue(null)
|
||||
vi.mocked(pricingService.applyPricingRules).mockResolvedValue({ applied: [], total: 0 })
|
||||
vi.mocked(repo.create).mockResolvedValue(makeReservation() as any)
|
||||
vi.mocked(insuranceService.applyInsurancesToReservation).mockResolvedValue(undefined as any)
|
||||
|
||||
await createReservation(COMPANY, {
|
||||
vehicleId: 'vehicle-1',
|
||||
customerId: 'customer-1',
|
||||
startDate: '2025-06-01T00:00:00.000Z',
|
||||
endDate: '2025-06-04T00:00:00.000Z',
|
||||
selectedInsurancePolicyIds: ['policy-1', 'policy-2'],
|
||||
additionalDrivers: [],
|
||||
})
|
||||
|
||||
expect(insuranceService.applyInsurancesToReservation).toHaveBeenCalledWith(
|
||||
RES_ID, COMPANY, ['policy-1', 'policy-2'], 3, 300,
|
||||
)
|
||||
})
|
||||
|
||||
it('skips insurance call when no IDs provided', async () => {
|
||||
vi.mocked(repo.findVehicle).mockResolvedValue(makeVehicle())
|
||||
vi.mocked(repo.findCustomer).mockResolvedValue({ id: 'customer-1' } as any)
|
||||
vi.mocked(repo.findConflict).mockResolvedValue(null)
|
||||
vi.mocked(repo.findActiveOffer).mockResolvedValue(null)
|
||||
vi.mocked(pricingService.applyPricingRules).mockResolvedValue({ applied: [], total: 0 })
|
||||
vi.mocked(repo.create).mockResolvedValue(makeReservation() as any)
|
||||
|
||||
await createReservation(COMPANY, {
|
||||
vehicleId: 'vehicle-1',
|
||||
customerId: 'customer-1',
|
||||
startDate: '2025-06-01T00:00:00.000Z',
|
||||
endDate: '2025-06-04T00:00:00.000Z',
|
||||
selectedInsurancePolicyIds: [],
|
||||
additionalDrivers: [],
|
||||
})
|
||||
|
||||
expect(insuranceService.applyInsurancesToReservation).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user