archetecture security fix
This commit is contained in:
+361
@@ -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
|
||||
Reference in New Issue
Block a user