refractor code,
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user