Files
gridpilot.gg/tests/integration/sponsor/sponsor-billing-use-cases.integration.test.ts
Marc Mintel 2fba80da57
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 4m46s
Contract Testing / contract-snapshot (pull_request) Has been skipped
integration tests
2026-01-22 19:16:43 +01:00

569 lines
21 KiB
TypeScript

/**
* Integration Test: Sponsor Billing Use Case Orchestration
*
* Tests the orchestration logic of sponsor billing-related Use Cases:
* - GetSponsorBillingUseCase: Retrieves sponsor billing information
* - Validates that Use Cases correctly interact with their Ports (Repositories)
* - Uses In-Memory adapters for fast, deterministic testing
*
* Focus: Business logic orchestration, NOT UI rendering
*/
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { InMemorySponsorRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySponsorRepository';
import { InMemorySeasonSponsorshipRepository } from '../../../adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
import { InMemoryPaymentRepository } from '../../../adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
import { GetSponsorBillingUseCase } from '../../../core/payments/application/use-cases/GetSponsorBillingUseCase';
import { Sponsor } from '../../../core/racing/domain/entities/sponsor/Sponsor';
import { SeasonSponsorship } from '../../../core/racing/domain/entities/season/SeasonSponsorship';
import { Payment, PaymentType, PaymentStatus } from '../../../core/payments/domain/entities/Payment';
import { Money } from '../../../core/racing/domain/value-objects/Money';
import { Logger } from '../../../core/shared/domain/Logger';
describe('Sponsor Billing Use Case Orchestration', () => {
let sponsorRepository: InMemorySponsorRepository;
let seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
let paymentRepository: InMemoryPaymentRepository;
let getSponsorBillingUseCase: GetSponsorBillingUseCase;
let mockLogger: Logger;
beforeAll(() => {
mockLogger = {
info: () => {},
debug: () => {},
warn: () => {},
error: () => {},
} as unknown as Logger;
sponsorRepository = new InMemorySponsorRepository(mockLogger);
seasonSponsorshipRepository = new InMemorySeasonSponsorshipRepository(mockLogger);
paymentRepository = new InMemoryPaymentRepository(mockLogger);
getSponsorBillingUseCase = new GetSponsorBillingUseCase(
paymentRepository,
seasonSponsorshipRepository,
);
});
beforeEach(() => {
sponsorRepository.clear();
seasonSponsorshipRepository.clear();
paymentRepository.clear();
});
describe('GetSponsorBillingUseCase - Success Path', () => {
it('should retrieve billing statistics for a sponsor with paid invoices', async () => {
// Given: A sponsor exists with ID "sponsor-123"
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// And: The sponsor has 2 active sponsorships
const sponsorship1 = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship1);
const sponsorship2 = SeasonSponsorship.create({
id: 'sponsorship-2',
sponsorId: 'sponsor-123',
seasonId: 'season-2',
tier: 'secondary',
pricing: Money.create(500, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship2);
// And: The sponsor has 3 paid invoices
const payment1: Payment = {
id: 'payment-1',
type: PaymentType.SPONSORSHIP,
amount: 1000,
platformFee: 100,
netAmount: 900,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-1',
seasonId: 'season-1',
status: PaymentStatus.COMPLETED,
createdAt: new Date('2025-01-15'),
completedAt: new Date('2025-01-15'),
};
await paymentRepository.create(payment1);
const payment2: Payment = {
id: 'payment-2',
type: PaymentType.SPONSORSHIP,
amount: 2000,
platformFee: 200,
netAmount: 1800,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-2',
seasonId: 'season-2',
status: PaymentStatus.COMPLETED,
createdAt: new Date('2025-02-15'),
completedAt: new Date('2025-02-15'),
};
await paymentRepository.create(payment2);
const payment3: Payment = {
id: 'payment-3',
type: PaymentType.SPONSORSHIP,
amount: 3000,
platformFee: 300,
netAmount: 2700,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-3',
seasonId: 'season-3',
status: PaymentStatus.COMPLETED,
createdAt: new Date('2025-03-15'),
completedAt: new Date('2025-03-15'),
};
await paymentRepository.create(payment3);
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
// Then: The result should contain billing data
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
// And: The invoices should contain all 3 paid invoices
expect(billing.invoices).toHaveLength(3);
expect(billing.invoices[0].status).toBe('paid');
expect(billing.invoices[1].status).toBe('paid');
expect(billing.invoices[2].status).toBe('paid');
// And: The stats should show correct total spent
// Total spent = 1000 + 2000 + 3000 = 6000
expect(billing.stats.totalSpent).toBe(6000);
// And: The stats should show no pending payments
expect(billing.stats.pendingAmount).toBe(0);
// And: The stats should show no next payment date
expect(billing.stats.nextPaymentDate).toBeNull();
expect(billing.stats.nextPaymentAmount).toBeNull();
// And: The stats should show correct active sponsorships
expect(billing.stats.activeSponsorships).toBe(2);
// And: The stats should show correct average monthly spend
// Average monthly spend = total / months = 6000 / 3 = 2000
expect(billing.stats.averageMonthlySpend).toBe(2000);
});
it('should retrieve billing statistics with pending invoices', async () => {
// Given: A sponsor exists with ID "sponsor-123"
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// And: The sponsor has 1 active sponsorship
const sponsorship = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship);
// And: The sponsor has 1 paid invoice and 1 pending invoice
const payment1: Payment = {
id: 'payment-1',
type: PaymentType.SPONSORSHIP,
amount: 1000,
platformFee: 100,
netAmount: 900,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-1',
seasonId: 'season-1',
status: PaymentStatus.COMPLETED,
createdAt: new Date('2025-01-15'),
completedAt: new Date('2025-01-15'),
};
await paymentRepository.create(payment1);
const payment2: Payment = {
id: 'payment-2',
type: PaymentType.SPONSORSHIP,
amount: 500,
platformFee: 50,
netAmount: 450,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-2',
seasonId: 'season-2',
status: PaymentStatus.PENDING,
createdAt: new Date('2025-02-15'),
};
await paymentRepository.create(payment2);
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
// Then: The result should contain billing data
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
// And: The invoices should contain both invoices
expect(billing.invoices).toHaveLength(2);
// And: The stats should show correct total spent (only paid invoices)
expect(billing.stats.totalSpent).toBe(1000);
// And: The stats should show correct pending amount
expect(billing.stats.pendingAmount).toBe(550); // 500 + 50
// And: The stats should show next payment date
expect(billing.stats.nextPaymentDate).toBeDefined();
expect(billing.stats.nextPaymentAmount).toBe(550);
// And: The stats should show correct active sponsorships
expect(billing.stats.activeSponsorships).toBe(1);
});
it('should retrieve billing statistics with zero values when no invoices exist', async () => {
// Given: A sponsor exists with ID "sponsor-123"
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// And: The sponsor has 1 active sponsorship
const sponsorship = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship);
// And: The sponsor has no invoices
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
// Then: The result should contain billing data
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
// And: The invoices should be empty
expect(billing.invoices).toHaveLength(0);
// And: The stats should show zero values
expect(billing.stats.totalSpent).toBe(0);
expect(billing.stats.pendingAmount).toBe(0);
expect(billing.stats.nextPaymentDate).toBeNull();
expect(billing.stats.nextPaymentAmount).toBeNull();
expect(billing.stats.activeSponsorships).toBe(1);
expect(billing.stats.averageMonthlySpend).toBe(0);
});
it('should retrieve billing statistics with mixed invoice statuses', async () => {
// Given: A sponsor exists with ID "sponsor-123"
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// And: The sponsor has 1 active sponsorship
const sponsorship = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship);
// And: The sponsor has invoices with different statuses
const payment1: Payment = {
id: 'payment-1',
type: PaymentType.SPONSORSHIP,
amount: 1000,
platformFee: 100,
netAmount: 900,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-1',
seasonId: 'season-1',
status: PaymentStatus.COMPLETED,
createdAt: new Date('2025-01-15'),
completedAt: new Date('2025-01-15'),
};
await paymentRepository.create(payment1);
const payment2: Payment = {
id: 'payment-2',
type: PaymentType.SPONSORSHIP,
amount: 500,
platformFee: 50,
netAmount: 450,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-2',
seasonId: 'season-2',
status: PaymentStatus.PENDING,
createdAt: new Date('2025-02-15'),
};
await paymentRepository.create(payment2);
const payment3: Payment = {
id: 'payment-3',
type: PaymentType.SPONSORSHIP,
amount: 300,
platformFee: 30,
netAmount: 270,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-3',
seasonId: 'season-3',
status: PaymentStatus.FAILED,
createdAt: new Date('2025-03-15'),
};
await paymentRepository.create(payment3);
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
// Then: The result should contain billing data
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
// And: The invoices should contain all 3 invoices
expect(billing.invoices).toHaveLength(3);
// And: The stats should show correct total spent (only paid invoices)
expect(billing.stats.totalSpent).toBe(1000);
// And: The stats should show correct pending amount (pending + failed)
expect(billing.stats.pendingAmount).toBe(550); // 500 + 50
// And: The stats should show correct active sponsorships
expect(billing.stats.activeSponsorships).toBe(1);
});
});
describe('GetSponsorBillingUseCase - Error Handling', () => {
it('should return error when sponsor does not exist', async () => {
// Given: No sponsor exists with the given ID
// When: GetSponsorBillingUseCase.execute() is called with non-existent sponsor ID
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'non-existent-sponsor' });
// Then: Should return an error
expect(result.isErr()).toBe(true);
const error = result.unwrapErr();
expect(error.code).toBe('SPONSOR_NOT_FOUND');
});
});
describe('Sponsor Billing Data Orchestration', () => {
it('should correctly aggregate billing statistics across multiple invoices', async () => {
// Given: A sponsor exists with ID "sponsor-123"
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// And: The sponsor has 1 active sponsorship
const sponsorship = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship);
// And: The sponsor has 5 invoices with different amounts and statuses
const invoices = [
{ id: 'payment-1', amount: 1000, status: PaymentStatus.COMPLETED, date: new Date('2025-01-15') },
{ id: 'payment-2', amount: 2000, status: PaymentStatus.COMPLETED, date: new Date('2025-02-15') },
{ id: 'payment-3', amount: 1500, status: PaymentStatus.PENDING, date: new Date('2025-03-15') },
{ id: 'payment-4', amount: 3000, status: PaymentStatus.COMPLETED, date: new Date('2025-04-15') },
{ id: 'payment-5', amount: 500, status: PaymentStatus.FAILED, date: new Date('2025-05-15') },
];
for (const invoice of invoices) {
const payment: Payment = {
id: invoice.id,
type: PaymentType.SPONSORSHIP,
amount: invoice.amount,
platformFee: invoice.amount * 0.1,
netAmount: invoice.amount * 0.9,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-1',
seasonId: 'season-1',
status: invoice.status,
createdAt: invoice.date,
completedAt: invoice.status === PaymentStatus.COMPLETED ? invoice.date : undefined,
};
await paymentRepository.create(payment);
}
// When: GetSponsorBillingUseCase.execute() is called
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
// Then: The billing statistics should be correctly aggregated
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
// Total spent = 1000 + 2000 + 3000 = 6000
expect(billing.stats.totalSpent).toBe(6000);
// Pending amount = 1500 + 500 = 2000
expect(billing.stats.pendingAmount).toBe(2000);
// Average monthly spend = 6000 / 5 = 1200
expect(billing.stats.averageMonthlySpend).toBe(1200);
// Active sponsorships = 1
expect(billing.stats.activeSponsorships).toBe(1);
});
it('should correctly calculate average monthly spend over time', async () => {
// Given: A sponsor exists with ID "sponsor-123"
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// And: The sponsor has 1 active sponsorship
const sponsorship = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship);
// And: The sponsor has invoices spanning 6 months
const invoices = [
{ id: 'payment-1', amount: 1000, date: new Date('2025-01-15') },
{ id: 'payment-2', amount: 1500, date: new Date('2025-02-15') },
{ id: 'payment-3', amount: 2000, date: new Date('2025-03-15') },
{ id: 'payment-4', amount: 2500, date: new Date('2025-04-15') },
{ id: 'payment-5', amount: 3000, date: new Date('2025-05-15') },
{ id: 'payment-6', amount: 3500, date: new Date('2025-06-15') },
];
for (const invoice of invoices) {
const payment: Payment = {
id: invoice.id,
type: PaymentType.SPONSORSHIP,
amount: invoice.amount,
platformFee: invoice.amount * 0.1,
netAmount: invoice.amount * 0.9,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-1',
seasonId: 'season-1',
status: PaymentStatus.COMPLETED,
createdAt: invoice.date,
completedAt: invoice.date,
};
await paymentRepository.create(payment);
}
// When: GetSponsorBillingUseCase.execute() is called
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
// Then: The average monthly spend should be calculated correctly
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
// Total = 1000 + 1500 + 2000 + 2500 + 3000 + 3500 = 13500
// Months = 6 (Jan to Jun)
// Average = 13500 / 6 = 2250
expect(billing.stats.averageMonthlySpend).toBe(2250);
});
it('should correctly identify next payment date from pending invoices', async () => {
// Given: A sponsor exists with ID "sponsor-123"
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
// And: The sponsor has 1 active sponsorship
const sponsorship = SeasonSponsorship.create({
id: 'sponsorship-1',
sponsorId: 'sponsor-123',
seasonId: 'season-1',
tier: 'main',
pricing: Money.create(1000, 'USD'),
status: 'active',
});
await seasonSponsorshipRepository.create(sponsorship);
// And: The sponsor has multiple pending invoices with different due dates
const invoices = [
{ id: 'payment-1', amount: 500, date: new Date('2025-03-15') },
{ id: 'payment-2', amount: 1000, date: new Date('2025-02-15') },
{ id: 'payment-3', amount: 750, date: new Date('2025-01-15') },
];
for (const invoice of invoices) {
const payment: Payment = {
id: invoice.id,
type: PaymentType.SPONSORSHIP,
amount: invoice.amount,
platformFee: invoice.amount * 0.1,
netAmount: invoice.amount * 0.9,
payerId: 'sponsor-123',
payerType: 'sponsor',
leagueId: 'league-1',
seasonId: 'season-1',
status: PaymentStatus.PENDING,
createdAt: invoice.date,
};
await paymentRepository.create(payment);
}
// When: GetSponsorBillingUseCase.execute() is called
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
// Then: The next payment should be the earliest pending invoice
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
// Next payment should be from payment-3 (earliest date)
expect(billing.stats.nextPaymentDate).toBe('2025-01-15T00:00:00.000Z');
expect(billing.stats.nextPaymentAmount).toBe(825); // 750 + 75
});
});
});