add car reservation feature

This commit is contained in:
root
2026-05-09 21:10:38 -04:00
parent 09b0e3b55f
commit 6322b7d2a1
37 changed files with 1169 additions and 119 deletions
+35 -22
View File
@@ -8,6 +8,7 @@ import { applyPricingRules } from '../services/pricingRuleService'
import { validateAndFlagLicense, validateLicense } from '../services/licenseValidationService'
import * as paypal from '../services/paypalService'
import { getMarketplaceHomepageContent } from '../services/platformContentService'
import { getVehicleAvailabilitySummary } from '../services/vehicleAvailabilityService'
const router = Router()
@@ -67,7 +68,18 @@ router.get('/:slug/vehicles', async (req, res, next) => {
where: { companyId: company.id, isPublished: true },
orderBy: { createdAt: 'desc' },
})
res.json({ data: vehicles })
const enrichedVehicles = await Promise.all(
vehicles.map(async (vehicle) => {
const availability = await getVehicleAvailabilitySummary(vehicle.id)
return {
...vehicle,
availability: availability.available,
availabilityStatus: availability.status,
nextAvailableAt: availability.nextAvailableAt,
}
}),
)
res.json({ data: enrichedVehicles })
} catch (err) {
if (isDatabaseUnavailableError(err)) return res.json({ data: [] })
next(err)
@@ -79,14 +91,9 @@ router.get('/:slug/vehicles/:id', async (req, res, next) => {
const company = await findCompanyBySlug(req.params.slug)
const vehicle = await prisma.vehicle.findFirstOrThrow({
where: { id: req.params.id, companyId: company.id, isPublished: true },
include: {
reservations: {
where: { status: { in: ['CONFIRMED', 'ACTIVE'] } },
select: { startDate: true, endDate: true, status: true },
},
},
})
res.json({ data: vehicle })
const availability = await getVehicleAvailabilitySummary(vehicle.id)
res.json({ data: { ...vehicle, availability: availability.available, availabilityStatus: availability.status, nextAvailableAt: availability.nextAvailableAt } })
} catch (err) {
if (isDatabaseUnavailableError(err)) {
return res.status(503).json({ error: 'database_unavailable', message: 'Vehicle details are temporarily unavailable', statusCode: 503 })
@@ -143,18 +150,10 @@ router.post('/:slug/availability', async (req, res, next) => {
endDate: z.string().datetime(),
}).parse(req.body)
const conflicts = await prisma.reservation.findMany({
where: {
companyId: company.id,
vehicleId,
status: { in: ['CONFIRMED', 'ACTIVE'] },
startDate: { lt: new Date(endDate) },
endDate: { gt: new Date(startDate) },
},
select: { startDate: true, endDate: true },
const availability = await getVehicleAvailabilitySummary(vehicleId, {
range: { startDate: new Date(startDate), endDate: new Date(endDate) },
})
res.json({ data: { available: conflicts.length === 0, conflicts } })
res.json({ data: { available: availability.available, nextAvailableAt: availability.nextAvailableAt } })
} catch (err) {
if (isDatabaseUnavailableError(err)) {
return res.status(503).json({ error: 'database_unavailable', message: 'Availability checks are temporarily unavailable', statusCode: 503 })
@@ -225,6 +224,23 @@ router.post('/:slug/book', async (req, res, next) => {
const vehicle = await prisma.vehicle.findFirstOrThrow({
where: { id: body.vehicleId, companyId: company.id, isPublished: true },
})
const start = new Date(body.startDate)
const end = new Date(body.endDate)
if (end <= start) {
return res.status(400).json({ error: 'invalid_dates', message: 'End date must be after start date', statusCode: 400 })
}
const availability = await getVehicleAvailabilitySummary(vehicle.id, {
range: { startDate: start, endDate: end },
})
if (!availability.available) {
return res.status(409).json({
error: 'unavailable',
message: 'Vehicle is not available for the selected dates',
statusCode: 409,
nextAvailableAt: availability.nextAvailableAt?.toISOString() ?? null,
})
}
const customer = await prisma.customer.upsert({
where: { companyId_email: { companyId: company.id, email: body.email } },
@@ -251,9 +267,6 @@ router.post('/:slug/book', async (req, res, next) => {
nationality: body.nationality ?? null,
},
})
const start = new Date(body.startDate)
const end = new Date(body.endDate)
const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86400000))
const baseAmount = vehicle.dailyRate * totalDays