reservation implementation
This commit is contained in:
@@ -53,9 +53,13 @@ function maskPhone(phone?: string | null) {
|
||||
export function presentPublicBooking(reservation: any) {
|
||||
return {
|
||||
id: reservation.id,
|
||||
status: reservation.status,
|
||||
bookingReference: reservation.bookingReference ?? null,
|
||||
status: reservation.status === 'DRAFT' ? 'PENDING' : reservation.status,
|
||||
companyName: reservation.company?.brand?.displayName ?? reservation.company?.name ?? null,
|
||||
pickupAt: reservation.startDate instanceof Date ? reservation.startDate.toISOString() : reservation.startDate,
|
||||
returnAt: reservation.endDate instanceof Date ? reservation.endDate.toISOString() : reservation.endDate,
|
||||
pickupLocation: reservation.pickupLocation ?? null,
|
||||
returnLocation: reservation.returnLocation ?? null,
|
||||
vehicle: {
|
||||
make: reservation.vehicle?.make,
|
||||
model: reservation.vehicle?.model,
|
||||
@@ -72,5 +76,6 @@ export function presentPublicBooking(reservation: any) {
|
||||
currency: 'MAD',
|
||||
},
|
||||
paymentStatus: reservation.paymentStatus,
|
||||
message: 'The company will review your request.',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,46 +45,53 @@ export async function upsertCustomer(companyId: string, data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
phone: string
|
||||
driverLicense: string
|
||||
dateOfBirth: string
|
||||
licenseExpiry: string
|
||||
licenseIssuedAt: string
|
||||
nationality: string
|
||||
identityDocumentNumber: string
|
||||
fullAddress: string
|
||||
licenseCountry: string
|
||||
driverLicense?: string
|
||||
dateOfBirth?: string
|
||||
licenseExpiry?: string
|
||||
licenseIssuedAt?: string
|
||||
nationality?: string
|
||||
identityDocumentNumber?: string
|
||||
fullAddress?: string
|
||||
licenseCountry?: string
|
||||
licenseNumber?: string | null
|
||||
licenseCategory: string
|
||||
licenseCategory?: string
|
||||
internationalLicenseNumber?: string | null
|
||||
}) {
|
||||
const normalizedEmail = data.email.trim().toLowerCase()
|
||||
const existing = await prisma.customer.findUnique({
|
||||
where: { companyId_email: { companyId, email: data.email } },
|
||||
where: { companyId_email: { companyId, email: normalizedEmail } },
|
||||
select: { id: true, address: true },
|
||||
})
|
||||
|
||||
const address = {
|
||||
...(existing?.address && typeof existing.address === 'object' && !Array.isArray(existing.address)
|
||||
? existing.address as Record<string, unknown>
|
||||
: {}),
|
||||
fullAddress: data.fullAddress,
|
||||
identityDocumentNumber: data.identityDocumentNumber,
|
||||
internationalLicenseNumber: data.internationalLicenseNumber ?? null,
|
||||
const addressPatch = {
|
||||
...(data.fullAddress ? { fullAddress: data.fullAddress } : {}),
|
||||
...(data.identityDocumentNumber ? { identityDocumentNumber: data.identityDocumentNumber } : {}),
|
||||
...(data.internationalLicenseNumber !== undefined ? { internationalLicenseNumber: data.internationalLicenseNumber ?? null } : {}),
|
||||
}
|
||||
|
||||
const address = Object.keys(addressPatch).length > 0
|
||||
? {
|
||||
...(existing?.address && typeof existing.address === 'object' && !Array.isArray(existing.address)
|
||||
? existing.address as Record<string, unknown>
|
||||
: {}),
|
||||
...addressPatch,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const payload = {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
email: normalizedEmail,
|
||||
phone: data.phone,
|
||||
driverLicense: data.driverLicense,
|
||||
dateOfBirth: new Date(data.dateOfBirth),
|
||||
nationality: data.nationality,
|
||||
address,
|
||||
licenseExpiry: new Date(data.licenseExpiry),
|
||||
licenseIssuedAt: new Date(data.licenseIssuedAt),
|
||||
licenseCountry: data.licenseCountry,
|
||||
licenseNumber: data.licenseNumber ?? data.driverLicense,
|
||||
licenseCategory: data.licenseCategory,
|
||||
...(data.driverLicense ? { driverLicense: data.driverLicense } : {}),
|
||||
...(data.dateOfBirth ? { dateOfBirth: new Date(data.dateOfBirth) } : {}),
|
||||
...(data.nationality ? { nationality: data.nationality } : {}),
|
||||
...(address ? { address } : {}),
|
||||
...(data.licenseExpiry ? { licenseExpiry: new Date(data.licenseExpiry) } : {}),
|
||||
...(data.licenseIssuedAt ? { licenseIssuedAt: new Date(data.licenseIssuedAt) } : {}),
|
||||
...(data.licenseCountry ? { licenseCountry: data.licenseCountry } : {}),
|
||||
...((data.licenseNumber ?? data.driverLicense) ? { licenseNumber: data.licenseNumber ?? data.driverLicense } : {}),
|
||||
...(data.licenseCategory ? { licenseCategory: data.licenseCategory } : {}),
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
@@ -106,14 +113,14 @@ export async function createReservation(data: object) {
|
||||
export async function findReservationWithDetails(reservationId: string) {
|
||||
return prisma.reservation.findUniqueOrThrow({
|
||||
where: { id: reservationId },
|
||||
include: { insurances: true, additionalDrivers: true },
|
||||
include: { insurances: true, additionalDrivers: true, vehicle: true, customer: true, company: { include: { brand: true } } },
|
||||
})
|
||||
}
|
||||
|
||||
export async function findBooking(reservationId: string, companyId: string) {
|
||||
return prisma.reservation.findFirstOrThrow({
|
||||
where: { id: reservationId, companyId },
|
||||
include: { vehicle: true, customer: true },
|
||||
include: { vehicle: true, customer: true, company: { include: { brand: true } } },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,15 +10,8 @@ describe('site.schemas', () => {
|
||||
lastName: 'Benali',
|
||||
email: 'aya@example.com',
|
||||
phone: '+212600000000',
|
||||
driverLicense: 'DL-123',
|
||||
dateOfBirth: '1995-01-01T00:00:00.000Z',
|
||||
licenseExpiry: '2028-01-01T00:00:00.000Z',
|
||||
licenseIssuedAt: '2020-01-01T00:00:00.000Z',
|
||||
nationality: 'Moroccan',
|
||||
identityDocumentNumber: 'ID-123',
|
||||
fullAddress: '12 Main Street',
|
||||
licenseCountry: 'MA',
|
||||
licenseCategory: 'B',
|
||||
pickupLocation: 'Casablanca Airport',
|
||||
returnLocation: 'Casablanca Airport',
|
||||
}
|
||||
|
||||
it('defaults optional public booking arrays and source', () => {
|
||||
@@ -53,7 +46,7 @@ describe('site.schemas', () => {
|
||||
expect(paySchema.parse({ provider: 'PAYPAL', successUrl: 'https://ok.test', failureUrl: 'https://fail.test', accessToken: 'booking-access-token-123' }).currency).toBe('MAD')
|
||||
})
|
||||
|
||||
it('keeps contact, PayPal capture, and required identity fields strict', () => {
|
||||
it('keeps contact, PayPal capture, and guest booking essentials strict', () => {
|
||||
expect(contactSchema.parse({ name: 'Aya', email: 'aya@example.com', message: 'Hello' })).toEqual({
|
||||
name: 'Aya', email: 'aya@example.com', message: 'Hello',
|
||||
})
|
||||
|
||||
@@ -19,16 +19,18 @@ export const bookSchema = z.object({
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
phone: z.string().min(1).max(30),
|
||||
driverLicense: z.string().min(1).max(50),
|
||||
dateOfBirth: z.string().datetime(),
|
||||
licenseExpiry: z.string().datetime(),
|
||||
licenseIssuedAt: z.string().datetime(),
|
||||
nationality: z.string().min(1).max(100),
|
||||
identityDocumentNumber: z.string().min(1).max(100),
|
||||
fullAddress: z.string().min(1).max(500),
|
||||
licenseCountry: z.string().min(1).max(100),
|
||||
pickupLocation: z.string().trim().min(1).max(100),
|
||||
returnLocation: z.string().trim().min(1).max(100),
|
||||
driverLicense: z.string().min(1).max(50).optional(),
|
||||
dateOfBirth: z.string().datetime().optional(),
|
||||
licenseExpiry: z.string().datetime().optional(),
|
||||
licenseIssuedAt: z.string().datetime().optional(),
|
||||
nationality: z.string().min(1).max(100).optional(),
|
||||
identityDocumentNumber: z.string().min(1).max(100).optional(),
|
||||
fullAddress: z.string().min(1).max(500).optional(),
|
||||
licenseCountry: z.string().min(1).max(100).optional(),
|
||||
licenseNumber: z.string().min(1).max(50).optional(),
|
||||
licenseCategory: z.string().min(1).max(20),
|
||||
licenseCategory: z.string().min(1).max(20).optional(),
|
||||
internationalLicenseNumber: z.string().trim().max(100).optional(),
|
||||
offerId: z.string().cuid().optional(),
|
||||
promoCodeUsed: z.string().optional(),
|
||||
|
||||
@@ -18,7 +18,13 @@ 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.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(),
|
||||
@@ -62,15 +68,8 @@ function bookingBody(overrides: Record<string, unknown> = {}) {
|
||||
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',
|
||||
pickupLocation: 'Casablanca',
|
||||
returnLocation: 'Casablanca',
|
||||
...overrides,
|
||||
} as any
|
||||
}
|
||||
@@ -108,7 +107,23 @@ describe('site.service public booking/payment boundaries', () => {
|
||||
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)
|
||||
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',
|
||||
@@ -122,13 +137,16 @@ describe('site.service public booking/payment boundaries', () => {
|
||||
discountAmount: 500,
|
||||
pricingRulesTotal: 90,
|
||||
totalAmount: 1090,
|
||||
pickupLocation: 'Casablanca',
|
||||
returnLocation: 'Casablanca',
|
||||
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)
|
||||
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 () => {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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 { validateLicense } from '../../services/licenseValidationService'
|
||||
import { getMarketplaceHomepageContent } from '../../services/platformContentService'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import * as amanpay from '../../services/amanpayService'
|
||||
@@ -48,6 +46,12 @@ function assertAllowedPaymentRedirect(urlValue: string, company: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function generateBookingReference() {
|
||||
const year = new Date().getUTCFullYear()
|
||||
const random = Math.random().toString(36).slice(2, 8).toUpperCase()
|
||||
return `BK-${year}-${random}`
|
||||
}
|
||||
|
||||
export async function getPlatformHomepage() {
|
||||
return getMarketplaceHomepageContent()
|
||||
}
|
||||
@@ -132,8 +136,9 @@ export async function validatePromoCode(slug: string, code: string) {
|
||||
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
|
||||
identityDocumentNumber: string; fullAddress: string; licenseCountry: string; licenseNumber?: string; licenseCategory: string; internationalLicenseNumber?: string
|
||||
pickupLocation: string; returnLocation: string
|
||||
driverLicense?: string; dateOfBirth?: string; licenseExpiry?: string; licenseIssuedAt?: string; nationality?: string
|
||||
identityDocumentNumber?: string; fullAddress?: string; licenseCountry?: string; licenseNumber?: string; licenseCategory?: string; internationalLicenseNumber?: string
|
||||
offerId?: string; promoCodeUsed?: string; notes?: string; source?: string
|
||||
selectedInsurancePolicyIds?: string[]; additionalDrivers?: any[]
|
||||
}) {
|
||||
@@ -143,6 +148,7 @@ export async function createBooking(slug: string, body: {
|
||||
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')
|
||||
if (start < new Date()) throw new AppError('Start date cannot be in the past', 400, 'invalid_dates')
|
||||
|
||||
const availability = await getVehicleAvailabilitySummary(vehicle.id, { companyId: company.id, range: { startDate: start, endDate: end } })
|
||||
if (!availability.available) {
|
||||
@@ -182,16 +188,10 @@ export async function createBooking(slug: string, body: {
|
||||
}
|
||||
}
|
||||
|
||||
const additionalDriversList = body.additionalDrivers ?? []
|
||||
const { applied, total: pricingRulesTotal } = await applyPricingRules(
|
||||
company.id, customer.id, additionalDriversList as any[], vehicle.dailyRate, totalDays,
|
||||
company.id, customer.id, [], 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,
|
||||
@@ -202,6 +202,8 @@ export async function createBooking(slug: string, body: {
|
||||
source: body.source ?? 'PUBLIC_SITE',
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
pickupLocation: body.pickupLocation,
|
||||
returnLocation: body.returnLocation,
|
||||
dailyRate: vehicle.dailyRate,
|
||||
totalDays,
|
||||
totalAmount: baseAmount - discountAmount + pricingRulesTotal,
|
||||
@@ -209,20 +211,10 @@ export async function createBooking(slug: string, body: {
|
||||
pricingRulesApplied: applied,
|
||||
pricingRulesTotal,
|
||||
notes: body.notes ?? null,
|
||||
bookingReference: generateBookingReference(),
|
||||
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 publicAccess = generatePublicAccessToken()
|
||||
await repo.createReservationPublicAccess(
|
||||
reservation.id,
|
||||
@@ -232,11 +224,10 @@ export async function createBooking(slug: string, body: {
|
||||
|
||||
const refreshed = await repo.findReservationWithDetails(reservation.id)
|
||||
return {
|
||||
...refreshed,
|
||||
...presentPublicBooking({ ...refreshed, company }),
|
||||
publicAccessToken: publicAccess.token,
|
||||
requiresManualApproval:
|
||||
primaryLicenseResult.requiresApproval ||
|
||||
refreshed.additionalDrivers.some((d: any) => d.requiresApproval),
|
||||
bookingReference: (refreshed as any).bookingReference,
|
||||
message: 'The company will review your request.',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -175,9 +175,12 @@ describe('validatePromoCode', () => {
|
||||
describe('createBooking', () => {
|
||||
const baseBody = {
|
||||
vehicleId: 'v-1',
|
||||
startDate: '2025-06-01T00:00:00.000Z',
|
||||
endDate: '2025-06-04T00:00:00.000Z',
|
||||
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(() => {
|
||||
@@ -186,20 +189,36 @@ describe('createBooking', () => {
|
||||
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(validateLicense).mockReturnValue({ status: 'VALID', requiresApproval: false } as any)
|
||||
vi.mocked(repo.createReservation).mockResolvedValue({ id: 'r-1' } as any)
|
||||
vi.mocked(repo.findReservationWithDetails).mockResolvedValue({ id: 'r-1', additionalDrivers: [], insurances: [] } 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 reservation with requiresManualApproval flag', async () => {
|
||||
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).requiresManualApproval).toBe(false)
|
||||
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: '2025-06-04T00:00:00.000Z', endDate: '2025-06-01T00:00:00.000Z' }),
|
||||
createBooking(SLUG, { ...baseBody, startDate: '2026-07-04T00:00:00.000Z', endDate: '2026-07-01T00:00:00.000Z' }),
|
||||
).rejects.toMatchObject({ error: 'invalid_dates' })
|
||||
})
|
||||
|
||||
@@ -207,11 +226,6 @@ describe('createBooking', () => {
|
||||
vi.mocked(getVehicleAvailabilitySummary).mockResolvedValue({ available: false, status: 'RESERVED', nextAvailableAt: null, blockingReason: 'RESERVATION' })
|
||||
await expect(createBooking(SLUG, baseBody)).rejects.toMatchObject({ error: 'unavailable' })
|
||||
})
|
||||
|
||||
it('throws AppError when primary driver license is expired', async () => {
|
||||
vi.mocked(validateLicense).mockReturnValue({ status: 'EXPIRED', requiresApproval: false } as any)
|
||||
await expect(createBooking(SLUG, { ...baseBody, licenseExpiry: '2020-01-01T00:00:00.000Z' })).rejects.toMatchObject({ error: 'license_expired' })
|
||||
})
|
||||
})
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user