Files
gridpilot.gg/apps/website/lib/view-models/BillingViewModel.test.ts
Marc Mintel d97f50ed72
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 6m4s
Contract Testing / contract-snapshot (pull_request) Has been skipped
view data fixes
2026-01-23 11:59:49 +01:00

246 lines
7.2 KiB
TypeScript

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');
});
});