archetecture security fix

This commit is contained in:
root
2026-06-11 03:22:12 -04:00
parent 6def9993da
commit 9483750161
3126 changed files with 177194 additions and 37211 deletions
+39
View File
@@ -0,0 +1,39 @@
type AdditionalDriverCharge = 'PER_DAY' | 'FLAT' | 'FREE';
export interface AdditionalDriverInput {
firstName: string;
lastName: string;
email?: string;
phone?: string;
driverLicense: string;
licenseExpiry?: string | null;
licenseIssuedAt?: string | null;
dateOfBirth?: string | null;
nationality?: string;
}
export declare function calculateAdditionalDriverCharge(chargeType: AdditionalDriverCharge, chargeValue: number, totalDays: number): number;
export declare function applyAdditionalDriversToReservation(reservationId: string, companyId: string, drivers: AdditionalDriverInput[], totalDays: number): Promise<{
records: {
reservationId: string;
companyId: string;
firstName: string;
lastName: string;
email: string | null;
phone: string | null;
driverLicense: string;
licenseExpiry: Date | null;
licenseIssuedAt: Date | null;
dateOfBirth: Date | null;
nationality: string | null;
chargeType: any;
chargeValue: any;
totalCharge: number;
licenseExpired: boolean;
licenseExpiringSoon: boolean;
requiresApproval: boolean;
approvalNote: string | null;
}[];
additionalDriverTotal: number;
requiresManualApproval: boolean;
}>;
export {};
//# sourceMappingURL=additionalDriverService.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"additionalDriverService.d.ts","sourceRoot":"","sources":["../../src/services/additionalDriverService.ts"],"names":[],"mappings":"AAEA,KAAK,sBAAsB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAA;AAGzD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,sBAAsB,EAClC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,UAUlB;AAED,wBAAsB,mCAAmC,CACvD,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,qBAAqB,EAAE,EAChC,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;GA2DlB"}
+69
View File
@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateAdditionalDriverCharge = calculateAdditionalDriverCharge;
exports.applyAdditionalDriversToReservation = applyAdditionalDriversToReservation;
const prisma_1 = require("../lib/prisma");
const licenseValidationService_1 = require("./licenseValidationService");
function calculateAdditionalDriverCharge(chargeType, chargeValue, totalDays) {
switch (chargeType) {
case 'PER_DAY':
return chargeValue * totalDays;
case 'FLAT':
return chargeValue;
default:
return 0;
}
}
async function applyAdditionalDriversToReservation(reservationId, companyId, drivers, totalDays) {
if (drivers.length === 0) {
return { records: [], additionalDriverTotal: 0, requiresManualApproval: false };
}
const settings = await prisma_1.prisma.contractSettings.findUnique({ where: { companyId } });
const chargeType = settings?.additionalDriverCharge ?? 'FREE';
const chargeValue = chargeType === 'PER_DAY'
? settings?.additionalDriverDailyRate ?? 0
: chargeType === 'FLAT'
? settings?.additionalDriverFlatRate ?? 0
: 0;
const records = drivers.map((driver) => {
const licenseResult = (0, licenseValidationService_1.validateLicense)(driver.licenseExpiry ? new Date(driver.licenseExpiry) : null);
const totalCharge = calculateAdditionalDriverCharge(chargeType, chargeValue, totalDays);
return {
reservationId,
companyId,
firstName: driver.firstName,
lastName: driver.lastName,
email: driver.email ?? null,
phone: driver.phone ?? null,
driverLicense: driver.driverLicense,
licenseExpiry: driver.licenseExpiry ? new Date(driver.licenseExpiry) : null,
licenseIssuedAt: driver.licenseIssuedAt ? new Date(driver.licenseIssuedAt) : null,
dateOfBirth: driver.dateOfBirth ? new Date(driver.dateOfBirth) : null,
nationality: driver.nationality ?? null,
chargeType,
chargeValue,
totalCharge,
licenseExpired: licenseResult.status === 'EXPIRED',
licenseExpiringSoon: licenseResult.status === 'EXPIRING',
requiresApproval: licenseResult.requiresApproval,
approvalNote: licenseResult.requiresApproval ? licenseResult.message : null,
};
});
const additionalDriverTotal = records.reduce((sum, record) => sum + record.totalCharge, 0);
await prisma_1.prisma.$transaction([
prisma_1.prisma.additionalDriver.createMany({ data: records }),
prisma_1.prisma.reservation.update({
where: { id: reservationId },
data: {
additionalDriverTotal,
totalAmount: { increment: additionalDriverTotal },
},
}),
]);
return {
records,
additionalDriverTotal,
requiresManualApproval: records.some((record) => record.requiresApproval),
};
}
//# sourceMappingURL=additionalDriverService.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"additionalDriverService.js","sourceRoot":"","sources":["../../src/services/additionalDriverService.ts"],"names":[],"mappings":"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"}
+22
View File
@@ -0,0 +1,22 @@
export interface AmanPayCreateParams {
amount: number;
currency: string;
orderId: string;
description: string;
customerEmail?: string;
customerName?: string;
successUrl: string;
failureUrl: string;
webhookUrl: string;
}
export interface AmanPayCheckoutResult {
transactionId: string;
checkoutUrl: string;
status: string;
}
export declare function createCheckout(params: AmanPayCreateParams): Promise<AmanPayCheckoutResult>;
export declare function getTransaction(transactionId: string): Promise<unknown>;
export declare function refundTransaction(transactionId: string, amount: number, reason?: string): Promise<unknown>;
export declare function verifyWebhookSignature(rawBody: string, signature: string): boolean;
export declare function isConfigured(): boolean;
//# sourceMappingURL=amanpayService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"amanpayService.d.ts","sourceRoot":"","sources":["../../src/services/amanpayService.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf;AAUD,wBAAsB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA6BhG;AAED,wBAAsB,cAAc,CAAC,aAAa,EAAE,MAAM,oBAMzD;AAED,wBAAsB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,oBAW7F;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAOlF;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
+83
View File
@@ -0,0 +1,83 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCheckout = createCheckout;
exports.getTransaction = getTransaction;
exports.refundTransaction = refundTransaction;
exports.verifyWebhookSignature = verifyWebhookSignature;
exports.isConfigured = isConfigured;
const crypto_1 = __importDefault(require("crypto"));
const BASE_URL = process.env.AMANPAY_BASE_URL ?? 'https://api.amanpay.net';
const MERCHANT_ID = process.env.AMANPAY_MERCHANT_ID ?? '';
const SECRET_KEY = process.env.AMANPAY_SECRET_KEY ?? '';
const WEBHOOK_SECRET = process.env.AMANPAY_WEBHOOK_SECRET ?? '';
async function getAuthHeaders() {
return {
'Content-Type': 'application/json',
'x-merchant-id': MERCHANT_ID,
'x-api-key': SECRET_KEY,
};
}
async function createCheckout(params) {
const res = await fetch(`${BASE_URL}/v1/payments`, {
method: 'POST',
headers: await getAuthHeaders(),
body: JSON.stringify({
merchant_id: MERCHANT_ID,
amount: params.amount,
currency: params.currency,
order_id: params.orderId,
description: params.description,
customer_email: params.customerEmail,
customer_name: params.customerName,
success_url: params.successUrl,
failure_url: params.failureUrl,
webhook_url: params.webhookUrl,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`AmanPay checkout failed: ${err?.message ?? res.statusText}`);
}
const json = await res.json();
return {
transactionId: (json.transaction_id ?? json.id),
checkoutUrl: (json.checkout_url ?? json.payment_url),
status: (json.status ?? 'PENDING'),
};
}
async function getTransaction(transactionId) {
const res = await fetch(`${BASE_URL}/v1/payments/${transactionId}`, {
headers: await getAuthHeaders(),
});
if (!res.ok)
throw new Error('AmanPay: failed to fetch transaction');
return res.json();
}
async function refundTransaction(transactionId, amount, reason) {
const res = await fetch(`${BASE_URL}/v1/payments/${transactionId}/refund`, {
method: 'POST',
headers: await getAuthHeaders(),
body: JSON.stringify({ amount, reason }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`AmanPay refund failed: ${err?.message ?? res.statusText}`);
}
return res.json();
}
function verifyWebhookSignature(rawBody, signature) {
if (!WEBHOOK_SECRET)
return false;
const expected = crypto_1.default
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
return crypto_1.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
function isConfigured() {
return !!(MERCHANT_ID && SECRET_KEY && MERCHANT_ID !== 'your-amanpay-merchant-id');
}
//# sourceMappingURL=amanpayService.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"amanpayService.js","sourceRoot":"","sources":["../../src/services/amanpayService.ts"],"names":[],"mappings":";;;;;AAiCA,wCA6BC;AAED,wCAMC;AAED,8CAWC;AAED,wDAOC;AAED,oCAEC;AAhGD,oDAA2B;AAE3B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,yBAAyB,CAAA;AAC1E,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,EAAE,CAAA;AACzD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAA;AACvD,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,EAAE,CAAA;AAoB/D,KAAK,UAAU,cAAc;IAC3B,OAAO;QACL,cAAc,EAAE,kBAAkB;QAClC,eAAe,EAAE,WAAW;QAC5B,WAAW,EAAE,UAAU;KACxB,CAAA;AACH,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,MAA2B;IAC9D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,cAAc,EAAE;QACjD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,MAAM,cAAc,EAAE;QAC/B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,WAAW,EAAE,WAAW;YACxB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,OAAO;YACxB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,cAAc,EAAE,MAAM,CAAC,aAAa;YACpC,aAAa,EAAE,MAAM,CAAC,YAAY;YAClC,WAAW,EAAE,MAAM,CAAC,UAAU;YAC9B,WAAW,EAAE,MAAM,CAAC,UAAU;YAC9B,WAAW,EAAE,MAAM,CAAC,UAAU;SAC/B,CAAC;KACH,CAAC,CAAA;IAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAA4B,CAAA;QACzE,MAAM,IAAI,KAAK,CAAC,4BAA6B,GAAG,EAAE,OAAkB,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAA;IAC3F,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAA6B,CAAA;IACxD,OAAO;QACL,aAAa,EAAE,CAAC,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,EAAE,CAAW;QACzD,WAAW,EAAE,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,WAAW,CAAW;QAC9D,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,SAAS,CAAW;KAC7C,CAAA;AACH,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,aAAqB;IACxD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,gBAAgB,aAAa,EAAE,EAAE;QAClE,OAAO,EAAE,MAAM,cAAc,EAAE;KAChC,CAAC,CAAA;IACF,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAA;IACpE,OAAO,GAAG,CAAC,IAAI,EAAE,CAAA;AACnB,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,aAAqB,EAAE,MAAc,EAAE,MAAe;IAC5F,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,gBAAgB,aAAa,SAAS,EAAE;QACzE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,MAAM,cAAc,EAAE;QAC/B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;KACzC,CAAC,CAAA;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAA4B,CAAA;QACzE,MAAM,IAAI,KAAK,CAAC,0BAA2B,GAAG,EAAE,OAAkB,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAA;IACzF,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,EAAE,CAAA;AACnB,CAAC;AAED,SAAgB,sBAAsB,CAAC,OAAe,EAAE,SAAiB;IACvE,IAAI,CAAC,cAAc;QAAE,OAAO,KAAK,CAAA;IACjC,MAAM,QAAQ,GAAG,gBAAM;SACpB,UAAU,CAAC,QAAQ,EAAE,cAAc,CAAC;SACpC,MAAM,CAAC,OAAO,CAAC;SACf,MAAM,CAAC,KAAK,CAAC,CAAA;IAChB,OAAO,gBAAM,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;AAC9E,CAAC;AAED,SAAgB,YAAY;IAC1B,OAAO,CAAC,CAAC,CAAC,WAAW,IAAI,UAAU,IAAI,WAAW,KAAK,0BAA0B,CAAC,CAAA;AACpF,CAAC"}
+17
View File
@@ -0,0 +1,17 @@
export declare function createCompanyContainer(company: {
id: string;
slug: string;
name: string;
}): Promise<void>;
export declare function startCompanyContainer(companyId: string): Promise<void>;
export declare function stopCompanyContainer(companyId: string): Promise<void>;
export declare function restartCompanyContainer(companyId: string): Promise<void>;
export declare function removeCompanyContainer(companyId: string): Promise<void>;
export declare function redeployCompanyContainer(company: {
id: string;
slug: string;
name: string;
}): Promise<void>;
export declare function getContainerLogs(companyId: string, tail?: number): Promise<string>;
export declare function syncContainerStatuses(): Promise<void>;
//# sourceMappingURL=containerService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"containerService.d.ts","sourceRoot":"","sources":["../../src/services/containerService.ts"],"names":[],"mappings":"AA4SA,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CAoChB;AAED,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5E;AAED,wBAAsB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9E;AAED,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB7E;AAED,wBAAsB,wBAAwB,CAAC,OAAO,EAAE;IACtD,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhB;AAED,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBrF;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkC3D"}
+421
View File
@@ -0,0 +1,421 @@
"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.createCompanyContainer = createCompanyContainer;
exports.startCompanyContainer = startCompanyContainer;
exports.stopCompanyContainer = stopCompanyContainer;
exports.restartCompanyContainer = restartCompanyContainer;
exports.removeCompanyContainer = removeCompanyContainer;
exports.redeployCompanyContainer = redeployCompanyContainer;
exports.getContainerLogs = getContainerLogs;
exports.syncContainerStatuses = syncContainerStatuses;
const child_process_1 = require("child_process");
const util_1 = require("util");
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const prisma_1 = require("../lib/prisma");
const db = prisma_1.prisma;
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
let composeCommand = null;
// ── Config ────────────────────────────────────────────────────────────────────
const COMPOSE_DIR = process.env.COMPANIES_COMPOSE_DIR || '/opt/companies';
const COMPOSE_FILE = path.join(COMPOSE_DIR, 'docker-compose.companies.yml');
const SERVICES_FILE = path.join(COMPOSE_DIR, 'companies-services.json'); // our source of truth
const DASHBOARD_IMAGE = process.env.DASHBOARD_CONTAINER_IMAGE || 'rentaldrivego/dashboard:latest';
const PORT_RANGE_START = parseInt(process.env.CONTAINER_PORT_START || '5100', 10);
const PORT_RANGE_END = parseInt(process.env.CONTAINER_PORT_END || '5999', 10);
const API_INTERNAL_URL = process.env.API_INTERNAL_URL || 'http://api:4000';
const CONTAINER_NETWORK = process.env.CONTAINER_NETWORK || 'rentaldrivego_default';
// ── Internal helpers ──────────────────────────────────────────────────────────
function serviceName(slug) {
return `company-${slug}`;
}
function containerName(slug) {
return `rdg-company-${slug}`;
}
async function ensureDir() {
await fs.mkdir(COMPOSE_DIR, { recursive: true });
}
async function readServices() {
try {
const raw = await fs.readFile(SERVICES_FILE, 'utf8');
return JSON.parse(raw);
}
catch {
return {};
}
}
async function writeServices(services) {
await ensureDir();
await fs.writeFile(SERVICES_FILE, JSON.stringify(services, null, 2), 'utf8');
await fs.writeFile(COMPOSE_FILE, buildComposeYaml(services), 'utf8');
}
async function composeFilesExist() {
try {
await fs.access(COMPOSE_FILE);
await fs.access(SERVICES_FILE);
return true;
}
catch {
return false;
}
}
async function rebuildServicesFromDatabase() {
const records = await db.companyContainer.findMany({
include: { company: { select: { slug: true } } },
});
return Object.fromEntries(records.map((record) => [
serviceName(record.company.slug),
{
companyId: record.companyId,
slug: record.company.slug,
image: record.image,
port: record.port,
},
]));
}
async function ensureComposeFiles() {
if (await composeFilesExist())
return;
const services = await rebuildServicesFromDatabase();
await writeServices(services);
}
function buildComposeYaml(services) {
const lines = ['services:'];
for (const [name, svc] of Object.entries(services)) {
lines.push(` ${name}:`);
lines.push(` image: "${svc.image}"`);
lines.push(` container_name: "${containerName(svc.slug)}"`);
lines.push(` restart: unless-stopped`);
lines.push(` environment:`);
lines.push(` COMPANY_ID: "${svc.companyId}"`);
lines.push(` COMPANY_SLUG: "${svc.slug}"`);
lines.push(` API_URL: "${API_INTERNAL_URL}"`);
lines.push(` NEXT_PUBLIC_API_URL: "${API_INTERNAL_URL}"`);
lines.push(` PORT: "3000"`);
lines.push(` NODE_ENV: "production"`);
lines.push(` ports:`);
lines.push(` - "${svc.port}:3000"`);
lines.push(` networks:`);
lines.push(` - ${CONTAINER_NETWORK}`);
lines.push(` labels:`);
lines.push(` rdg.managed: "true"`);
lines.push(` rdg.company.id: "${svc.companyId}"`);
lines.push(` rdg.company.slug: "${svc.slug}"`);
}
lines.push('');
lines.push('networks:');
lines.push(` ${CONTAINER_NETWORK}:`);
lines.push(` external: true`);
lines.push('');
return lines.join('\n');
}
async function resolveComposeCommand() {
if (composeCommand)
return composeCommand;
try {
await execFileAsync('docker-compose', ['version']);
composeCommand = 'docker-compose';
return composeCommand;
}
catch {
try {
await execFileAsync('docker', ['compose', 'version']);
composeCommand = 'docker';
return composeCommand;
}
catch (err) {
throw mapDockerError(err);
}
}
}
async function compose(...args) {
try {
await ensureComposeFiles();
const command = await resolveComposeCommand();
if (command === 'docker-compose') {
return await execFileAsync('docker-compose', ['-f', COMPOSE_FILE, ...args]);
}
return await execFileAsync('docker', ['compose', '-f', COMPOSE_FILE, ...args]);
}
catch (err) {
throw mapDockerError(err);
}
}
function dockerUnavailableError(message, details) {
const err = Object.assign(new Error(message), {
statusCode: 503,
code: 'docker_unavailable',
});
if (details) {
;
err.details = details;
}
return err;
}
function mapDockerError(err) {
if (err && typeof err === 'object') {
const error = err;
const stderr = error.stderr?.trim();
if (error.code === 'ENOENT' && error.path === 'docker') {
return dockerUnavailableError('Docker CLI is not available to the API service.', 'Install Docker in the API container or run the API on a host with Docker available in PATH.');
}
if (error.code === 'ENOENT' && error.path === 'docker-compose') {
return dockerUnavailableError('Docker Compose is not available to the API service.', 'Install Docker Compose in the API container or switch the service to a runtime that supports `docker compose`.');
}
if (stderr?.includes("docker: 'compose' is not a docker command.") ||
stderr?.includes("unknown shorthand flag: 'f' in -f")) {
return dockerUnavailableError('Docker Compose is not available to the API service.', 'Install Docker Compose in the API container or switch the service to a runtime that supports `docker compose`.');
}
if (stderr?.includes('Cannot connect to the Docker daemon') ||
stderr?.includes('permission denied while trying to connect to the Docker daemon socket') ||
stderr?.includes('error during connect')) {
return dockerUnavailableError('Docker daemon is not reachable from the API service.', 'Mount `/var/run/docker.sock` into the API container and ensure the Docker daemon is running.');
}
// Catch-all: docker ran but exited non-zero — surface stderr as a readable 502
if (typeof error.code === 'number' && error.code !== 0) {
const detail = stderr || error.stdout?.trim() || 'docker compose exited with a non-zero status';
return Object.assign(new Error(detail), { statusCode: 502, code: 'docker_error' });
}
}
return err;
}
async function setContainerError(companyId, message) {
await db.companyContainer.update({
where: { companyId },
data: { status: 'ERROR', errorMessage: message },
}).catch(() => null);
}
async function runContainerAction(companyId, slug, command, successStatus, preStatus) {
if (preStatus) {
await db.companyContainer.update({ where: { companyId }, data: { status: preStatus, errorMessage: null } });
}
try {
// Use `up -d --no-recreate` for start so it creates the container if it doesn't exist yet
const args = command === 'start'
? ['up', '-d', '--no-recreate', serviceName(slug)]
: [command, serviceName(slug)];
await compose(...args);
await db.companyContainer.update({
where: { companyId },
data: { status: successStatus, errorMessage: null },
});
}
catch (err) {
const message = err instanceof Error ? err.message : `Failed to ${command} container`;
await setContainerError(companyId, message);
throw err;
}
}
async function allocatePort() {
const last = await db.companyContainer.findFirst({
orderBy: { port: 'desc' },
select: { port: true },
});
const next = last ? last.port + 1 : PORT_RANGE_START;
if (next > PORT_RANGE_END)
throw new Error('No available ports in container port range');
return next;
}
async function getDockerServiceId(slug) {
try {
const name = serviceName(slug);
const { stdout } = await compose('ps', '--format', 'json', name);
const line = stdout.trim().split('\n')[0];
if (!line)
return null;
const info = JSON.parse(line);
return info.ID ?? info.Id ?? null;
}
catch {
return null;
}
}
// Maps docker compose State strings to our DB enum
function mapDockerState(state) {
switch (state.toLowerCase()) {
case 'running': return 'RUNNING';
case 'restarting': return 'RESTARTING';
case 'exited':
case 'created':
case 'paused': return 'STOPPED';
default: return 'ERROR';
}
}
// ── Public API ────────────────────────────────────────────────────────────────
async function createCompanyContainer(company) {
const name = serviceName(company.slug);
const port = await allocatePort();
const record = await db.companyContainer.create({
data: {
companyId: company.id,
containerName: containerName(company.slug),
status: 'CREATING',
port,
image: DASHBOARD_IMAGE,
},
});
try {
const services = await readServices();
services[name] = { companyId: company.id, slug: company.slug, image: DASHBOARD_IMAGE, port };
await writeServices(services);
await compose('up', '-d', '--no-recreate', name);
const dockerId = await getDockerServiceId(company.slug);
await db.companyContainer.update({
where: { id: record.id },
data: { dockerId, status: 'RUNNING' },
});
}
catch (err) {
await db.companyContainer.update({
where: { id: record.id },
data: {
status: 'ERROR',
errorMessage: err instanceof Error ? err.message : 'Unknown error during service creation',
},
});
throw err;
}
}
async function startCompanyContainer(companyId) {
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
});
await runContainerAction(companyId, record.company.slug, 'start', 'RUNNING');
}
async function stopCompanyContainer(companyId) {
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
});
await runContainerAction(companyId, record.company.slug, 'stop', 'STOPPED');
}
async function restartCompanyContainer(companyId) {
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
});
await runContainerAction(companyId, record.company.slug, 'restart', 'RUNNING', 'RESTARTING');
}
async function removeCompanyContainer(companyId) {
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
});
const name = serviceName(record.company.slug);
await db.companyContainer.update({ where: { companyId }, data: { status: 'REMOVING' } });
try {
await compose('stop', name);
}
catch { /* already stopped */ }
try {
await compose('rm', '-f', name);
}
catch { /* already removed */ }
const services = await readServices();
delete services[name];
await writeServices(services);
await db.companyContainer.delete({ where: { companyId } });
}
async function redeployCompanyContainer(company) {
const name = serviceName(company.slug);
const existing = await db.companyContainer.findUnique({ where: { companyId: company.id } });
if (existing) {
try {
await compose('stop', name);
}
catch { /* ok */ }
try {
await compose('rm', '-f', name);
}
catch { /* ok */ }
await db.companyContainer.delete({ where: { companyId: company.id } });
}
// Remove from compose file too, then recreate
const services = await readServices();
delete services[name];
await writeServices(services);
await createCompanyContainer(company);
}
async function getContainerLogs(companyId, tail = 150) {
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
});
try {
const { stdout } = await compose('logs', '--no-log-prefix', `--tail=${tail}`, serviceName(record.company.slug));
return stdout;
}
catch {
return '';
}
}
async function syncContainerStatuses() {
const records = await db.companyContainer.findMany({
where: { status: { notIn: ['PENDING', 'CREATING', 'REMOVING'] } },
include: { company: { select: { slug: true } } },
});
try {
// Get all compose service states in one shot
const { stdout } = await compose('ps', '--format', 'json');
const rows = stdout
.trim()
.split('\n')
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line);
}
catch {
return null;
}
})
.filter(Boolean);
const stateByService = Object.fromEntries(rows.map((r) => [r.Service, r.State]));
await Promise.allSettled(records.map(async (rec) => {
const name = serviceName(rec.company.slug);
const dockerState = stateByService[name];
const status = dockerState ? mapDockerState(dockerState) : 'STOPPED';
if (rec.status !== status) {
await db.companyContainer.update({ where: { id: rec.id }, data: { status } });
}
}));
}
catch {
// Docker daemon unreachable — leave statuses as-is
}
}
//# sourceMappingURL=containerService.js.map
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
export declare function generateFinancialReport(companyId: string, startDate: Date, endDate: Date): Promise<{
summary: {
totalReservations: any;
totalRentalRevenue: any;
totalDiscounts: any;
totalInsurance: any;
totalAdditionalDrivers: any;
totalPricingRulesAdj: any;
totalDeposits: any;
totalCollected: any;
averageRentalDays: number;
};
rows: any;
period: {
startDate: Date;
endDate: Date;
};
}>;
export declare function toCsv(rows: Record<string, any>[]): string;
//# sourceMappingURL=financialReportService.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"financialReportService.d.ts","sourceRoot":"","sources":["../../src/services/financialReportService.ts"],"names":[],"mappings":"AAEA,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI;;;;;;;;;;;;;;;;;GAiD9F;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,MAAM,CAYzD"}
+65
View File
@@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateFinancialReport = generateFinancialReport;
exports.toCsv = toCsv;
const prisma_1 = require("../lib/prisma");
async function generateFinancialReport(companyId, startDate, endDate) {
const reservations = await prisma_1.prisma.reservation.findMany({
where: { companyId, status: { in: ['CONFIRMED', 'ACTIVE', 'COMPLETED'] }, startDate: { gte: startDate }, endDate: { lte: endDate } },
include: {
vehicle: { select: { make: true, model: true, year: true, licensePlate: true } },
customer: { select: { firstName: true, lastName: true, email: true } },
rentalPayments: { where: { status: 'SUCCEEDED' } },
insurances: true,
additionalDrivers: true,
},
orderBy: { startDate: 'asc' },
});
const summary = {
totalReservations: reservations.length,
totalRentalRevenue: reservations.reduce((s, r) => s + r.dailyRate * r.totalDays, 0),
totalDiscounts: reservations.reduce((s, r) => s + r.discountAmount, 0),
totalInsurance: reservations.reduce((s, r) => s + r.insuranceTotal, 0),
totalAdditionalDrivers: reservations.reduce((s, r) => s + r.additionalDriverTotal, 0),
totalPricingRulesAdj: reservations.reduce((s, r) => s + r.pricingRulesTotal, 0),
totalDeposits: reservations.reduce((s, r) => s + r.depositAmount, 0),
totalCollected: reservations.reduce((s, r) => s + r.totalAmount, 0),
averageRentalDays: reservations.length > 0 ? reservations.reduce((s, r) => s + r.totalDays, 0) / reservations.length : 0,
};
const rows = reservations.map((r) => ({
reservationId: r.id,
contractNumber: r.contractNumber ?? '—',
invoiceNumber: r.invoiceNumber ?? '—',
customerName: `${r.customer.firstName} ${r.customer.lastName}`,
vehicle: `${r.vehicle.year} ${r.vehicle.make} ${r.vehicle.model}`,
plate: r.vehicle.licensePlate,
startDate: r.startDate.toISOString().split('T')[0],
endDate: r.endDate.toISOString().split('T')[0],
days: r.totalDays,
dailyRate: r.dailyRate,
baseAmount: r.dailyRate * r.totalDays,
discount: r.discountAmount,
insurance: r.insuranceTotal,
additionalDriver: r.additionalDriverTotal,
pricingAdj: r.pricingRulesTotal,
deposit: r.depositAmount,
totalAmount: r.totalAmount,
paymentStatus: r.rentalPayments[0]?.status ?? 'UNPAID',
paymentMethod: r.rentalPayments[0]?.paymentMethod ?? '—',
source: r.source,
}));
return { summary, rows, period: { startDate, endDate } };
}
function toCsv(rows) {
if (rows.length === 0)
return '';
const headers = Object.keys(rows[0]);
return [
headers.join(','),
...rows.map((row) => headers.map((h) => {
const val = row[h] ?? '';
return typeof val === 'string' && val.includes(',') ? `"${val}"` : String(val);
}).join(',')),
].join('\n');
}
//# sourceMappingURL=financialReportService.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"financialReportService.js","sourceRoot":"","sources":["../../src/services/financialReportService.ts"],"names":[],"mappings":";;AAEA,0DAiDC;AAED,sBAYC;AAjED,0CAAsC;AAE/B,KAAK,UAAU,uBAAuB,CAAC,SAAiB,EAAE,SAAe,EAAE,OAAa;IAC7F,MAAM,YAAY,GAAG,MAAM,eAAM,CAAC,WAAW,CAAC,QAAQ,CAAC;QACrD,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;QACpI,OAAO,EAAE;YACP,OAAO,EAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE;YACjF,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACtE,cAAc,EAAE,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE;YAClD,UAAU,EAAE,IAAI;YAChB,iBAAiB,EAAE,IAAI;SACxB;QACD,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;KAC9B,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG;QACd,iBAAiB,EAAU,YAAY,CAAC,MAAM;QAC9C,kBAAkB,EAAS,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;QACjI,cAAc,EAAa,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC;QACxH,cAAc,EAAa,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC;QACxH,sBAAsB,EAAK,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,qBAAqB,EAAE,CAAC,CAAC;QAC/H,oBAAoB,EAAO,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAC3H,aAAa,EAAc,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;QACvH,cAAc,EAAa,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;QACrH,iBAAiB,EAAU,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,CAAgC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;KACxK,CAAA;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAgC,EAAE,EAAE,CAAC,CAAC;QACnE,aAAa,EAAK,CAAC,CAAC,EAAE;QACtB,cAAc,EAAI,CAAC,CAAC,cAAc,IAAI,GAAG;QACzC,aAAa,EAAK,CAAC,CAAC,aAAa,IAAI,GAAG;QACxC,YAAY,EAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,EAAE;QAClE,OAAO,EAAW,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE;QAC1E,KAAK,EAAa,CAAC,CAAC,OAAO,CAAC,YAAY;QACxC,SAAS,EAAS,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACzD,OAAO,EAAW,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,EAAc,CAAC,CAAC,SAAS;QAC7B,SAAS,EAAS,CAAC,CAAC,SAAS;QAC7B,UAAU,EAAQ,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS;QAC3C,QAAQ,EAAU,CAAC,CAAC,cAAc;QAClC,SAAS,EAAS,CAAC,CAAC,cAAc;QAClC,gBAAgB,EAAE,CAAC,CAAC,qBAAqB;QACzC,UAAU,EAAQ,CAAC,CAAC,iBAAiB;QACrC,OAAO,EAAW,CAAC,CAAC,aAAa;QACjC,WAAW,EAAO,CAAC,CAAC,WAAW;QAC/B,aAAa,EAAK,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,QAAQ;QACzD,aAAa,EAAK,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,aAAa,IAAI,GAAG;QAC3D,MAAM,EAAY,CAAC,CAAC,MAAM;KAC3B,CAAC,CAAC,CAAA;IAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,CAAA;AAC1D,CAAC;AAED,SAAgB,KAAK,CAAC,IAA2B;IAC/C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAChC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAA;IACrC,OAAO;QACL,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QACjB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAClB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAChB,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;YACxB,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChF,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CACb;KACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC"}
+15
View File
@@ -0,0 +1,15 @@
import { InsurancePolicy } from '@rentaldrivego/database';
export declare function calculateInsuranceCharge(policy: InsurancePolicy, totalDays: number, baseRentalAmount: number): number;
export declare function applyInsurancesToReservation(reservationId: string, companyId: string, selectedPolicyIds: string[], totalDays: number, baseRentalAmount: number): Promise<{
records: {
reservationId: string;
insurancePolicyId: any;
policyName: any;
policyType: any;
chargeType: any;
chargeValue: any;
totalCharge: number;
}[];
insuranceTotal: number;
}>;
//# sourceMappingURL=insuranceService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"insuranceService.d.ts","sourceRoot":"","sources":["../../src/services/insuranceService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAOR;AAED,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,EAAE,EAC3B,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM;;;;;;;;;;;GA+BzB"}
+41
View File
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateInsuranceCharge = calculateInsuranceCharge;
exports.applyInsurancesToReservation = applyInsurancesToReservation;
const prisma_1 = require("../lib/prisma");
function calculateInsuranceCharge(policy, totalDays, baseRentalAmount) {
switch (policy.chargeType) {
case 'PER_DAY': return policy.chargeValue * totalDays;
case 'PER_RENTAL': return policy.chargeValue;
case 'PERCENTAGE_OF_RENTAL': return Math.round(baseRentalAmount * policy.chargeValue / 100);
default: return 0;
}
}
async function applyInsurancesToReservation(reservationId, companyId, selectedPolicyIds, totalDays, baseRentalAmount) {
const allPolicies = await prisma_1.prisma.insurancePolicy.findMany({ where: { companyId, isActive: true } });
const required = allPolicies.filter((p) => p.isRequired);
const selected = allPolicies.filter((p) => selectedPolicyIds.includes(p.id) && !p.isRequired);
const toApply = [...required, ...selected];
const records = toApply.map((policy) => ({
reservationId,
insurancePolicyId: policy.id,
policyName: policy.name,
policyType: policy.type,
chargeType: policy.chargeType,
chargeValue: policy.chargeValue,
totalCharge: calculateInsuranceCharge(policy, totalDays, baseRentalAmount),
}));
const insuranceTotal = records.reduce((s, r) => s + r.totalCharge, 0);
await prisma_1.prisma.$transaction([
prisma_1.prisma.reservationInsurance.createMany({ data: records }),
prisma_1.prisma.reservation.update({
where: { id: reservationId },
data: {
insuranceTotal,
totalAmount: { increment: insuranceTotal },
},
}),
]);
return { records, insuranceTotal };
}
//# sourceMappingURL=insuranceService.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"insuranceService.js","sourceRoot":"","sources":["../../src/services/insuranceService.ts"],"names":[],"mappings":";;AAGA,4DAWC;AAED,oEAoCC;AAnDD,0CAAsC;AAEtC,SAAgB,wBAAwB,CACtC,MAAuB,EACvB,SAAiB,EACjB,gBAAwB;IAExB,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC;QAC1B,KAAK,SAAS,CAAC,CAAe,OAAO,MAAM,CAAC,WAAW,GAAG,SAAS,CAAA;QACnE,KAAK,YAAY,CAAC,CAAY,OAAO,MAAM,CAAC,WAAW,CAAA;QACvD,KAAK,sBAAsB,CAAC,CAAE,OAAO,IAAI,CAAC,KAAK,CAAC,gBAAgB,GAAG,MAAM,CAAC,WAAW,GAAG,GAAG,CAAC,CAAA;QAC5F,OAAO,CAAC,CAAsB,OAAO,CAAC,CAAA;IACxC,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,4BAA4B,CAChD,aAAqB,EACrB,SAAiB,EACjB,iBAA2B,EAC3B,SAAiB,EACjB,gBAAwB;IAExB,MAAM,WAAW,GAAG,MAAM,eAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IACnG,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAkB,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;IACzE,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAkB,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;IAC9G,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,EAAE,GAAG,QAAQ,CAAC,CAAA;IAE1C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACvC,aAAa;QACb,iBAAiB,EAAE,MAAM,CAAC,EAAE;QAC5B,UAAU,EAAE,MAAM,CAAC,IAAI;QACvB,UAAU,EAAE,MAAM,CAAC,IAAI;QACvB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,WAAW,EAAE,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,gBAAgB,CAAC;KAC3E,CAAC,CAAC,CAAA;IAEH,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;IAErE,MAAM,eAAM,CAAC,YAAY,CAAC;QACxB,eAAM,CAAC,oBAAoB,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QACzD,eAAM,CAAC,WAAW,CAAC,MAAM,CAAC;YACxB,KAAK,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE;YAC5B,IAAI,EAAE;gBACJ,cAAc;gBACd,WAAW,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE;aAC3C;SACF,CAAC;KACH,CAAC,CAAA;IAEF,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,CAAA;AACpC,CAAC"}
+46
View File
@@ -0,0 +1,46 @@
interface InvoiceData {
invoiceNumber: string;
issueDate: string;
dueDate: string | null;
company: {
name: string;
email: string;
phone?: string | null;
address?: any;
};
subscription?: {
plan: string;
billingPeriod: string;
currentPeriodStart?: string | null;
currentPeriodEnd?: string | null;
currency: string;
};
amount: number;
currency: string;
status: string;
paymentProvider: string;
transactionId?: string | null;
paidAt?: string | null;
lineItems?: Array<{
description: string;
amount: number;
currency: string;
quantity?: number;
unitAmount?: number;
periodStart?: string | null;
periodEnd?: string | null;
}>;
totals?: {
subtotalAmount: number;
discountAmount: number;
creditAmount: number;
taxAmount: number;
totalAmount: number;
amountPaid: number;
amountDue: number;
};
}
export declare function generateInvoicePdf(data: InvoiceData): Promise<Buffer>;
export declare function buildInvoiceNumber(invoiceId: string, createdAt: Date): string;
export {};
//# sourceMappingURL=invoicePdfService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"invoicePdfService.d.ts","sourceRoot":"","sources":["../../src/services/invoicePdfService.ts"],"names":[],"mappings":"AAGA,UAAU,WAAW;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACrB,OAAO,CAAC,EAAE,GAAG,CAAA;KACd,CAAA;IACD,YAAY,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,aAAa,EAAE,MAAM,CAAA;QACrB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAClC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAChC,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,CAAC,EAAE,KAAK,CAAC;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,CAAC,CAAA;IACF,MAAM,CAAC,EAAE;QACP,cAAc,EAAE,MAAM,CAAA;QACtB,cAAc,EAAE,MAAM,CAAA;QACtB,YAAY,EAAE,MAAM,CAAA;QACpB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,UAAU,EAAE,MAAM,CAAA;QAClB,SAAS,EAAE,MAAM,CAAA;KAClB,CAAA;CACF;AAmgBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAI3E;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,MAAM,CAI7E"}
+361
View File
@@ -0,0 +1,361 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateInvoicePdf = generateInvoicePdf;
exports.buildInvoiceNumber = buildInvoiceNumber;
const react_1 = __importDefault(require("react"));
const renderer_1 = require("@react-pdf/renderer");
const PLAN_LABEL = {
STARTER: 'Starter',
GROWTH: 'Growth',
PRO: 'Pro',
};
const PERIOD_LABEL = {
MONTHLY: 'Monthly',
ANNUAL: 'Annual',
};
const STATUS_COLORS = {
PAID: '#10b981',
PENDING: '#f59e0b',
OPEN: '#f59e0b',
PAYMENT_PENDING: '#f59e0b',
PARTIALLY_PAID: '#f59e0b',
PAST_DUE: '#ef4444',
FAILED: '#ef4444',
REFUNDED: '#6b7280',
PARTIALLY_REFUNDED: '#6b7280',
VOID: '#6b7280',
UNCOLLECTIBLE: '#6b7280',
};
const colors = {
bg: '#0f0f11',
surface: '#18181b',
border: '#27272a',
textPrimary: '#f4f4f5',
textSecondary: '#a1a1aa',
textMuted: '#71717a',
accent: '#10b981',
white: '#ffffff',
};
const s = renderer_1.StyleSheet.create({
page: {
backgroundColor: colors.bg,
paddingHorizontal: 48,
paddingVertical: 48,
fontFamily: 'Helvetica',
color: colors.textPrimary,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 40,
paddingBottom: 24,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
brandName: {
fontSize: 22,
fontFamily: 'Helvetica-Bold',
color: colors.accent,
letterSpacing: 0.5,
},
brandTagline: {
fontSize: 9,
color: colors.textMuted,
marginTop: 3,
},
invoiceMeta: {
alignItems: 'flex-end',
},
invoiceTitle: {
fontSize: 26,
fontFamily: 'Helvetica-Bold',
color: colors.white,
letterSpacing: 1,
},
invoiceNumber: {
fontSize: 11,
color: colors.textSecondary,
marginTop: 4,
},
section: {
marginBottom: 24,
},
sectionLabel: {
fontSize: 8,
fontFamily: 'Helvetica-Bold',
color: colors.textMuted,
letterSpacing: 1.2,
textTransform: 'uppercase',
marginBottom: 8,
},
row: {
flexDirection: 'row',
gap: 24,
marginBottom: 24,
},
col: {
flex: 1,
backgroundColor: colors.surface,
borderRadius: 8,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
},
colLabel: {
fontSize: 8,
fontFamily: 'Helvetica-Bold',
color: colors.textMuted,
letterSpacing: 1,
textTransform: 'uppercase',
marginBottom: 10,
},
companyName: {
fontSize: 13,
fontFamily: 'Helvetica-Bold',
color: colors.textPrimary,
marginBottom: 4,
},
companyDetail: {
fontSize: 10,
color: colors.textSecondary,
marginBottom: 2,
},
metaRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 6,
},
metaKey: {
fontSize: 9,
color: colors.textMuted,
},
metaValue: {
fontSize: 9,
color: colors.textSecondary,
fontFamily: 'Helvetica-Bold',
},
table: {
backgroundColor: colors.surface,
borderRadius: 8,
borderWidth: 1,
borderColor: colors.border,
overflow: 'hidden',
marginBottom: 20,
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#1c1c1f',
paddingHorizontal: 16,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
tableHeaderCell: {
fontSize: 8,
fontFamily: 'Helvetica-Bold',
color: colors.textMuted,
letterSpacing: 0.8,
textTransform: 'uppercase',
},
tableRow: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
tableCell: {
fontSize: 10,
color: colors.textPrimary,
},
tableCellMuted: {
fontSize: 10,
color: colors.textSecondary,
},
col60: { flex: 6 },
col20: { flex: 2, textAlign: 'right' },
col20Center: { flex: 2, textAlign: 'center' },
totalRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginBottom: 6,
},
totalLabel: {
fontSize: 11,
color: colors.textSecondary,
marginRight: 32,
},
totalValue: {
fontSize: 11,
fontFamily: 'Helvetica-Bold',
color: colors.white,
minWidth: 100,
textAlign: 'right',
},
grandTotalRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginTop: 8,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: colors.border,
},
grandTotalLabel: {
fontSize: 13,
fontFamily: 'Helvetica-Bold',
color: colors.textSecondary,
marginRight: 32,
},
grandTotalValue: {
fontSize: 15,
fontFamily: 'Helvetica-Bold',
color: colors.accent,
minWidth: 100,
textAlign: 'right',
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
alignSelf: 'flex-start',
marginTop: 6,
},
statusText: {
fontSize: 9,
fontFamily: 'Helvetica-Bold',
letterSpacing: 0.5,
color: colors.white,
},
paymentBox: {
backgroundColor: colors.surface,
borderRadius: 8,
padding: 16,
borderWidth: 1,
borderColor: colors.border,
marginBottom: 24,
},
paymentRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
paymentKey: {
fontSize: 9,
color: colors.textMuted,
},
paymentValue: {
fontSize: 9,
color: colors.textSecondary,
fontFamily: 'Helvetica-Bold',
maxWidth: 280,
textAlign: 'right',
},
footer: {
position: 'absolute',
bottom: 32,
left: 48,
right: 48,
borderTopWidth: 1,
borderTopColor: colors.border,
paddingTop: 12,
flexDirection: 'row',
justifyContent: 'space-between',
},
footerText: {
fontSize: 8,
color: colors.textMuted,
},
});
function fmt(amount, currency) {
return `${(amount / 100).toFixed(2)} ${currency}`;
}
function fmtDate(iso) {
if (!iso)
return '—';
return new Date(iso).toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' });
}
function formatAddress(address) {
if (!address)
return '';
if (typeof address === 'string')
return address;
const parts = [address.street, address.city, address.region, address.country, address.zip];
return parts.filter(Boolean).join(', ');
}
function InvoiceDocument({ data }) {
const statusColor = STATUS_COLORS[data.status] ?? '#6b7280';
const addressStr = formatAddress(data.company.address);
const planLabel = data.subscription ? (PLAN_LABEL[data.subscription.plan] ?? data.subscription.plan) : 'Manual';
const periodLabel = data.subscription ? (PERIOD_LABEL[data.subscription.billingPeriod] ?? data.subscription.billingPeriod) : 'Custom';
const lineItems = data.lineItems?.length
? data.lineItems
: [{
description: `${planLabel} Plan — ${periodLabel} Subscription`,
amount: data.amount,
currency: data.currency,
periodStart: data.subscription?.currentPeriodStart ?? null,
periodEnd: data.subscription?.currentPeriodEnd ?? null,
}];
const totals = data.totals ?? {
subtotalAmount: data.amount,
discountAmount: 0,
creditAmount: 0,
taxAmount: 0,
totalAmount: data.amount,
amountPaid: data.paidAt ? data.amount : 0,
amountDue: data.paidAt ? 0 : data.amount,
};
return react_1.default.createElement(renderer_1.Document, { author: 'RentalDriveGo', title: `Invoice ${data.invoiceNumber}`, subject: 'Subscription Invoice' }, react_1.default.createElement(renderer_1.Page, { size: 'A4', style: s.page },
// ── Header ──────────────────────────────────────────────────
react_1.default.createElement(renderer_1.View, { style: s.header }, react_1.default.createElement(renderer_1.View, null, react_1.default.createElement(renderer_1.Text, { style: s.brandName }, 'RentalDriveGo'), react_1.default.createElement(renderer_1.Text, { style: s.brandTagline }, 'Car Rental Management Platform')), react_1.default.createElement(renderer_1.View, { style: s.invoiceMeta }, react_1.default.createElement(renderer_1.Text, { style: s.invoiceTitle }, 'INVOICE'), react_1.default.createElement(renderer_1.Text, { style: s.invoiceNumber }, data.invoiceNumber), react_1.default.createElement(renderer_1.View, { style: [s.statusBadge, { backgroundColor: statusColor }] }, react_1.default.createElement(renderer_1.Text, { style: s.statusText }, data.status)))),
// ── Bill To / Invoice Dates ──────────────────────────────────
react_1.default.createElement(renderer_1.View, { style: s.row }, react_1.default.createElement(renderer_1.View, { style: s.col }, react_1.default.createElement(renderer_1.Text, { style: s.colLabel }, 'Bill To'), react_1.default.createElement(renderer_1.Text, { style: s.companyName }, data.company.name), react_1.default.createElement(renderer_1.Text, { style: s.companyDetail }, data.company.email), data.company.phone
? react_1.default.createElement(renderer_1.Text, { style: s.companyDetail }, data.company.phone)
: null, addressStr
? react_1.default.createElement(renderer_1.Text, { style: [s.companyDetail, { marginTop: 4 }] }, addressStr)
: null), react_1.default.createElement(renderer_1.View, { style: s.col }, react_1.default.createElement(renderer_1.Text, { style: s.colLabel }, 'Invoice Details'), react_1.default.createElement(renderer_1.View, { style: s.metaRow }, react_1.default.createElement(renderer_1.Text, { style: s.metaKey }, 'Invoice Number'), react_1.default.createElement(renderer_1.Text, { style: s.metaValue }, data.invoiceNumber)), react_1.default.createElement(renderer_1.View, { style: s.metaRow }, react_1.default.createElement(renderer_1.Text, { style: s.metaKey }, 'Issue Date'), react_1.default.createElement(renderer_1.Text, { style: s.metaValue }, fmtDate(data.issueDate))), data.dueDate
? react_1.default.createElement(renderer_1.View, { style: s.metaRow }, react_1.default.createElement(renderer_1.Text, { style: s.metaKey }, 'Due Date'), react_1.default.createElement(renderer_1.Text, { style: s.metaValue }, fmtDate(data.dueDate)))
: null, react_1.default.createElement(renderer_1.View, { style: s.metaRow }, react_1.default.createElement(renderer_1.Text, { style: s.metaKey }, 'Currency'), react_1.default.createElement(renderer_1.Text, { style: s.metaValue }, data.currency)), react_1.default.createElement(renderer_1.View, { style: s.metaRow }, react_1.default.createElement(renderer_1.Text, { style: s.metaKey }, 'Billing Period'), react_1.default.createElement(renderer_1.Text, { style: s.metaValue }, periodLabel)), data.subscription
? react_1.default.createElement(renderer_1.View, { style: s.metaRow }, react_1.default.createElement(renderer_1.Text, { style: s.metaKey }, 'Plan'), react_1.default.createElement(renderer_1.Text, { style: s.metaValue }, planLabel))
: null)),
// ── Line Items ───────────────────────────────────────────────
react_1.default.createElement(renderer_1.View, { style: s.table }, react_1.default.createElement(renderer_1.View, { style: s.tableHeader }, react_1.default.createElement(renderer_1.Text, { style: [s.tableHeaderCell, s.col60] }, 'Description'), react_1.default.createElement(renderer_1.Text, { style: [s.tableHeaderCell, s.col20Center] }, 'Period'), react_1.default.createElement(renderer_1.Text, { style: [s.tableHeaderCell, s.col20] }, 'Amount')), ...lineItems.map((item) => {
const periodStr = item.periodStart && item.periodEnd
? `${fmtDate(item.periodStart)} ${fmtDate(item.periodEnd)}`
: '—';
const description = item.quantity && item.quantity > 1 && item.unitAmount !== undefined
? `${item.description} (${item.quantity} × ${fmt(item.unitAmount, item.currency)})`
: item.description;
return react_1.default.createElement(renderer_1.View, { key: `${item.description}-${item.amount}-${item.periodStart ?? ''}`, style: s.tableRow }, react_1.default.createElement(renderer_1.Text, { style: [s.tableCell, s.col60] }, description), react_1.default.createElement(renderer_1.Text, { style: [s.tableCellMuted, s.col20Center, { textAlign: 'center' }] }, periodStr), react_1.default.createElement(renderer_1.Text, { style: [s.tableCell, s.col20, { textAlign: 'right' }] }, fmt(item.amount, item.currency)));
})),
// ── Totals ───────────────────────────────────────────────────
react_1.default.createElement(renderer_1.View, { style: s.totalRow }, react_1.default.createElement(renderer_1.Text, { style: s.totalLabel }, 'Subtotal'), react_1.default.createElement(renderer_1.Text, { style: s.totalValue }, fmt(totals.subtotalAmount, data.currency))), totals.discountAmount > 0
? react_1.default.createElement(renderer_1.View, { style: s.totalRow }, react_1.default.createElement(renderer_1.Text, { style: s.totalLabel }, 'Discounts'), react_1.default.createElement(renderer_1.Text, { style: s.totalValue }, `- ${fmt(totals.discountAmount, data.currency)}`))
: null, totals.creditAmount > 0
? react_1.default.createElement(renderer_1.View, { style: s.totalRow }, react_1.default.createElement(renderer_1.Text, { style: s.totalLabel }, 'Credits'), react_1.default.createElement(renderer_1.Text, { style: s.totalValue }, `- ${fmt(totals.creditAmount, data.currency)}`))
: null, totals.taxAmount > 0
? react_1.default.createElement(renderer_1.View, { style: s.totalRow }, react_1.default.createElement(renderer_1.Text, { style: s.totalLabel }, 'Tax'), react_1.default.createElement(renderer_1.Text, { style: s.totalValue }, fmt(totals.taxAmount, data.currency)))
: null, react_1.default.createElement(renderer_1.View, { style: s.grandTotalRow }, react_1.default.createElement(renderer_1.Text, { style: s.grandTotalLabel }, 'Total Due'), react_1.default.createElement(renderer_1.Text, { style: s.grandTotalValue }, fmt(totals.totalAmount, data.currency))),
// ── Payment Info ─────────────────────────────────────────────
react_1.default.createElement(renderer_1.View, { style: [s.paymentBox, { marginTop: 24 }] }, react_1.default.createElement(renderer_1.Text, { style: [s.colLabel, { marginBottom: 10 }] }, 'Payment Information'), react_1.default.createElement(renderer_1.View, { style: s.paymentRow }, react_1.default.createElement(renderer_1.Text, { style: s.paymentKey }, 'Payment Provider'), react_1.default.createElement(renderer_1.Text, { style: s.paymentValue }, data.paymentProvider)), data.transactionId
? react_1.default.createElement(renderer_1.View, { style: s.paymentRow }, react_1.default.createElement(renderer_1.Text, { style: s.paymentKey }, 'Transaction ID'), react_1.default.createElement(renderer_1.Text, { style: s.paymentValue }, data.transactionId))
: null, react_1.default.createElement(renderer_1.View, { style: s.paymentRow }, react_1.default.createElement(renderer_1.Text, { style: s.paymentKey }, 'Payment Date'), react_1.default.createElement(renderer_1.Text, { style: s.paymentValue }, fmtDate(data.paidAt))), react_1.default.createElement(renderer_1.View, { style: s.paymentRow }, react_1.default.createElement(renderer_1.Text, { style: s.paymentKey }, 'Status'), react_1.default.createElement(renderer_1.Text, { style: [s.paymentValue, { color: statusColor }] }, data.status))),
// ── Footer ───────────────────────────────────────────────────
react_1.default.createElement(renderer_1.View, { style: s.footer }, react_1.default.createElement(renderer_1.Text, { style: s.footerText }, 'RentalDriveGo — Car Rental Management Platform'), react_1.default.createElement(renderer_1.Text, { style: s.footerText }, `Generated on ${fmtDate(new Date().toISOString())}`))));
}
async function generateInvoicePdf(data) {
const doc = react_1.default.createElement(InvoiceDocument, { data });
const buffer = await (0, renderer_1.renderToBuffer)(doc);
return buffer;
}
function buildInvoiceNumber(invoiceId, createdAt) {
const year = createdAt.getFullYear();
const short = invoiceId.slice(-6).toUpperCase();
return `INV-${year}-${short}`;
}
//# sourceMappingURL=invoicePdfService.js.map
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
export interface LicenseValidationResult {
status: 'VALID' | 'EXPIRING' | 'EXPIRED';
daysUntilExpiry: number | null;
requiresApproval: boolean;
message: string;
}
export declare function validateLicense(licenseExpiry: Date | null): LicenseValidationResult;
export declare function validateAndFlagLicense(customerId: string, companyId?: string): Promise<LicenseValidationResult>;
//# sourceMappingURL=licenseValidationService.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"licenseValidationService.d.ts","sourceRoot":"","sources":["../../src/services/licenseValidationService.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;IACxC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gBAAgB,EAAE,OAAO,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,eAAe,CAAC,aAAa,EAAE,IAAI,GAAG,IAAI,GAAG,uBAAuB,CAiBnF;AAED,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,oCAkClF"}
+53
View File
@@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateLicense = validateLicense;
exports.validateAndFlagLicense = validateAndFlagLicense;
const prisma_1 = require("../lib/prisma");
const THREE_MONTHS_MS = 3 * 30 * 24 * 60 * 60 * 1000;
function validateLicense(licenseExpiry) {
if (!licenseExpiry) {
return { status: 'VALID', daysUntilExpiry: null, requiresApproval: false, message: 'No expiry date on record' };
}
const now = new Date();
const daysUntilExpiry = Math.ceil((licenseExpiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (licenseExpiry <= now) {
return { status: 'EXPIRED', daysUntilExpiry, requiresApproval: true, message: `License expired ${Math.abs(daysUntilExpiry)} day(s) ago` };
}
if (licenseExpiry.getTime() - now.getTime() < THREE_MONTHS_MS) {
return { status: 'EXPIRING', daysUntilExpiry, requiresApproval: true, message: `License expires in ${daysUntilExpiry} day(s) — approval required` };
}
return { status: 'VALID', daysUntilExpiry, requiresApproval: false, message: `Valid — expires in ${daysUntilExpiry} day(s)` };
}
async function validateAndFlagLicense(customerId, companyId) {
const where = companyId ? { id: customerId, companyId } : { id: customerId };
const customer = await prisma_1.prisma.customer.findFirstOrThrow({ where });
const result = validateLicense(customer.licenseExpiry);
const status = result.status === 'EXPIRED' ? 'EXPIRED' :
result.status === 'EXPIRING' ? 'EXPIRING' : 'VALID';
if (companyId) {
await prisma_1.prisma.customer.updateMany({
where: { id: customerId, companyId },
data: {
licenseExpired: result.status === 'EXPIRED',
licenseExpiringSoon: result.status === 'EXPIRING',
licenseValidationStatus: status,
flagged: result.requiresApproval ? true : customer.flagged,
flagReason: result.requiresApproval ? result.message : customer.flagReason,
},
});
}
else {
await prisma_1.prisma.customer.update({
where: { id: customerId },
data: {
licenseExpired: result.status === 'EXPIRED',
licenseExpiringSoon: result.status === 'EXPIRING',
licenseValidationStatus: status,
flagged: result.requiresApproval ? true : customer.flagged,
flagReason: result.requiresApproval ? result.message : customer.flagReason,
},
});
}
return result;
}
//# sourceMappingURL=licenseValidationService.js.map
@@ -0,0 +1 @@
{"version":3,"file":"licenseValidationService.js","sourceRoot":"","sources":["../../src/services/licenseValidationService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
import type { NotificationChannel } from '@rentaldrivego/database';
import { prisma } from '../lib/prisma';
export declare const SUPPORTED_NOTIFICATION_LOCALES: readonly ["en", "fr", "ar"];
export type NotificationLocale = (typeof SUPPORTED_NOTIFICATION_LOCALES)[number];
export declare const DEFAULT_NOTIFICATION_LOCALE: NotificationLocale;
type TemplateDateValue = {
type: 'date';
value: Date | string | number;
options?: Intl.DateTimeFormatOptions;
};
type TemplateCurrencyValue = {
type: 'currency';
amountMinor: number;
currency: string;
};
type TemplateSubscriptionStatusValue = {
type: 'subscription_status';
status: string;
};
type TemplateInvoiceStatusValue = {
type: 'invoice_status';
status: string;
};
export type NotificationTemplateValue = string | number | boolean | Date | null | undefined | TemplateDateValue | TemplateCurrencyValue | TemplateSubscriptionStatusValue | TemplateInvoiceStatusValue;
export type NotificationTemplateVariables = Record<string, NotificationTemplateValue>;
type NotificationLocalizationDb = Pick<typeof prisma, 'brandSettings' | 'billingAccount' | 'employee' | 'notificationTemplate' | 'renter'>;
export declare function coerceNotificationLocale(locale?: string | null): NotificationLocale;
export declare function getNotificationTextDirection(locale: NotificationLocale): "ltr" | "rtl";
export declare function formatLocalizedDate(value: Date | string | number, locale: NotificationLocale, options?: Intl.DateTimeFormatOptions): string;
export declare function formatLocalizedCurrency(amountMinor: number, currency: string, locale: NotificationLocale): string;
export declare function localizeSubscriptionStatus(status: string, locale: NotificationLocale): string;
export declare function localizeInvoiceStatus(status: string, locale: NotificationLocale): string;
export declare function localizeBillingPeriod(period: string, locale: NotificationLocale): string;
export declare function localizePlanName(plan: string, locale: NotificationLocale): string;
export declare function resolveNotificationLocale(input: {
companyId?: string | null;
employeeId?: string | null;
renterId?: string | null;
billingAccountId?: string | null;
locale?: string | null;
}, db?: NotificationLocalizationDb): Promise<NotificationLocale>;
export declare function resolveNotificationTemplate(input: {
templateKey: string;
channel: NotificationChannel;
locale: NotificationLocale;
variables?: NotificationTemplateVariables;
}, db?: NotificationLocalizationDb): Promise<{
locale: "en" | "fr" | "ar";
subject: string | null;
body: string | null;
templateKey: string;
usedFallback: boolean;
}>;
export declare function renderNotificationText(template: string | null, variables: NotificationTemplateVariables | undefined, requiredVariables: string[] | undefined, locale: NotificationLocale): string | null;
export declare function renderLocalizedEmailHtml(body: string, locale: NotificationLocale): string;
export {};
//# sourceMappingURL=notificationLocalizationService.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"notificationLocalizationService.d.ts","sourceRoot":"","sources":["../../src/services/notificationLocalizationService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,316 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_NOTIFICATION_LOCALE = exports.SUPPORTED_NOTIFICATION_LOCALES = void 0;
exports.coerceNotificationLocale = coerceNotificationLocale;
exports.getNotificationTextDirection = getNotificationTextDirection;
exports.formatLocalizedDate = formatLocalizedDate;
exports.formatLocalizedCurrency = formatLocalizedCurrency;
exports.localizeSubscriptionStatus = localizeSubscriptionStatus;
exports.localizeInvoiceStatus = localizeInvoiceStatus;
exports.localizeBillingPeriod = localizeBillingPeriod;
exports.localizePlanName = localizePlanName;
exports.resolveNotificationLocale = resolveNotificationLocale;
exports.resolveNotificationTemplate = resolveNotificationTemplate;
exports.renderNotificationText = renderNotificationText;
exports.renderLocalizedEmailHtml = renderLocalizedEmailHtml;
const prisma_1 = require("../lib/prisma");
exports.SUPPORTED_NOTIFICATION_LOCALES = ['en', 'fr', 'ar'];
exports.DEFAULT_NOTIFICATION_LOCALE = 'en';
const localeConfig = {
en: { intl: 'en-US', direction: 'ltr' },
fr: { intl: 'fr-FR', direction: 'ltr' },
ar: { intl: 'ar-MA', direction: 'rtl' },
};
const subscriptionStatusLabels = {
en: {
trialing: 'Trial active',
active: 'Active',
payment_pending: 'Payment pending',
past_due: 'Payment overdue',
suspended: 'Suspended',
cancelled: 'Canceled',
canceled: 'Canceled',
expired: 'Expired',
},
fr: {
trialing: 'Essai actif',
active: 'Actif',
payment_pending: 'Paiement en attente',
past_due: 'Paiement en retard',
suspended: 'Suspendu',
cancelled: 'Annule',
canceled: 'Annule',
expired: 'Expire',
},
ar: {
trialing: 'الفترة التجريبية نشطة',
active: 'نشط',
payment_pending: 'الدفع قيد الانتظار',
past_due: 'الدفع متأخر',
suspended: 'معلّق',
cancelled: 'ملغى',
canceled: 'ملغى',
expired: 'منتهي',
},
};
const invoiceStatusLabels = {
en: {
draft: 'Preparing',
open: 'Due',
payment_pending: 'Payment processing',
paid: 'Paid',
partially_paid: 'Partially paid',
past_due: 'Past due',
void: 'Canceled',
uncollectible: 'Contact support',
refunded: 'Refunded',
partially_refunded: 'Partially refunded',
},
fr: {
draft: 'En preparation',
open: 'A payer',
payment_pending: 'Paiement en cours',
paid: 'Payee',
partially_paid: 'Partiellement payee',
past_due: 'En retard',
void: 'Annulee',
uncollectible: 'Contacter le support',
refunded: 'Remboursee',
partially_refunded: 'Partiellement remboursee',
},
ar: {
draft: 'قيد الإعداد',
open: 'مستحقة الدفع',
payment_pending: 'الدفع قيد المعالجة',
paid: 'مدفوعة',
partially_paid: 'مدفوعة جزئياً',
past_due: 'متأخرة',
void: 'ملغاة',
uncollectible: 'تواصل مع الدعم',
refunded: 'مستردة',
partially_refunded: 'مستردة جزئياً',
},
};
const billingPeriodLabels = {
en: {
monthly: 'monthly',
annual: 'annual',
},
fr: {
monthly: 'mensuel',
annual: 'annuel',
},
ar: {
monthly: 'شهري',
annual: 'سنوي',
},
};
const planLabels = {
en: {
starter: 'Starter',
growth: 'Growth',
pro: 'Pro',
},
fr: {
starter: 'Starter',
growth: 'Growth',
pro: 'Pro',
},
ar: {
starter: 'Starter',
growth: 'Growth',
pro: 'Pro',
},
};
function coerceNotificationLocale(locale) {
if (locale && exports.SUPPORTED_NOTIFICATION_LOCALES.includes(locale)) {
return locale;
}
return exports.DEFAULT_NOTIFICATION_LOCALE;
}
function getNotificationTextDirection(locale) {
return localeConfig[locale].direction;
}
function formatLocalizedDate(value, locale, options) {
const date = value instanceof Date ? value : new Date(value);
return new Intl.DateTimeFormat(localeConfig[locale].intl, options ?? {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
function formatLocalizedCurrency(amountMinor, currency, locale) {
return new Intl.NumberFormat(localeConfig[locale].intl, {
style: 'currency',
currency,
}).format(amountMinor / 100);
}
function localizeSubscriptionStatus(status, locale) {
const normalized = status.toLowerCase();
return subscriptionStatusLabels[locale][normalized] ?? status;
}
function localizeInvoiceStatus(status, locale) {
const normalized = status.toLowerCase();
return invoiceStatusLabels[locale][normalized] ?? status;
}
function localizeBillingPeriod(period, locale) {
const normalized = period.toLowerCase();
return billingPeriodLabels[locale][normalized] ?? period;
}
function localizePlanName(plan, locale) {
const normalized = plan.toLowerCase();
return planLabels[locale][normalized] ?? plan;
}
function isTemplateDateValue(value) {
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'date';
}
function isTemplateCurrencyValue(value) {
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'currency';
}
function isTemplateSubscriptionStatusValue(value) {
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'subscription_status';
}
function isTemplateInvoiceStatusValue(value) {
return typeof value === 'object' && value !== null && 'type' in value && value.type === 'invoice_status';
}
function formatTemplateValue(value, locale) {
if (value instanceof Date)
return formatLocalizedDate(value, locale);
if (isTemplateDateValue(value))
return formatLocalizedDate(value.value, locale, value.options);
if (isTemplateCurrencyValue(value))
return formatLocalizedCurrency(value.amountMinor, value.currency, locale);
if (isTemplateSubscriptionStatusValue(value))
return localizeSubscriptionStatus(value.status, locale);
if (isTemplateInvoiceStatusValue(value))
return localizeInvoiceStatus(value.status, locale);
if (value === null || value === undefined)
return '';
return String(value);
}
function normalizeTemplateVariableList(value) {
if (!Array.isArray(value))
return [];
return value.filter((item) => typeof item === 'string');
}
function escapeHtml(value) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
async function findTemplate(db, templateKey, channel, locale) {
return db.notificationTemplate.findFirst({
where: { templateKey, channel, locale, isActive: true },
orderBy: { version: 'desc' },
select: {
templateKey: true,
channel: true,
locale: true,
subject: true,
body: true,
requiredVariables: true,
optionalVariables: true,
version: true,
},
});
}
async function resolveNotificationLocale(input, db = prisma_1.prisma) {
if (input.locale)
return coerceNotificationLocale(input.locale);
const employeePromise = input.employeeId
? db.employee.findUnique({
where: { id: input.employeeId },
select: { preferredLanguage: true },
})
: Promise.resolve(null);
const renterPromise = input.renterId
? db.renter.findUnique({
where: { id: input.renterId },
select: { preferredLocale: true },
})
: Promise.resolve(null);
const workspacePromise = input.companyId
? db.brandSettings.findUnique({
where: { companyId: input.companyId },
select: { defaultLocale: true },
})
: Promise.resolve(null);
const billingPromise = input.billingAccountId
? db.billingAccount.findUnique({
where: { id: input.billingAccountId },
select: { preferredLanguage: true },
})
: input.companyId
? db.billingAccount.findFirst({
where: { companyId: input.companyId, isPrimary: true },
select: { preferredLanguage: true },
orderBy: { createdAt: 'asc' },
})
: Promise.resolve(null);
const [employee, renter, workspace, billingAccount] = await Promise.all([
employeePromise,
renterPromise,
workspacePromise,
billingPromise,
]);
return coerceNotificationLocale(employee?.preferredLanguage ??
renter?.preferredLocale ??
workspace?.defaultLocale ??
billingAccount?.preferredLanguage ??
exports.DEFAULT_NOTIFICATION_LOCALE);
}
async function resolveNotificationTemplate(input, db = prisma_1.prisma) {
const requested = await findTemplate(db, input.templateKey, input.channel, input.locale);
if (requested) {
return {
locale: coerceNotificationLocale(requested.locale),
subject: renderNotificationText(requested.subject, input.variables, normalizeTemplateVariableList(requested.requiredVariables), input.locale),
body: renderNotificationText(requested.body, input.variables, normalizeTemplateVariableList(requested.requiredVariables), input.locale),
templateKey: requested.templateKey,
usedFallback: false,
};
}
if (input.locale !== exports.DEFAULT_NOTIFICATION_LOCALE) {
console.warn(`[Notifications] Missing ${input.locale} template for ${input.templateKey}/${input.channel}; falling back to ${exports.DEFAULT_NOTIFICATION_LOCALE}.`);
}
const fallback = await findTemplate(db, input.templateKey, input.channel, exports.DEFAULT_NOTIFICATION_LOCALE);
if (!fallback) {
throw new Error(`Missing notification template for ${input.templateKey}/${input.channel} in ${input.locale} and ${exports.DEFAULT_NOTIFICATION_LOCALE}`);
}
return {
locale: exports.DEFAULT_NOTIFICATION_LOCALE,
subject: renderNotificationText(fallback.subject, input.variables, normalizeTemplateVariableList(fallback.requiredVariables), exports.DEFAULT_NOTIFICATION_LOCALE),
body: renderNotificationText(fallback.body, input.variables, normalizeTemplateVariableList(fallback.requiredVariables), exports.DEFAULT_NOTIFICATION_LOCALE),
templateKey: fallback.templateKey,
usedFallback: true,
};
}
function renderNotificationText(template, variables = {}, requiredVariables = [], locale) {
if (!template)
return null;
for (const variableName of requiredVariables) {
if (variables[variableName] === undefined || variables[variableName] === null) {
throw new Error(`Missing notification template variable: ${variableName}`);
}
}
return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_match, variableName) => formatTemplateValue(variables[variableName], locale));
}
function renderLocalizedEmailHtml(body, locale) {
const direction = getNotificationTextDirection(locale);
const textAlign = direction === 'rtl' ? 'right' : 'left';
const paragraphs = body
.split(/\n{2,}/)
.map((paragraph) => `<p style="margin:0 0 16px;text-align:${textAlign};">${escapeHtml(paragraph).replace(/\n/g, '<br />')}</p>`)
.join('');
return [
`<html lang="${locale}" dir="${direction}">`,
`<body style="font-family:sans-serif;max-width:560px;margin:0 auto;padding:32px;color:#1c1917;direction:${direction};text-align:${textAlign};">`,
paragraphs,
'</body>',
'</html>',
].join('');
}
//# sourceMappingURL=notificationLocalizationService.js.map
File diff suppressed because one or more lines are too long
+32
View File
@@ -0,0 +1,32 @@
import { NotificationType, NotificationChannel } from '@rentaldrivego/database';
import type { NotificationTemplateVariables } from './notificationLocalizationService';
interface SendNotificationOptions {
type: NotificationType;
title?: string;
body?: string;
data?: Record<string, unknown>;
companyId?: string;
employeeId?: string;
renterId?: string;
billingAccountId?: string;
email?: string;
phone?: string;
fcmToken?: string;
channels: NotificationChannel[];
locale?: string;
templateKey?: string;
templateVariables?: NotificationTemplateVariables;
}
export declare function sendNotification(opts: SendNotificationOptions): Promise<{
channel: NotificationChannel;
success: boolean;
error?: string;
}[]>;
export declare function sendTransactionalEmail(opts: {
to: string;
subject: string;
html: string;
text: string;
}): Promise<void>;
export {};
//# sourceMappingURL=notificationService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"notificationService.d.ts","sourceRoot":"","sources":["../../src/services/notificationService.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAM/E,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,mCAAmC,CAAA;AA+FtF,UAAU,uBAAuB;IAC/B,IAAI,EAAE,gBAAgB,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,6BAA6B,CAAA;CAClD;AAiFD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,uBAAuB;aAClC,mBAAmB;aAAW,OAAO;YAAU,MAAM;KAwHtF;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,iBAEA"}
+256
View File
@@ -0,0 +1,256 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendNotification = sendNotification;
exports.sendTransactionalEmail = sendTransactionalEmail;
const resend_1 = require("resend");
const twilio_1 = __importDefault(require("twilio"));
const firebase_admin_1 = __importDefault(require("firebase-admin"));
const prisma_1 = require("../lib/prisma");
const redis_1 = require("../lib/redis");
const notificationLocalizationService_1 = require("./notificationLocalizationService");
const resendApiKey = process.env.RESEND_API_KEY &&
process.env.RESEND_API_KEY !== 're_...' &&
process.env.RESEND_API_KEY.startsWith('re_')
? process.env.RESEND_API_KEY
: null;
const resend = resendApiKey ? new resend_1.Resend(resendApiKey) : null;
const emailFromAddress = process.env.EMAIL_FROM && process.env.EMAIL_FROM !== 'noreply@example.com'
? process.env.EMAIL_FROM
: null;
const emailFromName = process.env.EMAIL_FROM_NAME && process.env.EMAIL_FROM_NAME !== 'Example App'
? process.env.EMAIL_FROM_NAME
: null;
const smtpHost = process.env.MAIL_HOST;
const smtpPort = Number(process.env.MAIL_PORT ?? 0);
const smtpUser = process.env.MAIL_USERNAME;
const smtpPass = process.env.MAIL_PASSWORD;
const smtpSecure = process.env.MAIL_SCHEME === 'smtps' ||
process.env.MAIL_ENCRYPTION === 'ssl' ||
smtpPort === 465;
const smtpFromAddress = process.env.MAIL_FROM_ADDRESS && !process.env.MAIL_FROM_ADDRESS.includes('${')
? process.env.MAIL_FROM_ADDRESS
: null;
const smtpFromName = process.env.MAIL_FROM_NAME && !process.env.MAIL_FROM_NAME.includes('${')
? process.env.MAIL_FROM_NAME
: null;
let smtpTransport = null;
if (smtpHost && smtpPort && smtpUser && smtpPass) {
try {
const nodemailer = require('nodemailer');
smtpTransport = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
auth: {
user: smtpUser,
pass: smtpPass,
},
});
}
catch (err) {
console.warn('[Notifications] SMTP transport unavailable:', err?.message ?? String(err));
}
}
const twilioClient = process.env.TWILIO_ACCOUNT_SID &&
process.env.TWILIO_AUTH_TOKEN &&
process.env.TWILIO_ACCOUNT_SID !== 'AC...' &&
process.env.TWILIO_AUTH_TOKEN !== 'your-twilio-auth-token'
? (0, twilio_1.default)(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)
: null;
const firebasePrivateKey = process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n');
const hasFirebaseConfig = !!process.env.FIREBASE_PROJECT_ID &&
!!process.env.FIREBASE_CLIENT_EMAIL &&
!!firebasePrivateKey &&
!firebasePrivateKey.includes('BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY');
if (hasFirebaseConfig && !firebase_admin_1.default.apps.length) {
try {
firebase_admin_1.default.initializeApp({
credential: firebase_admin_1.default.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
privateKey: firebasePrivateKey,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
}),
});
}
catch (err) {
console.warn('[Notifications] Firebase init skipped:', err.message);
}
}
function resolveSmtpReplyTo() {
if (!process.env.MAIL_REPLY_TO_ADDRESS || process.env.MAIL_REPLY_TO_ADDRESS.includes('${')) {
return undefined;
}
if (process.env.MAIL_REPLY_TO_NAME && !process.env.MAIL_REPLY_TO_NAME.includes('${')) {
return `${process.env.MAIL_REPLY_TO_NAME} <${process.env.MAIL_REPLY_TO_ADDRESS}>`;
}
return process.env.MAIL_REPLY_TO_ADDRESS;
}
async function sendEmailWithProviders(opts) {
const errors = [];
if (resend) {
try {
if (!emailFromAddress || !emailFromName) {
throw new Error('Email sender identity is not configured');
}
const { data, error } = await resend.emails.send({
from: `${emailFromName} <${emailFromAddress}>`,
to: opts.to,
subject: opts.subject,
html: opts.html,
text: opts.text,
});
if (error) {
throw new Error(error.message);
}
return {
provider: 'resend',
providerMessageId: data?.id ?? null,
};
}
catch (err) {
errors.push(`Resend: ${err?.message ?? String(err)}`);
console.warn('[Notifications] Resend delivery failed, falling back if possible:', err?.message ?? String(err));
}
}
if (smtpTransport) {
try {
if (!smtpFromAddress) {
throw new Error('SMTP sender identity is not configured');
}
const info = await smtpTransport.sendMail({
from: smtpFromName ? `${smtpFromName} <${smtpFromAddress}>` : smtpFromAddress,
to: opts.to,
subject: opts.subject,
html: opts.html,
text: opts.text,
replyTo: resolveSmtpReplyTo(),
});
return {
provider: 'smtp',
providerMessageId: info.messageId ?? null,
};
}
catch (err) {
errors.push(`SMTP: ${err?.message ?? String(err)}`);
}
}
if (errors.length) {
throw new Error(errors.join(' | '));
}
throw new Error('No email provider is configured');
}
async function sendNotification(opts) {
const results = [];
const resolvedLocale = await (0, notificationLocalizationService_1.resolveNotificationLocale)({
companyId: opts.companyId,
employeeId: opts.employeeId,
renterId: opts.renterId,
billingAccountId: opts.billingAccountId,
locale: opts.locale,
});
for (const channel of opts.channels) {
try {
const rendered = opts.templateKey
? await (0, notificationLocalizationService_1.resolveNotificationTemplate)({
templateKey: opts.templateKey,
channel,
locale: resolvedLocale,
variables: opts.templateVariables,
})
: null;
const title = rendered?.subject ?? opts.title;
const body = rendered?.body ?? opts.body;
if (!title || !body) {
throw new Error(`Notification content is incomplete for channel ${channel}`);
}
const notification = await prisma_1.prisma.notification.create({
data: {
type: opts.type,
title,
body,
data: (opts.data ?? {}),
channel,
templateKey: rendered?.templateKey ?? opts.templateKey ?? null,
locale: rendered?.locale ?? resolvedLocale,
status: 'PENDING',
companyId: opts.companyId ?? null,
employeeId: opts.employeeId ?? null,
renterId: opts.renterId ?? null,
},
});
let providerMessageId = null;
let success = false;
if (channel === 'EMAIL' && opts.email) {
const emailResult = await sendEmailWithProviders({
to: opts.email,
subject: title,
html: (0, notificationLocalizationService_1.renderLocalizedEmailHtml)(body, rendered?.locale ?? resolvedLocale),
text: body,
});
providerMessageId = emailResult.providerMessageId;
success = true;
}
if (channel === 'SMS' && opts.phone) {
if (!twilioClient)
throw new Error('Twilio is not configured');
const msg = await twilioClient.messages.create({
body,
from: process.env.TWILIO_PHONE_NUMBER,
to: opts.phone,
});
providerMessageId = msg.sid;
success = true;
}
if (channel === 'WHATSAPP' && opts.phone) {
if (!twilioClient)
throw new Error('Twilio is not configured');
const msg = await twilioClient.messages.create({
body,
from: `whatsapp:${process.env.TWILIO_WHATSAPP_NUMBER}`,
to: `whatsapp:${opts.phone}`,
});
providerMessageId = msg.sid;
success = true;
}
if (channel === 'PUSH' && opts.fcmToken) {
if (!firebase_admin_1.default.apps.length)
throw new Error('Firebase is not configured');
const response = await firebase_admin_1.default.messaging().send({
token: opts.fcmToken,
notification: { title, body },
data: Object.fromEntries(Object.entries(opts.data ?? {}).map(([k, v]) => [k, String(v)])),
});
providerMessageId = response;
success = true;
}
if (channel === 'IN_APP') {
// Emit via Socket.io through Redis pub/sub
const targetId = opts.employeeId ?? opts.renterId;
if (targetId) {
await redis_1.redis.publish(`notifications:${targetId}`, JSON.stringify({ ...notification, status: 'DELIVERED' }));
}
success = true;
}
await prisma_1.prisma.notification.update({
where: { id: notification.id },
data: {
status: success ? 'SENT' : 'FAILED',
sentAt: success ? new Date() : null,
providerMessageId,
},
});
results.push({ channel, success });
}
catch (err) {
results.push({ channel, success: false, error: err.message });
}
}
return results;
}
async function sendTransactionalEmail(opts) {
await sendEmailWithProviders(opts);
}
//# sourceMappingURL=notificationService.js.map
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
export interface PayPalCreateOrderParams {
amount: number;
currency: string;
orderId: string;
description: string;
returnUrl: string;
cancelUrl: string;
}
export interface PayPalOrderResult {
orderId: string;
approveUrl: string;
status: string;
}
export declare function createOrder(params: PayPalCreateOrderParams): Promise<PayPalOrderResult>;
export declare function captureOrder(paypalOrderId: string): Promise<Record<string, unknown>>;
export declare function refundCapture(captureId: string, amount: number, currency: string, note?: string): Promise<Record<string, unknown>>;
export declare function verifyWebhookEvent(headers: Record<string, string | undefined>, rawBody: string): Promise<boolean>;
export declare function isConfigured(): boolean;
//# sourceMappingURL=paypalService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"paypalService.d.ts","sourceRoot":"","sources":["../../src/services/paypalService.ts"],"names":[],"mappings":"AA2CA,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAsB,WAAW,CAAC,MAAM,EAAE,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA8B7F;AAED,wBAAsB,YAAY,CAAC,aAAa,EAAE,MAAM,oCAEvD;AAED,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,oCAQrG;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,oBAkBpG;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
+111
View File
@@ -0,0 +1,111 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createOrder = createOrder;
exports.captureOrder = captureOrder;
exports.refundCapture = refundCapture;
exports.verifyWebhookEvent = verifyWebhookEvent;
exports.isConfigured = isConfigured;
const BASE_URL = process.env.PAYPAL_BASE_URL ?? 'https://api-m.paypal.com';
const CLIENT_ID = process.env.PAYPAL_CLIENT_ID ?? '';
const CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET ?? '';
let cachedToken = null;
async function getAccessToken() {
if (cachedToken && Date.now() < cachedToken.expiresAt - 30_000) {
return cachedToken.token;
}
const res = await fetch(`${BASE_URL}/v1/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: 'grant_type=client_credentials',
});
if (!res.ok)
throw new Error('PayPal: failed to obtain access token');
const json = await res.json();
cachedToken = { token: json.access_token, expiresAt: Date.now() + json.expires_in * 1000 };
return cachedToken.token;
}
async function ppFetch(path, init) {
const token = await getAccessToken();
const res = await fetch(`${BASE_URL}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...(init?.headers ?? {}),
},
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(`PayPal ${path} failed: ${err?.message ?? res.statusText}`);
}
return res.json();
}
async function createOrder(params) {
const json = await ppFetch('/v2/checkout/orders', {
method: 'POST',
body: JSON.stringify({
intent: 'CAPTURE',
purchase_units: [
{
reference_id: params.orderId,
description: params.description,
amount: {
currency_code: params.currency,
value: (params.amount / 100).toFixed(2),
},
},
],
application_context: {
return_url: params.returnUrl,
cancel_url: params.cancelUrl,
brand_name: 'RentalDriveGo',
user_action: 'PAY_NOW',
},
}),
});
const approveLink = json.links?.find((l) => l.rel === 'approve');
return {
orderId: json.id,
approveUrl: approveLink?.href ?? '',
status: json.status,
};
}
async function captureOrder(paypalOrderId) {
return ppFetch(`/v2/checkout/orders/${paypalOrderId}/capture`, { method: 'POST', body: '{}' });
}
async function refundCapture(captureId, amount, currency, note) {
return ppFetch(`/v2/payments/captures/${captureId}/refund`, {
method: 'POST',
body: JSON.stringify({
amount: { currency_code: currency, value: (amount / 100).toFixed(2) },
note_to_payer: note,
}),
});
}
async function verifyWebhookEvent(headers, rawBody) {
try {
const json = await ppFetch('/v1/notifications/verify-webhook-signature', {
method: 'POST',
body: JSON.stringify({
auth_algo: headers['paypal-auth-algo'],
cert_url: headers['paypal-cert-url'],
transmission_id: headers['paypal-transmission-id'],
transmission_sig: headers['paypal-transmission-sig'],
transmission_time: headers['paypal-transmission-time'],
webhook_id: process.env.PAYPAL_WEBHOOK_ID ?? '',
webhook_event: JSON.parse(rawBody),
}),
});
return json.verification_status === 'SUCCESS';
}
catch {
return false;
}
}
function isConfigured() {
return !!(CLIENT_ID && CLIENT_SECRET && CLIENT_ID !== 'your-paypal-client-id');
}
//# sourceMappingURL=paypalService.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"paypalService.js","sourceRoot":"","sources":["../../src/services/paypalService.ts"],"names":[],"mappings":"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"}
+4
View File
@@ -0,0 +1,4 @@
import { type MarketplaceHomepageConfig } from '@rentaldrivego/types';
export declare function getMarketplaceHomepageContent(): Promise<MarketplaceHomepageConfig>;
export declare function saveMarketplaceHomepageContent(homepage: MarketplaceHomepageConfig): Promise<MarketplaceHomepageConfig>;
//# sourceMappingURL=platformContentService.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"platformContentService.d.ts","sourceRoot":"","sources":["../../src/services/platformContentService.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmC,KAAK,yBAAyB,EAAE,MAAM,sBAAsB,CAAA;AA6BtG,wBAAsB,6BAA6B,uCAGlD;AAED,wBAAsB,8BAA8B,CAAC,QAAQ,EAAE,yBAAyB,sCAOvF"}
+43
View File
@@ -0,0 +1,43 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMarketplaceHomepageContent = getMarketplaceHomepageContent;
exports.saveMarketplaceHomepageContent = saveMarketplaceHomepageContent;
const promises_1 = require("fs/promises");
const path_1 = __importDefault(require("path"));
const types_1 = require("@rentaldrivego/types");
function resolveContentPath() {
const cwd = process.cwd();
const appScoped = path_1.default.resolve(cwd, 'src/data/platform-content.json');
const repoScoped = path_1.default.resolve(cwd, 'apps/api/src/data/platform-content.json');
return cwd.includes('/apps/api') ? appScoped : repoScoped;
}
const platformContentPath = resolveContentPath();
async function readPlatformContentFile() {
try {
const raw = await (0, promises_1.readFile)(platformContentPath, 'utf8');
return JSON.parse(raw);
}
catch {
return {};
}
}
async function writePlatformContentFile(content) {
await (0, promises_1.mkdir)(path_1.default.dirname(platformContentPath), { recursive: true });
await (0, promises_1.writeFile)(platformContentPath, JSON.stringify(content, null, 2));
}
async function getMarketplaceHomepageContent() {
const content = await readPlatformContentFile();
return content.marketplaceHomepage ?? (0, types_1.cloneMarketplaceHomepageContent)();
}
async function saveMarketplaceHomepageContent(homepage) {
const content = await readPlatformContentFile();
await writePlatformContentFile({
...content,
marketplaceHomepage: homepage,
});
return homepage;
}
//# sourceMappingURL=platformContentService.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"platformContentService.js","sourceRoot":"","sources":["../../src/services/platformContentService.ts"],"names":[],"mappings":";;;;;AA+BA,sEAGC;AAED,wEAOC;AA3CD,0CAAwD;AACxD,gDAAuB;AACvB,gDAAsG;AAEtG,SAAS,kBAAkB;IACzB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IACzB,MAAM,SAAS,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,EAAE,gCAAgC,CAAC,CAAA;IACrE,MAAM,UAAU,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,EAAE,yCAAyC,CAAC,CAAA;IAC/E,OAAO,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAA;AAC3D,CAAC;AAED,MAAM,mBAAmB,GAAG,kBAAkB,EAAE,CAAA;AAMhD,KAAK,UAAU,uBAAuB;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,IAAA,mBAAQ,EAAC,mBAAmB,EAAE,MAAM,CAAC,CAAA;QACvD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAwB,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,KAAK,UAAU,wBAAwB,CAAC,OAA4B;IAClE,MAAM,IAAA,gBAAK,EAAC,cAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACnE,MAAM,IAAA,oBAAS,EAAC,mBAAmB,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;AACxE,CAAC;AAEM,KAAK,UAAU,6BAA6B;IACjD,MAAM,OAAO,GAAG,MAAM,uBAAuB,EAAE,CAAA;IAC/C,OAAO,OAAO,CAAC,mBAAmB,IAAI,IAAA,uCAA+B,GAAE,CAAA;AACzE,CAAC;AAEM,KAAK,UAAU,8BAA8B,CAAC,QAAmC;IACtF,MAAM,OAAO,GAAG,MAAM,uBAAuB,EAAE,CAAA;IAC/C,MAAM,wBAAwB,CAAC;QAC7B,GAAG,OAAO;QACV,mBAAmB,EAAE,QAAQ;KAC9B,CAAC,CAAA;IACF,OAAO,QAAQ,CAAA;AACjB,CAAC"}
+10
View File
@@ -0,0 +1,10 @@
interface DriverInfo {
dateOfBirth?: Date | null;
licenseIssuedAt?: Date | null;
}
export declare function applyPricingRules(companyId: string, customerId: string, additionalDrivers: DriverInfo[], dailyRate: number, totalDays: number): Promise<{
applied: any[];
total: number;
}>;
export {};
//# sourceMappingURL=pricingRuleService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"pricingRuleService.d.ts","sourceRoot":"","sources":["../../src/services/pricingRuleService.ts"],"names":[],"mappings":"AAEA,UAAU,UAAU;IAClB,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IACzB,eAAe,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;CAC9B;AAED,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,iBAAiB,EAAE,UAAU,EAAE,EAC/B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,GAAG,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CA0C5C"}
+51
View File
@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyPricingRules = applyPricingRules;
const prisma_1 = require("../lib/prisma");
async function applyPricingRules(companyId, customerId, additionalDrivers, dailyRate, totalDays) {
const rules = await prisma_1.prisma.pricingRule.findMany({ where: { companyId, isActive: true } });
const customer = await prisma_1.prisma.customer.findFirstOrThrow({ where: { id: customerId, companyId } });
const allDrivers = [customer, ...additionalDrivers];
const applied = [];
const baseAmount = dailyRate * totalDays;
for (const driver of allDrivers) {
for (const rule of rules) {
const driverAge = driver.dateOfBirth
? Math.floor((Date.now() - driver.dateOfBirth.getTime()) / (365.25 * 24 * 60 * 60 * 1000))
: null;
const licenseYears = driver.licenseIssuedAt
? Math.floor((Date.now() - driver.licenseIssuedAt.getTime()) / (365.25 * 24 * 60 * 60 * 1000))
: null;
let conditionMet = false;
if (rule.condition === 'AGE_LESS_THAN' && driverAge !== null)
conditionMet = driverAge < rule.conditionValue;
else if (rule.condition === 'AGE_GREATER_THAN' && driverAge !== null)
conditionMet = driverAge > rule.conditionValue;
else if (rule.condition === 'LICENSE_YEARS_LESS_THAN' && licenseYears !== null)
conditionMet = licenseYears < rule.conditionValue;
else if (rule.condition === 'LICENSE_YEARS_GREATER_THAN' && licenseYears !== null)
conditionMet = licenseYears > rule.conditionValue;
if (!conditionMet)
continue;
let amount = 0;
if (rule.adjustmentType === 'PERCENTAGE')
amount = Math.round(baseAmount * rule.adjustmentValue / 100);
else if (rule.adjustmentType === 'FLAT_PER_DAY')
amount = rule.adjustmentValue * totalDays;
else
amount = rule.adjustmentValue;
if (rule.type === 'DISCOUNT')
amount = -amount;
applied.push({ ruleId: rule.id, name: rule.name, type: rule.type, amount });
}
}
// Deduplicate: each rule applied once even if multiple drivers qualify
const deduped = applied.reduce((acc, item) => {
if (!acc.find((a) => a.ruleId === item.ruleId))
acc.push(item);
return acc;
}, []);
const total = deduped.reduce((s, r) => s + r.amount, 0);
return { applied: deduped, total };
}
//# sourceMappingURL=pricingRuleService.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"pricingRuleService.js","sourceRoot":"","sources":["../../src/services/pricingRuleService.ts"],"names":[],"mappings":"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"}
+36
View File
@@ -0,0 +1,36 @@
import { EmployeeRole } from '@rentaldrivego/database';
export interface InvitePayload {
firstName: string;
lastName: string;
email: string;
role: EmployeeRole;
}
export interface TeamMember {
id: string;
clerkUserId: string;
firstName: string;
lastName: string;
email: string;
phone: string | null;
role: EmployeeRole;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
lastActiveAt?: Date | null;
invitationStatus?: 'accepted' | 'pending' | 'revoked';
}
export declare function listEmployees(companyId: string): Promise<TeamMember[]>;
export declare function inviteEmployee(companyId: string, inviterId: string, payload: InvitePayload): Promise<{
employee: any;
invitationId: any;
invitedBy: string;
}>;
export declare function updateEmployeeRole(companyId: string, requesterId: string, requesterRole: EmployeeRole, employeeId: string, payload: {
role: EmployeeRole;
}): Promise<any>;
export declare function deactivateEmployee(companyId: string, requesterRole: EmployeeRole, employeeId: string): Promise<any>;
export declare function reactivateEmployee(companyId: string, requesterRole: EmployeeRole, employeeId: string): Promise<any>;
export declare function removeEmployee(companyId: string, requesterRole: EmployeeRole, employeeId: string): Promise<{
success: boolean;
}>;
//# sourceMappingURL=teamService.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"teamService.d.ts","sourceRoot":"","sources":["../../src/services/teamService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAOtD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,YAAY,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;IACf,YAAY,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IAC1B,gBAAgB,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAA;CACtD;AAkED,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAW5E;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa;;;;GA6DhG;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,gBAShK;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAM1G;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,gBAK1G;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM;;GAOtG"}
+166
View File
@@ -0,0 +1,166 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listEmployees = listEmployees;
exports.inviteEmployee = inviteEmployee;
exports.updateEmployeeRole = updateEmployeeRole;
exports.deactivateEmployee = deactivateEmployee;
exports.reactivateEmployee = reactivateEmployee;
exports.removeEmployee = removeEmployee;
const crypto_1 = __importDefault(require("crypto"));
const prisma_1 = require("../lib/prisma");
const notificationService_1 = require("./notificationService");
const notificationLocalizationService_1 = require("./notificationLocalizationService");
const INVITE_TOKEN_TTL_MINUTES = 60 * 24 * 7;
function ensureDashboardBasePath(baseUrl) {
try {
const url = new URL(baseUrl);
const pathname = url.pathname.replace(/\/$/, '');
if (!pathname.endsWith('/dashboard'))
url.pathname = `${pathname}/dashboard`;
return url.toString().replace(/\/$/, '');
}
catch {
const trimmed = baseUrl.replace(/\/$/, '');
return trimmed.endsWith('/dashboard') ? trimmed : `${trimmed}/dashboard`;
}
}
function buildInviteEmail(resetUrl, companyName, firstName, role, locale) {
const greetingName = firstName.trim() || 'there';
if (locale === 'fr') {
return {
subject: `Vous avez ete invite chez ${companyName}`,
html: `
<p>Bonjour ${greetingName},</p>
<p>Vous avez ete invite a rejoindre ${companyName} sur RentalDriveGo en tant que ${role.toLowerCase()}.</p>
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">Definir votre mot de passe</a></p>
<p>Ce lien d'invitation expire dans 7 jours.</p>
<p>RentalDriveGo</p>
`,
text: `Bonjour ${greetingName},\n\nVous avez ete invite a rejoindre ${companyName} sur RentalDriveGo en tant que ${role.toLowerCase()}.\n\nDefinissez votre mot de passe ici : ${resetUrl}\n\nCe lien d'invitation expire dans 7 jours.\n\nRentalDriveGo`,
};
}
if (locale === 'ar') {
return {
subject: `تمت دعوتك إلى ${companyName}`,
html: `
<div dir="rtl" style="text-align:right">
<p>مرحباً ${greetingName}،</p>
<p>تمت دعوتك للانضمام إلى ${companyName} على RentalDriveGo بصفتك ${role.toLowerCase()}.</p>
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">عيّن كلمة المرور</a></p>
<p>تنتهي صلاحية رابط الدعوة هذا خلال 7 أيام.</p>
<p>RentalDriveGo</p>
</div>
`,
text: `مرحباً ${greetingName}،\n\nتمت دعوتك للانضمام إلى ${companyName} على RentalDriveGo بصفتك ${role.toLowerCase()}.\n\nعيّن كلمة المرور هنا: ${resetUrl}\n\nتنتهي صلاحية رابط الدعوة هذا خلال 7 أيام.\n\nRentalDriveGo`,
};
}
return {
subject: `You have been invited to ${companyName}`,
html: `
<p>Hi ${greetingName},</p>
<p>You were invited to join ${companyName} on RentalDriveGo as a ${role.toLowerCase()}.</p>
<p><a href="${resetUrl}" style="background:#1d4ed8;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;display:inline-block;font-weight:600;">Set your password</a></p>
<p>This invitation link expires in 7 days.</p>
<p>RentalDriveGo</p>
`,
text: `Hi ${greetingName},\n\nYou were invited to join ${companyName} on RentalDriveGo as a ${role.toLowerCase()}.\n\nSet your password here: ${resetUrl}\n\nThis invitation link expires in 7 days.\n\nRentalDriveGo`,
};
}
async function listEmployees(companyId) {
const employees = await prisma_1.prisma.employee.findMany({
where: { companyId },
orderBy: [{ role: 'asc' }, { createdAt: 'asc' }],
});
return employees.map((employee) => ({
...employee,
lastActiveAt: null,
invitationStatus: employee.passwordHash ? 'accepted' : 'pending',
}));
}
async function inviteEmployee(companyId, inviterId, payload) {
if (payload.role === 'OWNER') {
throw Object.assign(new Error('Cannot invite a member with the OWNER role'), { statusCode: 400, code: 'invalid_role' });
}
const existing = await prisma_1.prisma.employee.findFirst({ where: { companyId, email: payload.email } });
if (existing) {
throw Object.assign(new Error('An employee with this email already exists in your team'), { statusCode: 409, code: 'employee_already_exists' });
}
const company = await prisma_1.prisma.company.findUniqueOrThrow({
where: { id: companyId },
include: { brand: { select: { defaultLocale: true, displayName: true } } },
});
const rawToken = crypto_1.default.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + INVITE_TOKEN_TTL_MINUTES * 60 * 1000);
const locale = (0, notificationLocalizationService_1.coerceNotificationLocale)(company.brand?.defaultLocale);
const employee = await prisma_1.prisma.employee.create({
data: {
companyId,
clerkUserId: `local_member_${crypto_1.default.randomUUID()}`,
firstName: payload.firstName,
lastName: payload.lastName,
email: payload.email,
role: payload.role,
isActive: true,
preferredLanguage: locale,
passwordResetToken: rawToken,
passwordResetExpiresAt: expiresAt,
},
});
const dashboardUrl = ensureDashboardBasePath(process.env.DASHBOARD_URL ?? process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard');
const resetUrl = `${dashboardUrl}/reset-password?token=${rawToken}`;
const message = buildInviteEmail(resetUrl, company.brand?.displayName ?? company.name, payload.firstName, payload.role, locale);
await (0, notificationService_1.sendTransactionalEmail)({
to: payload.email,
subject: message.subject,
html: message.html,
text: message.text,
});
return {
employee: {
...employee,
lastActiveAt: null,
invitationStatus: 'pending',
},
invitationId: employee.id,
invitedBy: inviterId,
};
}
async function updateEmployeeRole(companyId, requesterId, requesterRole, employeeId, payload) {
const target = await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
if (requesterRole !== 'OWNER')
throw Object.assign(new Error('Only the account owner can change team member roles'), { statusCode: 403, code: 'forbidden' });
if (target.role === 'OWNER')
throw Object.assign(new Error('Cannot change the role of the account owner'), { statusCode: 400 });
if (payload.role === 'OWNER')
throw Object.assign(new Error('Cannot assign the OWNER role via this endpoint'), { statusCode: 400 });
if (target.id === requesterId)
throw Object.assign(new Error('You cannot change your own role'), { statusCode: 400 });
return prisma_1.prisma.employee.update({ where: { id: employeeId }, data: { role: payload.role } });
}
async function deactivateEmployee(companyId, requesterRole, employeeId) {
if (requesterRole !== 'OWNER')
throw Object.assign(new Error('Only the account owner can deactivate team members'), { statusCode: 403 });
const target = await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
if (target.role === 'OWNER')
throw Object.assign(new Error('Cannot deactivate the account owner'), { statusCode: 400 });
return prisma_1.prisma.employee.update({ where: { id: employeeId }, data: { isActive: false } });
}
async function reactivateEmployee(companyId, requesterRole, employeeId) {
if (requesterRole !== 'OWNER')
throw Object.assign(new Error('Only the account owner can reactivate team members'), { statusCode: 403 });
await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
return prisma_1.prisma.employee.update({ where: { id: employeeId }, data: { isActive: true } });
}
async function removeEmployee(companyId, requesterRole, employeeId) {
if (requesterRole !== 'OWNER')
throw Object.assign(new Error('Only the account owner can remove team members'), { statusCode: 403 });
const target = await prisma_1.prisma.employee.findFirstOrThrow({ where: { id: employeeId, companyId } });
if (target.role === 'OWNER')
throw Object.assign(new Error('Cannot remove the account owner'), { statusCode: 400 });
await prisma_1.prisma.employee.delete({ where: { id: employeeId } });
return { success: true };
}
//# sourceMappingURL=teamService.js.map
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
type AvailabilityStatus = 'AVAILABLE' | 'RESERVED' | 'MAINTENANCE' | 'UNAVAILABLE';
type DateRange = {
startDate: Date;
endDate: Date;
};
export interface VehicleAvailabilitySummary {
available: boolean;
status: AvailabilityStatus;
nextAvailableAt: Date | null;
blockingReason: 'RESERVATION' | 'MAINTENANCE' | 'BLOCK' | 'STATUS' | null;
}
export declare function getVehicleAvailabilitySummary(vehicleId: string, { range, includeDraftReservations, companyId, }?: {
range?: DateRange;
includeDraftReservations?: boolean;
companyId?: string;
}): Promise<VehicleAvailabilitySummary>;
export {};
//# sourceMappingURL=vehicleAvailabilityService.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"vehicleAvailabilityService.d.ts","sourceRoot":"","sources":["../../src/services/vehicleAvailabilityService.ts"],"names":[],"mappings":"AAIA,KAAK,kBAAkB,GAAG,WAAW,GAAG,UAAU,GAAG,aAAa,GAAG,aAAa,CAAA;AAElF,KAAK,SAAS,GAAG;IACf,SAAS,EAAE,IAAI,CAAA;IACf,OAAO,EAAE,IAAI,CAAA;CACd,CAAA;AAQD,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,kBAAkB,CAAA;IAC1B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAA;IAC5B,cAAc,EAAE,aAAa,GAAG,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAA;CAC1E;AAiCD,wBAAsB,6BAA6B,CACjD,SAAS,EAAE,MAAM,EACjB,EACE,KAAK,EACL,wBAA+B,EAC/B,SAAS,GACV,GAAE;IACD,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;CACd,GACL,OAAO,CAAC,0BAA0B,CAAC,CAqFrC"}
+111
View File
@@ -0,0 +1,111 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getVehicleAvailabilitySummary = getVehicleAvailabilitySummary;
const prisma_1 = require("../lib/prisma");
const BLOCKING_RESERVATION_STATUSES = ['DRAFT', 'CONFIRMED', 'ACTIVE'];
function startOfTodayUtc() {
const now = new Date();
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
}
function overlaps(range, source) {
return source.startDate < range.endDate && source.endDate > range.startDate;
}
function getNextAvailableAt(anchor, sources) {
const sorted = sources
.filter((source) => source.endDate > anchor)
.sort((a, b) => {
const startDelta = a.startDate.getTime() - b.startDate.getTime();
if (startDelta !== 0)
return startDelta;
return a.endDate.getTime() - b.endDate.getTime();
});
let candidate = anchor;
let advanced = false;
for (const source of sorted) {
if (source.endDate <= candidate)
continue;
if (source.startDate > candidate)
break;
candidate = new Date(Math.max(candidate.getTime(), source.endDate.getTime()));
advanced = true;
}
return advanced ? candidate : null;
}
async function getVehicleAvailabilitySummary(vehicleId, { range, includeDraftReservations = true, companyId, } = {}) {
const vehicle = await prisma_1.prisma.vehicle.findFirstOrThrow({
where: { id: vehicleId, ...(companyId ? { companyId } : {}) },
select: { status: true },
});
if (vehicle.status === 'OUT_OF_SERVICE') {
return {
available: false,
status: 'UNAVAILABLE',
nextAvailableAt: null,
blockingReason: 'STATUS',
};
}
const anchor = range?.startDate ?? startOfTodayUtc();
const reservationStatuses = includeDraftReservations
? [...BLOCKING_RESERVATION_STATUSES]
: BLOCKING_RESERVATION_STATUSES.filter((status) => status !== 'DRAFT');
const [reservations, blocks] = await Promise.all([
prisma_1.prisma.reservation.findMany({
where: {
vehicleId,
status: { in: reservationStatuses },
endDate: { gt: anchor },
},
select: { startDate: true, endDate: true },
orderBy: { startDate: 'asc' },
}),
prisma_1.prisma.vehicleCalendarBlock.findMany({
where: {
vehicleId,
endDate: { gt: anchor },
},
select: { startDate: true, endDate: true, type: true },
orderBy: { startDate: 'asc' },
}),
]);
const sources = [
...reservations.map((reservation) => ({
startDate: reservation.startDate,
endDate: reservation.endDate,
kind: 'RESERVATION',
})),
...blocks.map((block) => ({
startDate: block.startDate,
endDate: block.endDate,
kind: block.type === 'MAINTENANCE' ? 'MAINTENANCE' : 'BLOCK',
})),
];
const overlappingSources = range ? sources.filter((source) => overlaps(range, source)) : sources.filter((source) => source.startDate <= anchor && source.endDate > anchor);
const currentSource = overlappingSources.sort((a, b) => {
const priority = { MAINTENANCE: 0, BLOCK: 1, RESERVATION: 2 };
return priority[a.kind] - priority[b.kind];
})[0];
if (vehicle.status === 'MAINTENANCE') {
return {
available: false,
status: 'MAINTENANCE',
nextAvailableAt: getNextAvailableAt(anchor, sources) ?? null,
blockingReason: currentSource?.kind ?? 'STATUS',
};
}
if (!currentSource) {
return {
available: true,
status: 'AVAILABLE',
nextAvailableAt: null,
blockingReason: null,
};
}
const nextAvailableAnchor = currentSource.startDate > anchor ? currentSource.startDate : anchor;
return {
available: false,
status: currentSource.kind === 'RESERVATION' ? 'RESERVED' : currentSource.kind === 'MAINTENANCE' ? 'MAINTENANCE' : 'UNAVAILABLE',
nextAvailableAt: getNextAvailableAt(nextAvailableAnchor, sources),
blockingReason: currentSource.kind,
};
}
//# sourceMappingURL=vehicleAvailabilityService.js.map
@@ -0,0 +1 @@
{"version":3,"file":"vehicleAvailabilityService.js","sourceRoot":"","sources":["../../src/services/vehicleAvailabilityService.ts"],"names":[],"mappings":"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"}