import { PaymentStatus, PaymentType } from '../../domain/entities/Payment'; import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository'; import type { IPaymentRepository } from '../../domain/repositories/IPaymentRepository'; import type { UseCase } from '@core/shared/application/UseCase'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; export interface SponsorBillingStats { totalSpent: number; pendingAmount: number; nextPaymentDate: string | null; nextPaymentAmount: number | null; activeSponsorships: number; averageMonthlySpend: number; } export interface SponsorInvoiceSummary { id: string; invoiceNumber: string; date: string; dueDate: string; amount: number; vatAmount: number; totalAmount: number; status: 'paid' | 'pending' | 'overdue' | 'failed'; description: string; sponsorshipType: 'league' | 'team' | 'driver' | 'race' | 'platform'; pdfUrl: string; } export interface SponsorPaymentMethodSummary { id: string; type: 'card' | 'bank' | 'sepa'; last4: string; brand?: string; isDefault: boolean; expiryMonth?: number; expiryYear?: number; bankName?: string; } export interface SponsorBillingSummary { paymentMethods: SponsorPaymentMethodSummary[]; invoices: SponsorInvoiceSummary[]; stats: SponsorBillingStats; } export interface GetSponsorBillingInput { sponsorId: string; } export interface GetSponsorBillingResult { paymentMethods: SponsorPaymentMethodSummary[]; invoices: SponsorInvoiceSummary[]; stats: SponsorBillingStats; } export type GetSponsorBillingErrorCode = never; export class GetSponsorBillingUseCase implements UseCase { constructor( private readonly paymentRepository: IPaymentRepository, private readonly seasonSponsorshipRepository: ISeasonSponsorshipRepository, ) {} async execute(input: GetSponsorBillingInput): Promise>> { const { sponsorId } = input; // In this in-memory implementation we derive billing data from payments // where the sponsor is the payer. const payments = await this.paymentRepository.findByFilters({ payerId: sponsorId, type: PaymentType.SPONSORSHIP, }); const sponsorships = await this.seasonSponsorshipRepository.findBySponsorId(sponsorId); const invoices: SponsorInvoiceSummary[] = payments.map((payment, index) => { const createdAt = payment.createdAt ?? new Date(); const dueDate = new Date(createdAt.getTime()); dueDate.setDate(dueDate.getDate() + 14); const vatAmount = +(payment.amount * 0.19).toFixed(2); const totalAmount = +(payment.amount + vatAmount).toFixed(2); let status: 'paid' | 'pending' | 'overdue' | 'failed' = 'pending'; if (payment.status === PaymentStatus.COMPLETED) status = 'paid'; else if (payment.status === PaymentStatus.FAILED || payment.status === PaymentStatus.REFUNDED) status = 'failed'; const now = new Date(); if (status === 'pending' && dueDate < now) { status = 'overdue'; } return { id: payment.id, invoiceNumber: `GP-${createdAt.getFullYear()}-${String(index + 1).padStart(6, '0')}`, date: createdAt.toISOString(), dueDate: dueDate.toISOString(), amount: payment.amount, vatAmount, totalAmount, status, description: 'Sponsorship payment', sponsorshipType: 'league', pdfUrl: '#', }; }); const totalSpent = invoices .filter(i => i.status === 'paid') .reduce((sum, i) => sum + i.totalAmount, 0); const pendingAmount = invoices .filter(i => i.status === 'pending' || i.status === 'overdue') .reduce((sum, i) => sum + i.totalAmount, 0); const pendingInvoices = invoices.filter(i => i.status === 'pending' || i.status === 'overdue'); const nextPending = pendingInvoices.length > 0 ? pendingInvoices.reduce((earliest, current) => (current.dueDate < earliest.dueDate ? current : earliest)) : null; const stats: SponsorBillingStats = { totalSpent, pendingAmount, nextPaymentDate: nextPending ? nextPending.dueDate : null, nextPaymentAmount: nextPending ? nextPending.totalAmount : null, activeSponsorships: sponsorships.filter(s => s.status === 'active').length, averageMonthlySpend: this.calculateAverageMonthlySpend(invoices), }; // NOTE: Payment methods are not yet persisted in core. For now, we expose // an empty list so the UI can still render correctly. A dedicated // payment-methods port can be added later when the concept exists in core. const paymentMethods: SponsorPaymentMethodSummary[] = []; const result: GetSponsorBillingResult = { paymentMethods, invoices, stats, }; return Result.ok(result); } private calculateAverageMonthlySpend(invoices: SponsorInvoiceSummary[]): number { if (invoices.length === 0) return 0; const sorted = [...invoices].sort((a, b) => a.date.localeCompare(b.date)); const first = new Date(sorted[0]!.date); const last = new Date(sorted[sorted.length - 1]!.date); const months = this.monthDiff(first, last) || 1; const total = sorted.reduce((sum, inv) => sum + inv.totalAmount, 0); return Math.round((total / months) * 100) / 100; } private monthDiff(d1: Date, d2: Date): number { const years = d2.getFullYear() - d1.getFullYear(); const months = d2.getMonth() - d1.getMonth(); return years * 12 + months + 1; } }