Files
alrahma_sunday_school_api/apps/api/dist/modules/site/site.service.js
T
2026-06-11 03:22:12 -04:00

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