refractor code,
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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 { getMarketplaceHomepageContent } from '../../services/platformContentService'
|
||||
import * as amanpay from '../../services/amanpayService'
|
||||
import * as paypal from '../../services/paypalService'
|
||||
import * as repo from './site.repo'
|
||||
import { presentBrand } from './site.presenter'
|
||||
|
||||
export async function getPlatformHomepage() {
|
||||
return getMarketplaceHomepageContent()
|
||||
}
|
||||
|
||||
export async function getBrand(slug: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
return presentBrand(company)
|
||||
}
|
||||
|
||||
export async function getPublicVehicles(slug: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
const vehicles = await repo.findPublishedVehicles(company.id)
|
||||
return Promise.all(
|
||||
vehicles.map(async (v) => {
|
||||
const a = await getVehicleAvailabilitySummary(v.id)
|
||||
return { ...v, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function getVehicleDetail(slug: string, vehicleId: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
const vehicle = await repo.findVehicleById(vehicleId, company.id)
|
||||
const a = await getVehicleAvailabilitySummary(vehicle.id)
|
||||
return { ...vehicle, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt }
|
||||
}
|
||||
|
||||
export async function getOffers(slug: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
return repo.findActiveOffers(company.id)
|
||||
}
|
||||
|
||||
export async function getBookingOptions(slug: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
const insurancePolicies = await repo.findInsurancePolicies(company.id)
|
||||
return { insurancePolicies, contractSettings: company.contractSettings }
|
||||
}
|
||||
|
||||
export async function checkAvailability(slug: string, vehicleId: string, startDate: string, endDate: string) {
|
||||
await repo.findCompanyBySlug(slug)
|
||||
const availability = await getVehicleAvailabilitySummary(vehicleId, {
|
||||
range: { startDate: new Date(startDate), endDate: new Date(endDate) },
|
||||
})
|
||||
return { available: availability.available, nextAvailableAt: availability.nextAvailableAt }
|
||||
}
|
||||
|
||||
export async function validatePromoCode(slug: string, code: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
const offer = await repo.findOfferByPromoCode(company.id, code)
|
||||
if (!offer) throw new NotFoundError('Promo code not found or expired')
|
||||
return offer
|
||||
}
|
||||
|
||||
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
|
||||
offerId?: string; promoCodeUsed?: string; notes?: string; source?: string
|
||||
selectedInsurancePolicyIds?: string[]; additionalDrivers?: any[]
|
||||
}) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
const vehicle = await repo.findVehicleById(body.vehicleId, company.id)
|
||||
|
||||
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')
|
||||
|
||||
const availability = await getVehicleAvailabilitySummary(vehicle.id, { range: { startDate: start, endDate: end } })
|
||||
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.upsertCustomer(company.id, {
|
||||
email: body.email, firstName: body.firstName, lastName: body.lastName, phone: body.phone,
|
||||
driverLicense: body.driverLicense, dateOfBirth: body.dateOfBirth,
|
||||
licenseExpiry: body.licenseExpiry, licenseIssuedAt: body.licenseIssuedAt, nationality: body.nationality,
|
||||
})
|
||||
|
||||
const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86_400_000))
|
||||
const baseAmount = vehicle.dailyRate * totalDays
|
||||
|
||||
let discountAmount = 0
|
||||
if (body.promoCodeUsed) {
|
||||
const offer = await repo.findOfferByPromoCode(company.id, body.promoCodeUsed)
|
||||
if (offer) {
|
||||
if (offer.type === 'PERCENTAGE') discountAmount = Math.round(baseAmount * offer.discountValue / 100)
|
||||
else if (offer.type === 'FIXED_AMOUNT') discountAmount = offer.discountValue
|
||||
else if (offer.type === 'FREE_DAY') discountAmount = vehicle.dailyRate * offer.discountValue
|
||||
}
|
||||
}
|
||||
|
||||
const additionalDriversList = body.additionalDrivers ?? []
|
||||
const { applied, total: pricingRulesTotal } = await applyPricingRules(
|
||||
company.id, customer.id, additionalDriversList as any[], 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,
|
||||
customerId: customer.id,
|
||||
offerId: body.offerId ?? null,
|
||||
promoCodeUsed: body.promoCodeUsed ?? null,
|
||||
vehicleCategory: vehicle.category,
|
||||
source: body.source ?? 'PUBLIC_SITE',
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
dailyRate: vehicle.dailyRate,
|
||||
totalDays,
|
||||
totalAmount: baseAmount - discountAmount + pricingRulesTotal,
|
||||
discountAmount,
|
||||
pricingRulesApplied: applied,
|
||||
pricingRulesTotal,
|
||||
notes: body.notes ?? null,
|
||||
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 refreshed = await repo.findReservationWithDetails(reservation.id)
|
||||
return {
|
||||
...refreshed,
|
||||
requiresManualApproval:
|
||||
primaryLicenseResult.requiresApproval ||
|
||||
refreshed.additionalDrivers.some((d) => d.requiresApproval),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBooking(slug: string, reservationId: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
return repo.findBooking(reservationId, company.id)
|
||||
}
|
||||
|
||||
export async function initPayment(slug: string, reservationId: string, body: {
|
||||
provider: 'AMANPAY' | 'PAYPAL'; currency?: 'MAD' | 'USD' | 'EUR'; successUrl: string; failureUrl: string
|
||||
}) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
const reservation = await repo.findReservationForPayment(reservationId, company.id)
|
||||
|
||||
if (reservation.paymentStatus === 'PAID') {
|
||||
throw new AppError('This reservation is already paid', 409, 'already_paid')
|
||||
}
|
||||
|
||||
const customerLicenseResult = validateLicense(reservation.customer.licenseExpiry)
|
||||
const licenseBlocked =
|
||||
reservation.customer.licenseValidationStatus === 'DENIED' ||
|
||||
customerLicenseResult.status === 'EXPIRED' ||
|
||||
(customerLicenseResult.requiresApproval && reservation.customer.licenseValidationStatus !== 'APPROVED') ||
|
||||
reservation.additionalDrivers.some((d) => d.licenseExpired || (d.requiresApproval && !d.approvedAt))
|
||||
|
||||
if (licenseBlocked) {
|
||||
throw new AppError('This reservation requires license review before payment can be processed', 409, 'license_review_required')
|
||||
}
|
||||
|
||||
const currency = body.currency ?? 'MAD'
|
||||
const amount = reservation.totalAmount
|
||||
const description = `Rental: ${reservation.vehicle.make} ${reservation.vehicle.model}`
|
||||
const orderId = `res-${reservation.id}-${Date.now()}`
|
||||
const webhookBase = process.env.API_URL ?? 'http://localhost:4000'
|
||||
|
||||
let checkoutUrl: string
|
||||
let amanpayTransactionId: string | null = null
|
||||
let paypalCaptureId: string | null = null
|
||||
|
||||
if (body.provider === 'AMANPAY') {
|
||||
if (!amanpay.isConfigured()) {
|
||||
throw new AppError('Online payment is not available for this company', 503, 'provider_not_configured')
|
||||
}
|
||||
const brand = company.brand as any
|
||||
const merchantId = brand?.amanpayMerchantId ?? process.env.AMANPAY_MERCHANT_ID ?? ''
|
||||
const secretKey = brand?.amanpaySecretKey ?? process.env.AMANPAY_SECRET_KEY ?? ''
|
||||
if (!merchantId || !secretKey) {
|
||||
throw new AppError('AmanPay is not configured for this company', 503, 'provider_not_configured')
|
||||
}
|
||||
const result = await amanpay.createCheckout({
|
||||
amount, currency, orderId, description,
|
||||
customerEmail: reservation.customer.email,
|
||||
customerName: `${reservation.customer.firstName} ${reservation.customer.lastName}`,
|
||||
successUrl: body.successUrl,
|
||||
failureUrl: body.failureUrl,
|
||||
webhookUrl: `${webhookBase}/api/v1/payments/webhooks/amanpay`,
|
||||
})
|
||||
checkoutUrl = result.checkoutUrl
|
||||
amanpayTransactionId = result.transactionId
|
||||
} else {
|
||||
if (!paypal.isConfigured()) {
|
||||
throw new AppError('PayPal is not available for this company', 503, 'provider_not_configured')
|
||||
}
|
||||
const result = await paypal.createOrder({
|
||||
amount, currency, orderId, description,
|
||||
returnUrl: body.successUrl,
|
||||
cancelUrl: body.failureUrl,
|
||||
})
|
||||
checkoutUrl = result.approveUrl
|
||||
paypalCaptureId = result.orderId
|
||||
}
|
||||
|
||||
await repo.createRentalPayment({
|
||||
companyId: company.id,
|
||||
reservationId: reservation.id,
|
||||
amount,
|
||||
currency,
|
||||
paymentProvider: body.provider,
|
||||
amanpayTransactionId,
|
||||
paypalCaptureId,
|
||||
})
|
||||
|
||||
return { checkoutUrl }
|
||||
}
|
||||
|
||||
export async function capturePaypal(slug: string, paypalOrderId: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
const payment = await repo.findPaymentByPaypalOrderId(paypalOrderId, company.id)
|
||||
const capture = await paypal.captureOrder(paypalOrderId) as Record<string, any>
|
||||
const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId
|
||||
await repo.capturePaypalPayment(payment.id, captureId, payment.reservationId, payment.amount)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function handleContact(slug: string, body: { name: string; email: string; message: string }) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
return {
|
||||
success: true,
|
||||
deliveredTo: company.brand?.publicEmail ?? company.email,
|
||||
preview: body,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user