fix payment customer and dashboard settings
Build & Deploy / Build & Push Docker Image (push) Successful in 12m39s
Test / Type Check (all packages) (push) Successful in 5m8s
Build & Deploy / Deploy to VPS (push) Successful in 7s
Test / API Unit Tests (push) Failing after 4m24s
Test / Homepage Unit Tests (push) Successful in 3m27s
Test / Storefront Unit Tests (push) Successful in 4m48s
Test / Admin Unit Tests (push) Successful in 3m0s
Test / Dashboard Unit Tests (push) Successful in 4m18s
Test / API Integration Tests (push) Failing after 4m34s
Build & Deploy / Build & Push Docker Image (push) Successful in 12m39s
Test / Type Check (all packages) (push) Successful in 5m8s
Build & Deploy / Deploy to VPS (push) Successful in 7s
Test / API Unit Tests (push) Failing after 4m24s
Test / Homepage Unit Tests (push) Successful in 3m27s
Test / Storefront Unit Tests (push) Successful in 4m48s
Test / Admin Unit Tests (push) Successful in 3m0s
Test / Dashboard Unit Tests (push) Successful in 4m18s
Test / API Integration Tests (push) Failing after 4m34s
This commit is contained in:
@@ -318,17 +318,27 @@ function fmt(amount: number, currency: string) {
|
||||
}
|
||||
|
||||
function fmtDate(iso: string | null | undefined) {
|
||||
if (!iso) return '—'
|
||||
if (!iso) return '-'
|
||||
return new Date(iso).toLocaleDateString('en-GB', { day: '2-digit', month: 'long', year: 'numeric' })
|
||||
}
|
||||
|
||||
function formatAddress(address: any): string {
|
||||
if (!address) return ''
|
||||
if (typeof address === 'string') return address
|
||||
if (typeof address.formatted === 'string') return address.formatted
|
||||
const parts = [address.street, address.city, address.region, address.country, address.zip]
|
||||
return parts.filter(Boolean).join(', ')
|
||||
}
|
||||
|
||||
function pdfText(value: unknown) {
|
||||
return String(value ?? '')
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[\u2013\u2014]/g, '-')
|
||||
.replace(/\u00d7/g, 'x')
|
||||
.replace(/[^\x20-\x7E]/g, '')
|
||||
}
|
||||
|
||||
function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
const statusColor = STATUS_COLORS[data.status] ?? '#6b7280'
|
||||
const addressStr = formatAddress(data.company.address)
|
||||
@@ -374,11 +384,11 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
View,
|
||||
{ style: s.invoiceMeta },
|
||||
React.createElement(Text, { style: s.invoiceTitle }, 'INVOICE'),
|
||||
React.createElement(Text, { style: s.invoiceNumber }, data.invoiceNumber),
|
||||
React.createElement(Text, { style: s.invoiceNumber }, pdfText(data.invoiceNumber)),
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: [s.statusBadge, { backgroundColor: statusColor }] },
|
||||
React.createElement(Text, { style: s.statusText }, data.status),
|
||||
React.createElement(Text, { style: s.statusText }, pdfText(data.status)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -391,13 +401,13 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
View,
|
||||
{ style: s.col },
|
||||
React.createElement(Text, { style: s.colLabel }, 'Bill To'),
|
||||
React.createElement(Text, { style: s.companyName }, data.company.name),
|
||||
React.createElement(Text, { style: s.companyDetail }, data.company.email),
|
||||
React.createElement(Text, { style: s.companyName }, pdfText(data.company.name)),
|
||||
React.createElement(Text, { style: s.companyDetail }, pdfText(data.company.email)),
|
||||
data.company.phone
|
||||
? React.createElement(Text, { style: s.companyDetail }, data.company.phone)
|
||||
? React.createElement(Text, { style: s.companyDetail }, pdfText(data.company.phone))
|
||||
: null,
|
||||
addressStr
|
||||
? React.createElement(Text, { style: [s.companyDetail, { marginTop: 4 }] }, addressStr)
|
||||
? React.createElement(Text, { style: [s.companyDetail, { marginTop: 4 }] }, pdfText(addressStr))
|
||||
: null,
|
||||
),
|
||||
React.createElement(
|
||||
@@ -408,7 +418,7 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
View,
|
||||
{ style: s.metaRow },
|
||||
React.createElement(Text, { style: s.metaKey }, 'Invoice Number'),
|
||||
React.createElement(Text, { style: s.metaValue }, data.invoiceNumber),
|
||||
React.createElement(Text, { style: s.metaValue }, pdfText(data.invoiceNumber)),
|
||||
),
|
||||
React.createElement(
|
||||
View,
|
||||
@@ -428,20 +438,20 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
View,
|
||||
{ style: s.metaRow },
|
||||
React.createElement(Text, { style: s.metaKey }, 'Currency'),
|
||||
React.createElement(Text, { style: s.metaValue }, data.currency),
|
||||
React.createElement(Text, { style: s.metaValue }, pdfText(data.currency)),
|
||||
),
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.metaRow },
|
||||
React.createElement(Text, { style: s.metaKey }, 'Billing Period'),
|
||||
React.createElement(Text, { style: s.metaValue }, periodLabel),
|
||||
React.createElement(Text, { style: s.metaValue }, pdfText(periodLabel)),
|
||||
),
|
||||
data.subscription
|
||||
? React.createElement(
|
||||
View,
|
||||
{ style: s.metaRow },
|
||||
React.createElement(Text, { style: s.metaKey }, 'Plan'),
|
||||
React.createElement(Text, { style: s.metaValue }, planLabel),
|
||||
React.createElement(Text, { style: s.metaValue }, pdfText(planLabel)),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@@ -460,17 +470,17 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
),
|
||||
...lineItems.map((item) => {
|
||||
const periodStr = item.periodStart && item.periodEnd
|
||||
? `${fmtDate(item.periodStart)} – ${fmtDate(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} (${item.quantity} x ${fmt(item.unitAmount, item.currency)})`
|
||||
: item.description
|
||||
return React.createElement(
|
||||
View,
|
||||
{ key: `${item.description}-${item.amount}-${item.periodStart ?? ''}`, style: s.tableRow },
|
||||
React.createElement(Text, { style: [s.tableCell, s.col60] }, description),
|
||||
React.createElement(Text, { style: [s.tableCellMuted, s.col20Center, { textAlign: 'center' }] }, periodStr),
|
||||
React.createElement(Text, { style: [s.tableCell, s.col20, { textAlign: 'right' }] }, fmt(item.amount, item.currency)),
|
||||
React.createElement(Text, { style: [s.tableCell, s.col60] }, pdfText(description)),
|
||||
React.createElement(Text, { style: [s.tableCellMuted, s.col20Center, { textAlign: 'center' }] }, pdfText(periodStr)),
|
||||
React.createElement(Text, { style: [s.tableCell, s.col20, { textAlign: 'right' }] }, pdfText(fmt(item.amount, item.currency))),
|
||||
)
|
||||
}),
|
||||
),
|
||||
@@ -522,14 +532,14 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
View,
|
||||
{ style: s.paymentRow },
|
||||
React.createElement(Text, { style: s.paymentKey }, 'Payment Provider'),
|
||||
React.createElement(Text, { style: s.paymentValue }, data.paymentProvider),
|
||||
React.createElement(Text, { style: s.paymentValue }, pdfText(data.paymentProvider)),
|
||||
),
|
||||
data.transactionId
|
||||
? React.createElement(
|
||||
View,
|
||||
{ style: s.paymentRow },
|
||||
React.createElement(Text, { style: s.paymentKey }, 'Transaction ID'),
|
||||
React.createElement(Text, { style: s.paymentValue }, data.transactionId),
|
||||
React.createElement(Text, { style: s.paymentValue }, pdfText(data.transactionId)),
|
||||
)
|
||||
: null,
|
||||
React.createElement(
|
||||
@@ -542,7 +552,7 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
View,
|
||||
{ style: s.paymentRow },
|
||||
React.createElement(Text, { style: s.paymentKey }, 'Status'),
|
||||
React.createElement(Text, { style: [s.paymentValue, { color: statusColor }] }, data.status),
|
||||
React.createElement(Text, { style: [s.paymentValue, { color: statusColor }] }, pdfText(data.status)),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -550,7 +560,7 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
React.createElement(
|
||||
View,
|
||||
{ style: s.footer },
|
||||
React.createElement(Text, { style: s.footerText }, 'RentalDriveGo — Car Rental Management Platform'),
|
||||
React.createElement(Text, { style: s.footerText }, 'RentalDriveGo - Car Rental Management Platform'),
|
||||
React.createElement(Text, { style: s.footerText }, `Generated on ${fmtDate(new Date().toISOString())}`),
|
||||
),
|
||||
),
|
||||
@@ -559,8 +569,12 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
|
||||
|
||||
export async function generateInvoicePdf(data: InvoiceData): Promise<Buffer> {
|
||||
const doc = React.createElement(InvoiceDocument, { data })
|
||||
const buffer = await renderToBuffer(doc as any)
|
||||
return buffer as unknown as Buffer
|
||||
try {
|
||||
const buffer = await renderToBuffer(doc as any)
|
||||
return buffer as unknown as Buffer
|
||||
} catch {
|
||||
return buildSimpleInvoicePdf(data)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildInvoiceNumber(invoiceId: string, createdAt: Date): string {
|
||||
@@ -568,3 +582,81 @@ export function buildInvoiceNumber(invoiceId: string, createdAt: Date): string {
|
||||
const short = invoiceId.slice(-6).toUpperCase()
|
||||
return `INV-${year}-${short}`
|
||||
}
|
||||
|
||||
function pdfEscape(value: unknown) {
|
||||
return pdfText(value).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)')
|
||||
}
|
||||
|
||||
function buildSimpleInvoicePdf(data: InvoiceData) {
|
||||
const lineItems = data.lineItems?.length
|
||||
? data.lineItems
|
||||
: [{ description: 'Invoice charge', amount: data.amount, currency: data.currency }]
|
||||
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,
|
||||
}
|
||||
const address = formatAddress(data.company.address)
|
||||
const lines = [
|
||||
'RentalDriveGo - Invoice',
|
||||
`Invoice Number: ${data.invoiceNumber}`,
|
||||
`Status: ${data.status}`,
|
||||
`Issue Date: ${fmtDate(data.issueDate)}`,
|
||||
data.dueDate ? `Due Date: ${fmtDate(data.dueDate)}` : null,
|
||||
`Bill To: ${data.company.name}`,
|
||||
`Email: ${data.company.email}`,
|
||||
data.company.phone ? `Phone: ${data.company.phone}` : null,
|
||||
address ? `Address: ${address}` : null,
|
||||
'',
|
||||
'Line Items',
|
||||
...lineItems.map((item) => `${item.description} - ${fmt(item.amount, item.currency)}`),
|
||||
'',
|
||||
`Subtotal: ${fmt(totals.subtotalAmount, data.currency)}`,
|
||||
totals.discountAmount ? `Discounts: -${fmt(totals.discountAmount, data.currency)}` : null,
|
||||
totals.creditAmount ? `Credits: -${fmt(totals.creditAmount, data.currency)}` : null,
|
||||
totals.taxAmount ? `Tax: ${fmt(totals.taxAmount, data.currency)}` : null,
|
||||
`Total: ${fmt(totals.totalAmount, data.currency)}`,
|
||||
`Amount Paid: ${fmt(totals.amountPaid, data.currency)}`,
|
||||
`Amount Due: ${fmt(totals.amountDue, data.currency)}`,
|
||||
'',
|
||||
`Payment Provider: ${data.paymentProvider}`,
|
||||
data.transactionId ? `Transaction ID: ${data.transactionId}` : null,
|
||||
].filter((line): line is string => line !== null)
|
||||
|
||||
const content = [
|
||||
'BT',
|
||||
'/F1 18 Tf',
|
||||
'50 790 Td',
|
||||
`(${pdfEscape(lines[0])}) Tj`,
|
||||
'/F1 10 Tf',
|
||||
...lines.slice(1).flatMap((line) => ['0 -18 Td', `(${pdfEscape(line)}) Tj`]),
|
||||
'ET',
|
||||
].join('\n')
|
||||
const objects = [
|
||||
'<< /Type /Catalog /Pages 2 0 R >>',
|
||||
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
|
||||
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
|
||||
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
|
||||
`<< /Length ${Buffer.byteLength(content, 'utf8')} >>\nstream\n${content}\nendstream`,
|
||||
]
|
||||
|
||||
let pdf = '%PDF-1.4\n'
|
||||
const offsets = [0]
|
||||
objects.forEach((object, index) => {
|
||||
offsets.push(Buffer.byteLength(pdf, 'utf8'))
|
||||
pdf += `${index + 1} 0 obj\n${object}\nendobj\n`
|
||||
})
|
||||
const xrefOffset = Buffer.byteLength(pdf, 'utf8')
|
||||
pdf += `xref\n0 ${objects.length + 1}\n`
|
||||
pdf += '0000000000 65535 f \n'
|
||||
for (let i = 1; i < offsets.length; i += 1) {
|
||||
pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`
|
||||
}
|
||||
pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`
|
||||
|
||||
return Buffer.from(pdf, 'utf8')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user