billing invoice

This commit is contained in:
root
2026-05-25 05:35:55 -04:00
parent 8d0b7a5ceb
commit 33b6bb55f0
12 changed files with 5434 additions and 645 deletions
+95 -22
View File
@@ -11,7 +11,7 @@ interface InvoiceData {
phone?: string | null
address?: any
}
subscription: {
subscription?: {
plan: string
billingPeriod: string
currentPeriodStart?: string | null
@@ -24,6 +24,24 @@ interface InvoiceData {
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
}
}
const PLAN_LABEL: Record<string, string> = {
@@ -40,8 +58,15 @@ const PERIOD_LABEL: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = {
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 = {
@@ -307,12 +332,26 @@ function formatAddress(address: any): string {
function InvoiceDocument({ data }: { data: InvoiceData }) {
const statusColor = STATUS_COLORS[data.status] ?? '#6b7280'
const addressStr = formatAddress(data.company.address)
const planLabel = PLAN_LABEL[data.subscription.plan] ?? data.subscription.plan
const periodLabel = PERIOD_LABEL[data.subscription.billingPeriod] ?? data.subscription.billingPeriod
const description = `${planLabel} Plan — ${periodLabel} Subscription`
const periodStr = data.subscription.currentPeriodStart && data.subscription.currentPeriodEnd
? `${fmtDate(data.subscription.currentPeriodStart)} ${fmtDate(data.subscription.currentPeriodEnd)}`
: '—'
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.createElement(
Document,
@@ -397,12 +436,14 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
React.createElement(Text, { style: s.metaKey }, 'Billing Period'),
React.createElement(Text, { style: s.metaValue }, periodLabel),
),
React.createElement(
View,
{ style: s.metaRow },
React.createElement(Text, { style: s.metaKey }, 'Plan'),
React.createElement(Text, { style: s.metaValue }, planLabel),
),
data.subscription
? React.createElement(
View,
{ style: s.metaRow },
React.createElement(Text, { style: s.metaKey }, 'Plan'),
React.createElement(Text, { style: s.metaValue }, planLabel),
)
: null,
),
),
@@ -417,13 +458,21 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
React.createElement(Text, { style: [s.tableHeaderCell, s.col20Center] }, 'Period'),
React.createElement(Text, { style: [s.tableHeaderCell, s.col20] }, 'Amount'),
),
React.createElement(
View,
{ 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(data.amount, data.currency)),
),
...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.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)),
)
}),
),
// ── Totals ───────────────────────────────────────────────────
@@ -431,13 +480,37 @@ function InvoiceDocument({ data }: { data: InvoiceData }) {
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Subtotal'),
React.createElement(Text, { style: s.totalValue }, fmt(data.amount, data.currency)),
React.createElement(Text, { style: s.totalValue }, fmt(totals.subtotalAmount, data.currency)),
),
totals.discountAmount > 0
? React.createElement(
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Discounts'),
React.createElement(Text, { style: s.totalValue }, `- ${fmt(totals.discountAmount, data.currency)}`),
)
: null,
totals.creditAmount > 0
? React.createElement(
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Credits'),
React.createElement(Text, { style: s.totalValue }, `- ${fmt(totals.creditAmount, data.currency)}`),
)
: null,
totals.taxAmount > 0
? React.createElement(
View,
{ style: s.totalRow },
React.createElement(Text, { style: s.totalLabel }, 'Tax'),
React.createElement(Text, { style: s.totalValue }, fmt(totals.taxAmount, data.currency)),
)
: null,
React.createElement(
View,
{ style: s.grandTotalRow },
React.createElement(Text, { style: s.grandTotalLabel }, 'Total Due'),
React.createElement(Text, { style: s.grandTotalValue }, fmt(data.amount, data.currency)),
React.createElement(Text, { style: s.grandTotalValue }, fmt(totals.totalAmount, data.currency)),
),
// ── Payment Info ─────────────────────────────────────────────