Files
gridpilot.gg/apps/website/lib/view-models/BillingViewModel.ts
2025-12-19 23:18:53 +01:00

138 lines
3.9 KiB
TypeScript

/**
* Billing View Model
*
* View model for sponsor billing data with UI-specific transformations.
*/
export class BillingViewModel {
paymentMethods: PaymentMethodViewModel[];
invoices: InvoiceViewModel[];
stats: BillingStatsViewModel;
constructor(data: {
paymentMethods: any[];
invoices: any[];
stats: any;
}) {
this.paymentMethods = data.paymentMethods.map(pm => new PaymentMethodViewModel(pm));
this.invoices = data.invoices.map(inv => new InvoiceViewModel(inv));
this.stats = new BillingStatsViewModel(data.stats);
}
}
export class PaymentMethodViewModel {
id: string;
type: 'card' | 'bank' | 'sepa';
last4: string;
brand?: string;
isDefault: boolean;
expiryMonth?: number;
expiryYear?: number;
bankName?: string;
constructor(data: any) {
this.id = data.id;
this.type = data.type;
this.last4 = data.last4;
this.brand = data.brand;
this.isDefault = data.isDefault;
this.expiryMonth = data.expiryMonth;
this.expiryYear = data.expiryYear;
this.bankName = data.bankName;
}
get displayLabel(): string {
if (this.type === 'sepa' && this.bankName) {
return `${this.bankName} •••• ${this.last4}`;
}
return `${this.brand} •••• ${this.last4}`;
}
get expiryDisplay(): string | null {
if (this.expiryMonth && this.expiryYear) {
return `${String(this.expiryMonth).padStart(2, '0')}/${this.expiryYear}`;
}
return null;
}
}
export class InvoiceViewModel {
id: string;
invoiceNumber: string;
date: Date;
dueDate: Date;
amount: number;
vatAmount: number;
totalAmount: number;
status: 'paid' | 'pending' | 'overdue' | 'failed';
description: string;
sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform';
pdfUrl: string;
constructor(data: any) {
this.id = data.id;
this.invoiceNumber = data.invoiceNumber;
this.date = new Date(data.date);
this.dueDate = new Date(data.dueDate);
this.amount = data.amount;
this.vatAmount = data.vatAmount;
this.totalAmount = data.totalAmount;
this.status = data.status;
this.description = data.description;
this.sponsorshipType = data.sponsorshipType;
this.pdfUrl = data.pdfUrl;
}
get formattedTotalAmount(): string {
return `${this.totalAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
}
get formattedVatAmount(): string {
return `${this.vatAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
}
get formattedDate(): string {
return this.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
get isOverdue(): boolean {
return this.status === 'overdue' || (this.status === 'pending' && new Date() > this.dueDate);
}
}
export class BillingStatsViewModel {
totalSpent: number;
pendingAmount: number;
nextPaymentDate: Date;
nextPaymentAmount: number;
activeSponsorships: number;
averageMonthlySpend: number;
constructor(data: any) {
this.totalSpent = data.totalSpent;
this.pendingAmount = data.pendingAmount;
this.nextPaymentDate = new Date(data.nextPaymentDate);
this.nextPaymentAmount = data.nextPaymentAmount;
this.activeSponsorships = data.activeSponsorships;
this.averageMonthlySpend = data.averageMonthlySpend;
}
get formattedTotalSpent(): string {
return `${this.totalSpent.toLocaleString('de-DE')}`;
}
get formattedPendingAmount(): string {
return `${this.pendingAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
}
get formattedNextPaymentAmount(): string {
return `${this.nextPaymentAmount.toLocaleString('de-DE', { minimumFractionDigits: 2 })}`;
}
get formattedAverageMonthlySpend(): string {
return `${this.averageMonthlySpend.toLocaleString('de-DE')}`;
}
get formattedNextPaymentDate(): string {
return this.nextPaymentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
}