import { describe, it, expect } from 'vitest'; import type { BillingViewData } from '@/lib/view-data/BillingViewData'; import { BillingViewModel, PaymentMethodViewModel, InvoiceViewModel, BillingStatsViewModel } from './BillingViewModel'; describe('BillingViewModel', () => { it('maps arrays of payment methods, invoices and stats into view models', () => { const viewData: BillingViewData = { paymentMethods: [ { id: 'pm-1', type: 'card', last4: '4242', brand: 'Visa', isDefault: true, expiryMonth: 12, expiryYear: 2030, displayLabel: 'Visa •••• 4242', expiryDisplay: '12/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', formattedTotalAmount: '€119,00', formattedVatAmount: '€19,00', formattedDate: '2024-01-01', isOverdue: false, }, ], stats: { totalSpent: 1000, pendingAmount: 200, nextPaymentDate: '2024-03-01', nextPaymentAmount: 50, activeSponsorships: 3, averageMonthlySpend: 250, formattedTotalSpent: '€1.000,00', formattedPendingAmount: '€200,00', formattedNextPaymentAmount: '€50,00', formattedAverageMonthlySpend: '€250,00', formattedNextPaymentDate: '2024-03-01', }, }; const vm = new BillingViewModel(viewData); 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 = { id: 'pm-1', type: 'card' as const, last4: '4242', brand: 'Visa', isDefault: true, displayLabel: 'Visa •••• 4242', expiryDisplay: null, }; const sepa = { id: 'pm-2', type: 'sepa' as const, last4: '1337', bankName: 'Test Bank', isDefault: false, displayLabel: 'Test Bank •••• 1337', expiryDisplay: null, }; const cardVm = new PaymentMethodViewModel(card); const sepaVm = new PaymentMethodViewModel(sepa); expect(cardVm.displayLabel).toBe('Visa •••• 4242'); expect(sepaVm.displayLabel).toBe('Test Bank •••• 1337'); }); it('returns expiryDisplay when month and year are provided', () => { const withExpiry = { id: 'pm-1', type: 'card' as const, last4: '4242', brand: 'Visa', isDefault: true, expiryMonth: 3, expiryYear: 2030, displayLabel: 'Visa •••• 4242', expiryDisplay: '03/2030', }; const withoutExpiry = { id: 'pm-2', type: 'card' as const, last4: '9999', brand: 'Mastercard', isDefault: false, displayLabel: 'Mastercard •••• 9999', expiryDisplay: null, }; const withExpiryVm = new PaymentMethodViewModel(withExpiry); const withoutExpiryVm = new PaymentMethodViewModel(withoutExpiry); expect(withExpiryVm.expiryDisplay).toBe('03/2030'); expect(withoutExpiryVm.expiryDisplay).toBeNull(); }); }); describe('InvoiceViewModel', () => { it('formats monetary amounts and dates', () => { const viewData = { id: 'inv-1', invoiceNumber: 'INV-1', date: '2024-01-15', dueDate: '2024-02-01', amount: 100, vatAmount: 19, totalAmount: 119, status: 'paid' as const, description: 'Sponsorship', sponsorshipType: 'league' as const, pdfUrl: 'https://example.com/invoice.pdf', formattedTotalAmount: '€119,00', formattedVatAmount: '€19,00', formattedDate: '2024-01-15', isOverdue: false, }; const vm = new InvoiceViewModel(viewData); expect(vm.formattedTotalAmount).toBe('€119,00'); expect(vm.formattedVatAmount).toBe('€19,00'); 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 = { id: 'inv-1', invoiceNumber: 'INV-1', date: pastDate, dueDate: pastDate, amount: 0, vatAmount: 0, totalAmount: 0, status: 'overdue' as const, description: '', sponsorshipType: 'league' as const, pdfUrl: '', formattedTotalAmount: '€0,00', formattedVatAmount: '€0,00', formattedDate: pastDate, isOverdue: true, }; const pendingPastDue = { id: 'inv-2', invoiceNumber: 'INV-2', date: pastDate, dueDate: pastDate, amount: 0, vatAmount: 0, totalAmount: 0, status: 'pending' as const, description: '', sponsorshipType: 'league' as const, pdfUrl: '', formattedTotalAmount: '€0,00', formattedVatAmount: '€0,00', formattedDate: pastDate, isOverdue: true, }; const pendingFuture = { id: 'inv-3', invoiceNumber: 'INV-3', date: pastDate, dueDate: futureDate, amount: 0, vatAmount: 0, totalAmount: 0, status: 'pending' as const, description: '', sponsorshipType: 'league' as const, pdfUrl: '', formattedTotalAmount: '€0,00', formattedVatAmount: '€0,00', formattedDate: pastDate, isOverdue: false, }; const overdueVm = new InvoiceViewModel(overdue); const pendingPastDueVm = new InvoiceViewModel(pendingPastDue); const pendingFutureVm = new InvoiceViewModel(pendingFuture); expect(overdueVm.isOverdue).toBe(true); expect(pendingPastDueVm.isOverdue).toBe(true); expect(pendingFutureVm.isOverdue).toBe(false); }); }); describe('BillingStatsViewModel', () => { it('formats monetary fields and next payment date', () => { const viewData = { totalSpent: 1234, pendingAmount: 56.78, nextPaymentDate: '2024-03-01', nextPaymentAmount: 42, activeSponsorships: 2, averageMonthlySpend: 321, formattedTotalSpent: '€1.234,00', formattedPendingAmount: '€56,78', formattedNextPaymentAmount: '€42,00', formattedAverageMonthlySpend: '€321,00', formattedNextPaymentDate: '2024-03-01', }; const vm = new BillingStatsViewModel(viewData); expect(vm.formattedTotalSpent).toBe('€1.234,00'); expect(vm.formattedPendingAmount).toBe('€56,78'); expect(vm.formattedNextPaymentAmount).toBe('€42,00'); expect(vm.formattedAverageMonthlySpend).toBe('€321,00'); expect(typeof vm.formattedNextPaymentDate).toBe('string'); }); });