279 lines
13 KiB
JavaScript
279 lines
13 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.formatDocumentNumber = formatDocumentNumber;
|
|
exports.ensureReservationDocumentNumbers = ensureReservationDocumentNumbers;
|
|
exports.buildReservationInvoiceLineItems = buildReservationInvoiceLineItems;
|
|
exports.getContract = getContract;
|
|
exports.getBilling = getBilling;
|
|
const prisma_1 = require("../../lib/prisma");
|
|
const repo = __importStar(require("./reservation.repo"));
|
|
function formatDocumentNumber(prefix, sequence) {
|
|
return `${prefix}-${String(sequence).padStart(6, '0')}`;
|
|
}
|
|
async function ensureReservationDocumentNumbers(companyId, reservationId) {
|
|
return prisma_1.prisma.$transaction(async (tx) => {
|
|
const reservation = await tx.reservation.findFirstOrThrow({
|
|
where: { id: reservationId, companyId },
|
|
select: { id: true, contractNumber: true, invoiceNumber: true },
|
|
});
|
|
if (reservation.contractNumber && reservation.invoiceNumber)
|
|
return reservation;
|
|
const settings = await tx.contractSettings.upsert({
|
|
where: { companyId },
|
|
update: {},
|
|
create: { companyId },
|
|
});
|
|
const data = {};
|
|
const settingsUpdate = {};
|
|
if (!reservation.contractNumber) {
|
|
const nextSeq = settings.lastContractSeq + 1;
|
|
data.contractNumber = formatDocumentNumber(settings.contractPrefix, nextSeq);
|
|
settingsUpdate.lastContractSeq = nextSeq;
|
|
}
|
|
if (!reservation.invoiceNumber) {
|
|
const nextSeq = settings.lastInvoiceSeq + 1;
|
|
data.invoiceNumber = formatDocumentNumber(settings.invoicePrefix, nextSeq);
|
|
settingsUpdate.lastInvoiceSeq = nextSeq;
|
|
}
|
|
if (Object.keys(settingsUpdate).length > 0) {
|
|
await tx.contractSettings.update({ where: { companyId }, data: settingsUpdate });
|
|
}
|
|
return tx.reservation.update({
|
|
where: { id: reservationId },
|
|
data,
|
|
select: { id: true, contractNumber: true, invoiceNumber: true },
|
|
});
|
|
});
|
|
}
|
|
function buildReservationInvoiceLineItems(reservation) {
|
|
const baseAmount = reservation.dailyRate * reservation.totalDays;
|
|
return [
|
|
{
|
|
description: `${reservation.vehicle.year} ${reservation.vehicle.make} ${reservation.vehicle.model}`,
|
|
qty: reservation.totalDays,
|
|
unitPrice: reservation.dailyRate,
|
|
total: baseAmount,
|
|
category: 'RENTAL',
|
|
},
|
|
...reservation.insurances.map((ins) => ({
|
|
description: ins.policyName,
|
|
qty: ins.chargeType === 'PER_DAY' ? reservation.totalDays : 1,
|
|
unitPrice: ins.chargeType === 'PER_DAY' ? ins.chargeValue : ins.totalCharge,
|
|
total: ins.totalCharge,
|
|
category: 'INSURANCE',
|
|
})),
|
|
...reservation.additionalDrivers
|
|
.filter((d) => d.totalCharge > 0)
|
|
.map((d) => ({
|
|
description: `Additional driver - ${d.firstName} ${d.lastName}`,
|
|
qty: 1,
|
|
unitPrice: d.totalCharge,
|
|
total: d.totalCharge,
|
|
category: 'ADDITIONAL_DRIVER',
|
|
})),
|
|
...((reservation.pricingRulesApplied ?? []).map((rule, i) => ({
|
|
description: rule.name?.trim() || `Pricing adjustment ${i + 1}`,
|
|
qty: 1,
|
|
unitPrice: Number(rule.amount ?? 0),
|
|
total: Number(rule.amount ?? 0),
|
|
category: 'PRICING_RULE',
|
|
}))),
|
|
...(reservation.discountAmount > 0 ? [{
|
|
description: 'Discount',
|
|
qty: 1,
|
|
unitPrice: -reservation.discountAmount,
|
|
total: -reservation.discountAmount,
|
|
category: 'DISCOUNT',
|
|
}] : []),
|
|
...(reservation.depositAmount > 0 ? [{
|
|
description: 'Security deposit',
|
|
qty: 1,
|
|
unitPrice: reservation.depositAmount,
|
|
total: reservation.depositAmount,
|
|
category: 'DEPOSIT',
|
|
}] : []),
|
|
];
|
|
}
|
|
async function getContract(id, companyId) {
|
|
await ensureReservationDocumentNumbers(companyId, id);
|
|
const reservation = await repo.findForContract(id, companyId);
|
|
const contractSettings = reservation.company.contractSettings ?? await prisma_1.prisma.contractSettings.upsert({
|
|
where: { companyId },
|
|
update: {},
|
|
create: { companyId },
|
|
});
|
|
const extras = reservation.extras && typeof reservation.extras === 'object' && !Array.isArray(reservation.extras)
|
|
? reservation.extras
|
|
: {};
|
|
const invoiceLineItems = buildReservationInvoiceLineItems({
|
|
dailyRate: reservation.dailyRate,
|
|
totalDays: reservation.totalDays,
|
|
discountAmount: reservation.discountAmount,
|
|
depositAmount: reservation.depositAmount,
|
|
pricingRulesApplied: Array.isArray(reservation.pricingRulesApplied)
|
|
? reservation.pricingRulesApplied
|
|
: [],
|
|
insurances: reservation.insurances,
|
|
additionalDrivers: reservation.additionalDrivers,
|
|
vehicle: reservation.vehicle,
|
|
});
|
|
const subtotal = invoiceLineItems.reduce((s, i) => s + i.total, 0);
|
|
const taxes = contractSettings.showTax && contractSettings.taxRate
|
|
? [{ label: contractSettings.taxLabel?.trim() || 'Tax', rate: contractSettings.taxRate, amount: Math.round(subtotal * contractSettings.taxRate / 100) }]
|
|
: [];
|
|
const taxTotal = taxes.reduce((s, t) => s + t.amount, 0);
|
|
const grandTotal = subtotal + taxTotal;
|
|
const amountPaid = reservation.rentalPayments
|
|
.filter((p) => p.status === 'SUCCEEDED')
|
|
.reduce((s, p) => s + p.amount, 0);
|
|
const checkInInspection = reservation.inspections.find((i) => i.type === 'CHECKIN') ?? null;
|
|
const checkOutInspection = reservation.inspections.find((i) => i.type === 'CHECKOUT') ?? null;
|
|
return {
|
|
reservationId: reservation.id,
|
|
contractLocale: reservation.company.brand?.defaultLocale ?? 'en',
|
|
contractNumber: reservation.contractNumber,
|
|
invoiceNumber: reservation.invoiceNumber,
|
|
generatedAt: new Date().toISOString(),
|
|
status: reservation.status,
|
|
paymentStatus: reservation.paymentStatus,
|
|
paymentMode: typeof extras.paymentMode === 'string' ? extras.paymentMode : null,
|
|
notes: reservation.notes,
|
|
company: {
|
|
name: reservation.company.brand?.displayName ?? reservation.company.name,
|
|
legalName: contractSettings.legalName || reservation.company.name,
|
|
email: reservation.company.brand?.publicEmail ?? reservation.company.email,
|
|
phone: reservation.company.brand?.publicPhone ?? reservation.company.phone,
|
|
address: reservation.company.brand?.publicAddress ?? reservation.company.address ?? null,
|
|
city: reservation.company.brand?.publicCity ?? null,
|
|
country: reservation.company.brand?.publicCountry ?? null,
|
|
registrationNumber: contractSettings.registrationNumber,
|
|
taxId: contractSettings.taxId,
|
|
logoUrl: reservation.company.brand?.logoUrl ?? null,
|
|
},
|
|
driver: {
|
|
firstName: reservation.customer.firstName,
|
|
lastName: reservation.customer.lastName,
|
|
email: reservation.customer.email,
|
|
phone: reservation.customer.phone,
|
|
dateOfBirth: reservation.customer.dateOfBirth,
|
|
driverLicense: reservation.customer.driverLicense,
|
|
licenseCountry: reservation.customer.licenseCountry,
|
|
licenseCategory: reservation.customer.licenseCategory,
|
|
licenseIssuedAt: reservation.customer.licenseIssuedAt,
|
|
licenseExpiry: reservation.customer.licenseExpiry,
|
|
nationality: reservation.customer.nationality,
|
|
},
|
|
additionalDrivers: reservation.additionalDrivers.map((d) => ({
|
|
id: d.id, firstName: d.firstName, lastName: d.lastName,
|
|
email: d.email, phone: d.phone, driverLicense: d.driverLicense,
|
|
licenseIssuedAt: d.licenseIssuedAt, licenseExpiry: d.licenseExpiry,
|
|
licenseCountry: d.nationality, dateOfBirth: d.dateOfBirth, totalCharge: d.totalCharge,
|
|
})),
|
|
vehicle: {
|
|
make: reservation.vehicle.make, model: reservation.vehicle.model, year: reservation.vehicle.year,
|
|
color: reservation.vehicle.color, licensePlate: reservation.vehicle.licensePlate,
|
|
vin: reservation.vehicle.vin, category: reservation.vehicle.category,
|
|
},
|
|
rentalPeriod: {
|
|
startDate: reservation.startDate, endDate: reservation.endDate, totalDays: reservation.totalDays,
|
|
pickupLocation: reservation.pickupLocation, returnLocation: reservation.returnLocation,
|
|
},
|
|
insurance: {
|
|
total: reservation.insuranceTotal,
|
|
policies: reservation.insurances.map((ins) => ({
|
|
id: ins.id, name: ins.policyName, chargeType: ins.chargeType,
|
|
unitPrice: ins.chargeValue, totalCharge: ins.totalCharge,
|
|
})),
|
|
},
|
|
inspections: { checkIn: checkInInspection, checkOut: checkOutInspection },
|
|
terms: {
|
|
terms: contractSettings.terms,
|
|
fuelPolicy: contractSettings.fuelPolicy,
|
|
fuelPolicyType: contractSettings.fuelPolicyType,
|
|
depositPolicy: contractSettings.depositPolicy,
|
|
lateFeePolicy: contractSettings.lateFeePolicy,
|
|
damagePolicy: contractSettings.damagePolicy,
|
|
footerNote: contractSettings.contractFooterNote,
|
|
signatureRequired: contractSettings.signatureRequired,
|
|
},
|
|
invoice: {
|
|
currency: reservation.company.accountingSettings?.currency ?? reservation.company.brand?.defaultCurrency ?? 'MAD',
|
|
lineItems: invoiceLineItems,
|
|
subtotal, taxes, taxTotal, total: grandTotal, amountPaid,
|
|
balanceDue: grandTotal - amountPaid,
|
|
payments: reservation.rentalPayments.map((p) => ({
|
|
id: p.id, amount: p.amount, currency: p.currency, type: p.type,
|
|
status: p.status, provider: p.paymentProvider,
|
|
paymentMethod: p.paymentMethod, paidAt: p.paidAt, createdAt: p.createdAt,
|
|
})),
|
|
},
|
|
};
|
|
}
|
|
async function getBilling(id, companyId) {
|
|
const reservation = await repo.findForBilling(id, companyId);
|
|
const baseAmount = reservation.dailyRate * reservation.totalDays;
|
|
const lineItems = [
|
|
{
|
|
description: `${reservation.vehicle.year} ${reservation.vehicle.make} ${reservation.vehicle.model} — ${reservation.totalDays} day(s)`,
|
|
qty: reservation.totalDays, unitPrice: reservation.dailyRate, total: baseAmount, category: 'RENTAL',
|
|
},
|
|
...reservation.insurances.map((ins) => ({
|
|
description: ins.policyName,
|
|
qty: ins.chargeType === 'PER_DAY' ? reservation.totalDays : 1,
|
|
unitPrice: ins.chargeType === 'PER_DAY' ? ins.chargeValue : ins.totalCharge,
|
|
total: ins.totalCharge, category: 'INSURANCE',
|
|
})),
|
|
...reservation.additionalDrivers
|
|
.filter((d) => d.totalCharge > 0)
|
|
.map((d) => ({
|
|
description: `Additional Driver — ${d.firstName} ${d.lastName}`,
|
|
qty: 1, unitPrice: d.totalCharge, total: d.totalCharge, category: 'ADDITIONAL_DRIVER',
|
|
})),
|
|
...(reservation.depositAmount > 0 ? [{
|
|
description: 'Security Deposit (refundable)',
|
|
qty: 1, unitPrice: reservation.depositAmount, total: reservation.depositAmount, category: 'DEPOSIT',
|
|
}] : []),
|
|
];
|
|
const grandTotal = lineItems.reduce((s, i) => s + i.total, 0) - reservation.discountAmount;
|
|
return {
|
|
lineItems,
|
|
discountAmount: reservation.discountAmount,
|
|
pricingRulesApplied: reservation.pricingRulesApplied,
|
|
pricingRulesTotal: reservation.pricingRulesTotal,
|
|
grandTotal,
|
|
};
|
|
}
|
|
//# sourceMappingURL=reservation.document.service.js.map
|