import { describe, it, expect } from 'vitest'; import { BillingViewModel, PaymentMethodViewModel, InvoiceViewModel, BillingStatsViewModel } from './BillingViewModel'; describe('BillingViewModel', () => { it('maps arrays of payment methods, invoices and stats into view models', () => { const data = { paymentMethods: [ { id: 'pm-1', type: 'card', last4: '4242', brand: 'Visa', isDefault: true, expiryMonth: 12, expiryYear: 2030 }, ], invoices: [ { id: 'inv-1', invoiceNumber: 'INV-1', date: '2024-01-01', dueDate: '2024-02-01', amount: 100, vatAmount: 19, totalAmount: 119, status: 'pending', description: 'Sponsorship', sponsorshipType: 'league', pdfUrl: 'https://example.com/invoice.pdf', }, ], stats: { totalSpent: 1000, pendingAmount: 200, nextPaymentDate: '2024-03-01', nextPaymentAmount: 50, activeSponsorships: 3, averageMonthlySpend: 250, }, } as any; const vm = new BillingViewModel(data); expect(vm.paymentMethods).toHaveLength(1); expect(vm.paymentMethods[0]).toBeInstanceOf(PaymentMethodViewModel); expect(vm.invoices).toHaveLength(1); expect(vm.invoices[0]).toBeInstanceOf(InvoiceViewModel); expect(vm.stats).toBeInstanceOf(BillingStatsViewModel); }); }); describe('PaymentMethodViewModel', () => { it('builds displayLabel based on type and bankName/brand', () => { const card = new PaymentMethodViewModel({ id: 'pm-1', type: 'card', last4: '4242', brand: 'Visa', isDefault: true, }); const sepa = new PaymentMethodViewModel({ id: 'pm-2', type: 'sepa', last4: '1337', bankName: 'Test Bank', isDefault: false, }); expect(card.displayLabel).toBe('Visa •••• 4242'); expect(sepa.displayLabel).toBe('Test Bank •••• 1337'); }); it('returns expiryDisplay when month and year are provided', () => { const withExpiry = new PaymentMethodViewModel({ id: 'pm-1', type: 'card', last4: '4242', brand: 'Visa', isDefault: true, expiryMonth: 3, expiryYear: 2030, }); const withoutExpiry = new PaymentMethodViewModel({ id: 'pm-2', type: 'card', last4: '9999', brand: 'Mastercard', isDefault: false, }); expect(withExpiry.expiryDisplay).toBe('03/2030'); expect(withoutExpiry.expiryDisplay).toBeNull(); }); }); describe('InvoiceViewModel', () => { it('formats monetary amounts and dates', () => { const dto = { id: 'inv-1', invoiceNumber: 'INV-1', date: '2024-01-15', dueDate: '2024-02-01', amount: 100, vatAmount: 19, totalAmount: 119, status: 'paid', description: 'Sponsorship', sponsorshipType: 'league', pdfUrl: 'https://example.com/invoice.pdf', } as any; const vm = new InvoiceViewModel(dto); expect(vm.formattedTotalAmount).toBe(`€${(119).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); expect(vm.formattedVatAmount).toBe(`€${(19).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); expect(typeof vm.formattedDate).toBe('string'); }); it('detects overdue when status is overdue or pending past due date', () => { const now = new Date(); const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); const overdue = new InvoiceViewModel({ id: 'inv-1', invoiceNumber: 'INV-1', date: pastDate, dueDate: pastDate, amount: 0, vatAmount: 0, totalAmount: 0, status: 'overdue', description: '', sponsorshipType: 'league', pdfUrl: '', } as any); const pendingPastDue = new InvoiceViewModel({ id: 'inv-2', invoiceNumber: 'INV-2', date: pastDate, dueDate: pastDate, amount: 0, vatAmount: 0, totalAmount: 0, status: 'pending', description: '', sponsorshipType: 'league', pdfUrl: '', } as any); const pendingFuture = new InvoiceViewModel({ id: 'inv-3', invoiceNumber: 'INV-3', date: pastDate, dueDate: futureDate, amount: 0, vatAmount: 0, totalAmount: 0, status: 'pending', description: '', sponsorshipType: 'league', pdfUrl: '', } as any); expect(overdue.isOverdue).toBe(true); expect(pendingPastDue.isOverdue).toBe(true); expect(pendingFuture.isOverdue).toBe(false); }); }); describe('BillingStatsViewModel', () => { it('formats monetary fields and next payment date', () => { const dto = { totalSpent: 1234, pendingAmount: 56.78, nextPaymentDate: '2024-03-01', nextPaymentAmount: 42, activeSponsorships: 2, averageMonthlySpend: 321, } as any; const vm = new BillingStatsViewModel(dto); expect(vm.formattedTotalSpent).toBe(`€${(1234).toLocaleString('de-DE')}`); expect(vm.formattedPendingAmount).toBe(`€${(56.78).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); expect(vm.formattedNextPaymentAmount).toBe(`€${(42).toLocaleString('de-DE', { minimumFractionDigits: 2 })}`); expect(vm.formattedAverageMonthlySpend).toBe(`€${(321).toLocaleString('de-DE')}`); expect(typeof vm.formattedNextPaymentDate).toBe('string'); }); });