"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getPlatformHomepage = getPlatformHomepage; exports.getPlatformPricing = getPlatformPricing; exports.getBrand = getBrand; exports.getPublicVehicles = getPublicVehicles; exports.getVehicleDetail = getVehicleDetail; exports.getOffers = getOffers; exports.getBookingOptions = getBookingOptions; exports.checkAvailability = checkAvailability; exports.validatePromoCode = validatePromoCode; exports.createBooking = createBooking; exports.getBooking = getBooking; exports.initPayment = initPayment; exports.capturePaypal = capturePaypal; exports.handleContact = handleContact; const types_1 = require("@rentaldrivego/types"); const errors_1 = require("../../http/errors"); const vehicleAvailabilityService_1 = require("../../services/vehicleAvailabilityService"); const insuranceService_1 = require("../../services/insuranceService"); const additionalDriverService_1 = require("../../services/additionalDriverService"); const pricingRuleService_1 = require("../../services/pricingRuleService"); const licenseValidationService_1 = require("../../services/licenseValidationService"); const platformContentService_1 = require("../../services/platformContentService"); const prisma_1 = require("../../lib/prisma"); const amanpay = __importStar(require("../../services/amanpayService")); const paypal = __importStar(require("../../services/paypalService")); const repo = __importStar(require("./site.repo")); const site_presenter_1 = require("./site.presenter"); const publicAccessTokens_1 = require("../../security/publicAccessTokens"); function assertAllowedPaymentRedirect(urlValue, company) { let parsed; try { parsed = new URL(urlValue); } catch { throw new errors_1.AppError('Invalid payment redirect URL', 400, 'invalid_redirect_url'); } if (process.env.NODE_ENV === 'production' && parsed.protocol !== 'https:') { throw new errors_1.AppError('Payment redirect URLs must use HTTPS', 400, 'invalid_redirect_url'); } const allowedHosts = new Set(); if (process.env.NODE_ENV !== 'production') { allowedHosts.add('localhost:3000'); allowedHosts.add('localhost:4000'); allowedHosts.add('127.0.0.1:3000'); allowedHosts.add('127.0.0.1:4000'); } for (const value of [process.env.MARKETPLACE_URL, process.env.DASHBOARD_URL, process.env.NEXT_PUBLIC_MARKETPLACE_URL, process.env.NEXT_PUBLIC_DASHBOARD_URL]) { if (!value) continue; try { allowedHosts.add(new URL(value).host); } catch { } } const brand = company.brand; if (brand?.customDomain && brand?.customDomainVerified) allowedHosts.add(brand.customDomain); if (brand?.subdomain && process.env.PUBLIC_SITE_BASE_DOMAIN) { allowedHosts.add(`${brand.subdomain}.${process.env.PUBLIC_SITE_BASE_DOMAIN}`); } if (!allowedHosts.has(parsed.host)) { throw new errors_1.AppError('Payment redirect host is not allowed', 400, 'invalid_redirect_url'); } } async function getPlatformHomepage() { return (0, platformContentService_1.getMarketplaceHomepageContent)(); } async function getPlatformPricing() { const [configs, features] = await Promise.all([ prisma_1.prisma.pricingConfig.findMany(), prisma_1.prisma.planFeature.findMany({ orderBy: [{ plan: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }], }), ]); const prices = configs.length === 0 ? types_1.PLAN_PRICES : configs.reduce((acc, config) => { if (!acc[config.plan]) acc[config.plan] = {}; acc[config.plan][config.billingPeriod] = { MAD: config.amount }; return acc; }, {}); const planFeatures = Object.fromEntries(Object.entries(types_1.PLAN_FEATURES).map(([plan, fallback]) => { const labels = features.filter((feature) => feature.plan === plan).map((feature) => feature.label); return [plan, labels.length > 0 ? labels : fallback]; })); return { prices, planFeatures }; } async function getBrand(slug) { const company = await repo.findCompanyBySlug(slug); return (0, site_presenter_1.presentBrand)(company); } async function getPublicVehicles(slug) { const company = await repo.findCompanyBySlug(slug); const vehicles = await repo.findPublishedVehicles(company.id); return Promise.all(vehicles.map(async (v) => { const a = await (0, vehicleAvailabilityService_1.getVehicleAvailabilitySummary)(v.id, { companyId: v.companyId }); return { ...v, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt }; })); } async function getVehicleDetail(slug, vehicleId) { const company = await repo.findCompanyBySlug(slug); const vehicle = await repo.findVehicleById(vehicleId, company.id); const a = await (0, vehicleAvailabilityService_1.getVehicleAvailabilitySummary)(vehicle.id, { companyId: vehicle.companyId }); return { ...vehicle, availability: a.available, availabilityStatus: a.status, nextAvailableAt: a.nextAvailableAt }; } async function getOffers(slug) { const company = await repo.findCompanyBySlug(slug); return repo.findActiveOffers(company.id); } async function getBookingOptions(slug) { const company = await repo.findCompanyBySlug(slug); const insurancePolicies = await repo.findInsurancePolicies(company.id); return { insurancePolicies, contractSettings: company.contractSettings }; } async function checkAvailability(slug, vehicleId, startDate, endDate) { const company = await repo.findCompanyBySlug(slug); const vehicle = await repo.findVehicleById(vehicleId, company.id); const availability = await (0, vehicleAvailabilityService_1.getVehicleAvailabilitySummary)(vehicle.id, { companyId: company.id, range: { startDate: new Date(startDate), endDate: new Date(endDate) }, }); return { available: availability.available, nextAvailableAt: availability.nextAvailableAt }; } async function validatePromoCode(slug, code) { const company = await repo.findCompanyBySlug(slug); const offer = await repo.findOfferByPromoCode(company.id, code); if (!offer) throw new errors_1.NotFoundError('Promo code not found or expired'); return offer; } async function createBooking(slug, body) { 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 errors_1.AppError('End date must be after start date', 400, 'invalid_dates'); const availability = await (0, vehicleAvailabilityService_1.getVehicleAvailabilitySummary)(vehicle.id, { companyId: company.id, range: { startDate: start, endDate: end } }); if (!availability.available) { throw new errors_1.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, identityDocumentNumber: body.identityDocumentNumber, fullAddress: body.fullAddress, licenseCountry: body.licenseCountry, licenseNumber: body.licenseNumber, licenseCategory: body.licenseCategory, internationalLicenseNumber: body.internationalLicenseNumber, }); 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 (0, pricingRuleService_1.applyPricingRules)(company.id, customer.id, additionalDriversList, vehicle.dailyRate, totalDays); const primaryLicenseResult = (0, licenseValidationService_1.validateLicense)(body.licenseExpiry ? new Date(body.licenseExpiry) : null); if (primaryLicenseResult.status === 'EXPIRED') { throw new errors_1.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 (0, insuranceService_1.applyInsurancesToReservation)(reservation.id, company.id, insuranceIds, totalDays, baseAmount); } if (additionalDriversList.length > 0) { await (0, additionalDriverService_1.applyAdditionalDriversToReservation)(reservation.id, company.id, additionalDriversList, totalDays); } if (body.licenseExpiry) { await (0, licenseValidationService_1.validateAndFlagLicense)(customer.id).catch(() => null); } const publicAccess = (0, publicAccessTokens_1.generatePublicAccessToken)(); await repo.createReservationPublicAccess(reservation.id, publicAccess.tokenHash, new Date(Date.now() + 14 * 24 * 60 * 60 * 1000)); const refreshed = await repo.findReservationWithDetails(reservation.id); return { ...refreshed, publicAccessToken: publicAccess.token, requiresManualApproval: primaryLicenseResult.requiresApproval || refreshed.additionalDrivers.some((d) => d.requiresApproval), }; } async function assertPublicBookingAccess(reservationId, token) { if (!token) throw new errors_1.AppError('Booking access token is required', 403, 'booking_token_required'); const access = await repo.findReservationPublicAccess(reservationId, (0, publicAccessTokens_1.hashPublicAccessToken)(token)); if (!access) throw new errors_1.AppError('Booking not found', 404, 'not_found'); await repo.markReservationPublicAccessUsed(access.id); } async function getBooking(slug, reservationId, accessToken) { const company = await repo.findCompanyBySlug(slug); await assertPublicBookingAccess(reservationId, accessToken); return (0, site_presenter_1.presentPublicBooking)(await repo.findBooking(reservationId, company.id)); } async function initPayment(slug, reservationId, body) { const company = await repo.findCompanyBySlug(slug); await assertPublicBookingAccess(reservationId, body.accessToken); const reservation = await repo.findReservationForPayment(reservationId, company.id); if (reservation.paymentStatus === 'PAID') { throw new errors_1.AppError('This reservation is already paid', 409, 'already_paid'); } const customerLicenseResult = (0, licenseValidationService_1.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 errors_1.AppError('This reservation requires license review before payment can be processed', 409, 'license_review_required'); } assertAllowedPaymentRedirect(body.successUrl, company); assertAllowedPaymentRedirect(body.failureUrl, company); 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; let amanpayTransactionId = null; let paypalCaptureId = null; if (body.provider === 'AMANPAY') { if (!amanpay.isConfigured()) { throw new errors_1.AppError('Online payment is not available for this company', 503, 'provider_not_configured'); } const brand = company.brand; const merchantId = brand?.amanpayMerchantId ?? process.env.AMANPAY_MERCHANT_ID ?? ''; const secretKey = brand?.amanpaySecretKey ?? process.env.AMANPAY_SECRET_KEY ?? ''; if (!merchantId || !secretKey) { throw new errors_1.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 errors_1.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 }; } async function capturePaypal(slug, paypalOrderId) { const company = await repo.findCompanyBySlug(slug); const payment = await repo.findPaymentByPaypalOrderId(paypalOrderId, company.id); const capture = await paypal.captureOrder(paypalOrderId); const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId; await repo.capturePaypalPayment(payment.id, captureId, payment.reservationId, payment.amount); return { success: true }; } async function handleContact(slug, body) { const company = await repo.findCompanyBySlug(slug); return { success: true, deliveredTo: company.brand?.publicEmail ?? company.email, preview: body, }; } //# sourceMappingURL=site.service.js.map