refractor code,

This commit is contained in:
root
2026-05-21 12:35:49 -04:00
parent e74681e810
commit f009ca10c6
158 changed files with 215801 additions and 5884 deletions
@@ -0,0 +1,213 @@
import { AppError, NotFoundError, ConflictError } from '../../http/errors'
import { getVehicleAvailabilitySummary } from '../../services/vehicleAvailabilityService'
import { sendNotification, sendTransactionalEmail } from '../../services/notificationService'
import { marketplaceReservationEmail, type Lang } from '../../lib/emailTranslations'
import * as repo from './marketplace.repo'
export async function getPublicOffers() {
return repo.findPublicOffers()
}
export async function getCities() {
const rows = await repo.findCitiesFromCompanies()
const map = new Map<string, string>()
for (const row of rows) {
const city = row.brand?.publicCity?.trim()
if (!city) continue
const key = city.toLocaleLowerCase()
if (!map.has(key)) map.set(key, city)
}
return Array.from(map.values()).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
}
export async function getListedCompanies(params: {
city?: string; hasOffer?: string; page?: number; pageSize?: number
}) {
const page = params.page ?? 1
const pageSize = params.pageSize ?? 20
const where: any = { status: { in: ['ACTIVE', 'TRIALING'] }, brand: { isListedOnMarketplace: true } }
if (params.city) where.brand = { ...where.brand, publicCity: { contains: params.city, mode: 'insensitive' } }
if (params.hasOffer === 'true') {
where.offers = { some: { isPublic: true, isActive: true, validUntil: { gte: new Date() } } }
}
return repo.findListedCompanies(where, (page - 1) * pageSize, pageSize)
}
export async function searchVehicles(params: {
city?: string; startDate?: string; endDate?: string; category?: string
maxPrice?: number; transmission?: string; make?: string; model?: string
page?: number; pageSize?: number
}) {
const page = params.page ?? 1
const pageSize = params.pageSize ?? 20
const where: any = {
isPublished: true,
status: 'AVAILABLE',
company: { status: { in: ['ACTIVE', 'TRIALING'] }, brand: { isListedOnMarketplace: true } },
}
if (params.category) where.category = params.category
if (params.maxPrice !== undefined) where.dailyRate = { lte: params.maxPrice }
if (params.transmission) where.transmission = params.transmission
if (params.make) where.make = { contains: params.make, mode: 'insensitive' }
if (params.model) where.model = { contains: params.model, mode: 'insensitive' }
if (params.city) {
where.company = { ...where.company, brand: { isListedOnMarketplace: true, publicCity: { contains: params.city, mode: 'insensitive' } } }
}
const vehicles = await repo.findPublishedVehicles(where, (page - 1) * pageSize, pageSize)
const availability = await Promise.all(
vehicles.map(async (v) => {
const a = await getVehicleAvailabilitySummary(v.id, {
range: params.startDate && params.endDate
? { startDate: new Date(params.startDate), endDate: new Date(params.endDate) }
: undefined,
})
return [v.id, a] as const
}),
)
const availMap = new Map(availability)
return vehicles.map((v) => ({
...v,
availability: availMap.get(v.id)?.available ?? null,
availabilityStatus: availMap.get(v.id)?.status ?? 'AVAILABLE',
nextAvailableAt: availMap.get(v.id)?.nextAvailableAt ?? null,
}))
}
export async function createMarketplaceReservation(body: {
vehicleId: string; companySlug: string; firstName: string; lastName: string
email: string; phone?: string; startDate: string; endDate: string
notes?: string; language?: string
}) {
const startDate = new Date(body.startDate)
const endDate = new Date(body.endDate)
if (endDate <= startDate) {
throw new AppError('End date must be after start date', 400, 'invalid_dates')
}
const vehicle = await repo.findVehicleForMarketplace(body.vehicleId, body.companySlug)
if (!vehicle) throw new NotFoundError('Vehicle not found')
const availability = await getVehicleAvailabilitySummary(body.vehicleId, { range: { startDate, endDate } })
if (!availability.available) {
throw new AppError('Vehicle is not available for the selected dates', 409, 'unavailable', {
nextAvailableAt: availability.nextAvailableAt?.toISOString() ?? null,
})
}
const customer = await repo.upsertMarketplaceCustomer(vehicle.companyId, {
email: body.email, firstName: body.firstName, lastName: body.lastName, phone: body.phone,
})
const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / 86_400_000))
const totalAmount = vehicle.dailyRate * totalDays
const reservation = await repo.createMarketplaceReservation({
companyId: vehicle.companyId, vehicleId: body.vehicleId, customerId: customer.id,
startDate, endDate, dailyRate: vehicle.dailyRate, totalDays, totalAmount, notes: body.notes,
})
const lang = (body.language ?? 'fr') as Lang
const companyName = vehicle.company.brand?.displayName ?? (vehicle.company as any).name
const emailOpts = {
firstName: body.firstName, vehicleYear: vehicle.year, vehicleMake: vehicle.make, vehicleModel: vehicle.model,
companyName, startDate, endDate, totalDays,
rateDisplay: (vehicle.dailyRate / 100).toFixed(2),
totalDisplay: (totalAmount / 100).toFixed(2),
email: body.email, phone: body.phone,
}
sendTransactionalEmail({
to: body.email,
subject: marketplaceReservationEmail.subject(`${vehicle.make} ${vehicle.model}`, lang),
html: marketplaceReservationEmail.html(emailOpts, lang),
text: marketplaceReservationEmail.text(emailOpts, lang),
}).catch(() => null)
sendNotification({
type: 'NEW_BOOKING',
title: 'New Marketplace Reservation Request',
body: `${body.firstName} ${body.lastName} requested ${vehicle.make} ${vehicle.model} from ${startDate.toISOString().slice(0, 10)} to ${endDate.toISOString().slice(0, 10)}.`,
companyId: vehicle.companyId,
channels: ['IN_APP'],
}).catch(() => null)
return { reservationId: reservation.id }
}
export async function getCompanyPage(slug: string) {
const company = await repo.findCompanyPage(slug)
if (!company) throw new NotFoundError('Company not found')
const vehicles = await Promise.all(
company.vehicles.map(async (v) => {
const a = await getVehicleAvailabilitySummary(v.id)
return { ...v, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt }
}),
)
return { ...company, vehicles }
}
export async function getCompanyReviews(slug: string) {
const company = await repo.findCompanyBySlug(slug)
return repo.findCompanyReviews(company.id)
}
export async function getCompanyVehicles(slug: string) {
const company = await repo.findCompanyBySlug(slug)
return repo.findCompanyVehicles(company.id)
}
export async function getVehicleDetail(slug: string, vehicleId: string) {
const vehicle = await repo.findVehicleById(slug, vehicleId)
const availability = await getVehicleAvailabilitySummary(vehicle.id)
return { ...vehicle, availability: availability.available, availabilityStatus: availability.status, nextAvailableAt: availability.nextAvailableAt }
}
export async function getCompanyOffers(slug: string) {
const company = await repo.findCompanyBySlug(slug)
return repo.findCompanyOffers(company.id)
}
export async function getReviewContext(token: string) {
const reservation = await repo.findReservationByReviewToken(token)
if (!reservation) throw new NotFoundError('Review link is invalid or has expired')
if (reservation.review) throw new ConflictError('A review has already been submitted for this reservation')
return {
reservationId: reservation.id,
companyName: reservation.company.brand?.displayName ?? (reservation.company as any).name,
companyLogoUrl: reservation.company.brand?.logoUrl ?? null,
vehicle: `${reservation.vehicle.year} ${reservation.vehicle.make} ${reservation.vehicle.model}`,
vehiclePhoto: reservation.vehicle.photos[0] ?? null,
}
}
export async function submitReview(token: string, body: {
overallRating: number; vehicleRating?: number; serviceRating?: number; comment?: string
}) {
const reservation = await repo.findReservationForReviewSubmit(token)
if (!reservation) throw new NotFoundError('Review link is invalid or has expired')
if (reservation.review) throw new ConflictError('A review has already been submitted for this reservation')
const review = await repo.createReview({
reservationId: reservation.id,
companyId: reservation.companyId,
renterId: reservation.renterId,
...body,
})
await repo.invalidateReviewToken(reservation.id)
return { reviewId: review.id }
}
export async function validateOfferCode(code: string) {
const offer = await repo.findOfferByCode(code)
if (!offer) throw new NotFoundError('Promo code not found or expired')
if (offer.maxRedemptions && offer.redemptionCount >= offer.maxRedemptions) {
throw new ConflictError('Promo code has reached its maximum redemptions')
}
return offer
}