354 lines
17 KiB
JavaScript
354 lines
17 KiB
JavaScript
"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
|