integration tests
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 4m46s
Contract Testing / contract-snapshot (pull_request) Has been skipped

This commit is contained in:
2026-01-22 19:16:43 +01:00
parent 597bb48248
commit 2fba80da57
25 changed files with 5143 additions and 7496 deletions

View File

@@ -1,359 +1,568 @@
/**
* Integration Test: Sponsor Billing Use Case Orchestration
*
*
* Tests the orchestration logic of sponsor billing-related Use Cases:
* - GetBillingStatisticsUseCase: Retrieves billing statistics
* - GetPaymentMethodsUseCase: Retrieves payment methods
* - SetDefaultPaymentMethodUseCase: Sets default payment method
* - GetInvoicesUseCase: Retrieves invoices
* - DownloadInvoiceUseCase: Downloads invoice
* - Validates that Use Cases correctly interact with their Ports (Repositories, Event Publishers)
* - 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, afterAll, beforeEach } from 'vitest';
import { InMemorySponsorRepository } from '../../../adapters/sponsors/persistence/inmemory/InMemorySponsorRepository';
import { InMemoryBillingRepository } from '../../../adapters/billing/persistence/inmemory/InMemoryBillingRepository';
import { InMemoryEventPublisher } from '../../../adapters/events/InMemoryEventPublisher';
import { GetBillingStatisticsUseCase } from '../../../core/sponsors/use-cases/GetBillingStatisticsUseCase';
import { GetPaymentMethodsUseCase } from '../../../core/sponsors/use-cases/GetPaymentMethodsUseCase';
import { SetDefaultPaymentMethodUseCase } from '../../../core/sponsors/use-cases/SetDefaultPaymentMethodUseCase';
import { GetInvoicesUseCase } from '../../../core/sponsors/use-cases/GetInvoicesUseCase';
import { DownloadInvoiceUseCase } from '../../../core/sponsors/use-cases/DownloadInvoiceUseCase';
import { GetBillingStatisticsQuery } from '../../../core/sponsors/ports/GetBillingStatisticsQuery';
import { GetPaymentMethodsQuery } from '../../../core/sponsors/ports/GetPaymentMethodsQuery';
import { SetDefaultPaymentMethodCommand } from '../../../core/sponsors/ports/SetDefaultPaymentMethodCommand';
import { GetInvoicesQuery } from '../../../core/sponsors/ports/GetInvoicesQuery';
import { DownloadInvoiceCommand } from '../../../core/sponsors/ports/DownloadInvoiceCommand';
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 billingRepository: InMemoryBillingRepository;
let eventPublisher: InMemoryEventPublisher;
let getBillingStatisticsUseCase: GetBillingStatisticsUseCase;
let getPaymentMethodsUseCase: GetPaymentMethodsUseCase;
let setDefaultPaymentMethodUseCase: SetDefaultPaymentMethodUseCase;
let getInvoicesUseCase: GetInvoicesUseCase;
let downloadInvoiceUseCase: DownloadInvoiceUseCase;
let seasonSponsorshipRepository: InMemorySeasonSponsorshipRepository;
let paymentRepository: InMemoryPaymentRepository;
let getSponsorBillingUseCase: GetSponsorBillingUseCase;
let mockLogger: Logger;
beforeAll(() => {
// TODO: Initialize In-Memory repositories and event publisher
// sponsorRepository = new InMemorySponsorRepository();
// billingRepository = new InMemoryBillingRepository();
// eventPublisher = new InMemoryEventPublisher();
// getBillingStatisticsUseCase = new GetBillingStatisticsUseCase({
// sponsorRepository,
// billingRepository,
// eventPublisher,
// });
// getPaymentMethodsUseCase = new GetPaymentMethodsUseCase({
// sponsorRepository,
// billingRepository,
// eventPublisher,
// });
// setDefaultPaymentMethodUseCase = new SetDefaultPaymentMethodUseCase({
// sponsorRepository,
// billingRepository,
// eventPublisher,
// });
// getInvoicesUseCase = new GetInvoicesUseCase({
// sponsorRepository,
// billingRepository,
// eventPublisher,
// });
// downloadInvoiceUseCase = new DownloadInvoiceUseCase({
// sponsorRepository,
// billingRepository,
// eventPublisher,
// });
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(() => {
// TODO: Clear all In-Memory repositories before each test
// sponsorRepository.clear();
// billingRepository.clear();
// eventPublisher.clear();
sponsorRepository.clear();
seasonSponsorshipRepository.clear();
paymentRepository.clear();
});
describe('GetBillingStatisticsUseCase - Success Path', () => {
it('should retrieve billing statistics for a sponsor', async () => {
// TODO: Implement test
// Scenario: Sponsor with billing data
describe('GetSponsorBillingUseCase - Success Path', () => {
it('should retrieve billing statistics for a sponsor with paid invoices', async () => {
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has total spent: $5000
// And: The sponsor has pending payments: $1000
// And: The sponsor has next payment date: "2024-02-01"
// And: The sponsor has monthly average spend: $1250
// When: GetBillingStatisticsUseCase.execute() is called with sponsor ID
// Then: The result should show total spent: $5000
// And: The result should show pending payments: $1000
// And: The result should show next payment date: "2024-02-01"
// And: The result should show monthly average spend: $1250
// And: EventPublisher should emit BillingStatisticsAccessedEvent
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 statistics with zero values', async () => {
// TODO: Implement test
// Scenario: Sponsor with no billing data
it('should retrieve billing statistics with pending invoices', async () => {
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has no billing history
// When: GetBillingStatisticsUseCase.execute() is called with sponsor ID
// Then: The result should show total spent: $0
// And: The result should show pending payments: $0
// And: The result should show next payment date: null
// And: The result should show monthly average spend: $0
// And: EventPublisher should emit BillingStatisticsAccessedEvent
});
});
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
describe('GetBillingStatisticsUseCase - Error Handling', () => {
it('should throw error when sponsor does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent sponsor
// Given: No sponsor exists with the given ID
// When: GetBillingStatisticsUseCase.execute() is called with non-existent sponsor ID
// Then: Should throw SponsorNotFoundError
// And: EventPublisher should NOT emit any events
// 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 throw error when sponsor ID is invalid', async () => {
// TODO: Implement test
// Scenario: Invalid sponsor ID
// Given: An invalid sponsor ID (e.g., empty string, null, undefined)
// When: GetBillingStatisticsUseCase.execute() is called with invalid sponsor ID
// Then: Should throw ValidationError
// And: EventPublisher should NOT emit any events
});
});
describe('GetPaymentMethodsUseCase - Success Path', () => {
it('should retrieve payment methods for a sponsor', async () => {
// TODO: Implement test
// Scenario: Sponsor with multiple payment methods
it('should retrieve billing statistics with zero values when no invoices exist', async () => {
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 3 payment methods (1 default, 2 non-default)
// When: GetPaymentMethodsUseCase.execute() is called with sponsor ID
// Then: The result should contain all 3 payment methods
// And: Each payment method should display its details
// And: The default payment method should be marked
// And: EventPublisher should emit PaymentMethodsAccessedEvent
});
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
it('should retrieve payment methods with minimal data', async () => {
// TODO: Implement test
// Scenario: Sponsor with single payment method
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 1 payment method (default)
// When: GetPaymentMethodsUseCase.execute() is called with sponsor ID
// Then: The result should contain the single payment method
// And: EventPublisher should emit PaymentMethodsAccessedEvent
});
// 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);
it('should retrieve payment methods with empty result', async () => {
// TODO: Implement test
// Scenario: Sponsor with no payment methods
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has no payment methods
// When: GetPaymentMethodsUseCase.execute() is called with sponsor ID
// Then: The result should be empty
// And: EventPublisher should emit PaymentMethodsAccessedEvent
});
});
describe('GetPaymentMethodsUseCase - Error Handling', () => {
it('should throw error when sponsor does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent sponsor
// Given: No sponsor exists with the given ID
// When: GetPaymentMethodsUseCase.execute() is called with non-existent sponsor ID
// Then: Should throw SponsorNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('SetDefaultPaymentMethodUseCase - Success Path', () => {
it('should set default payment method for a sponsor', async () => {
// TODO: Implement test
// Scenario: Set default payment method
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 3 payment methods (1 default, 2 non-default)
// When: SetDefaultPaymentMethodUseCase.execute() is called with payment method ID
// Then: The payment method should become default
// And: The previous default should no longer be default
// And: EventPublisher should emit PaymentMethodUpdatedEvent
});
it('should set default payment method when no default exists', async () => {
// TODO: Implement test
// Scenario: Set default when none exists
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 2 payment methods (no default)
// When: SetDefaultPaymentMethodUseCase.execute() is called with payment method ID
// Then: The payment method should become default
// And: EventPublisher should emit PaymentMethodUpdatedEvent
});
});
describe('SetDefaultPaymentMethodUseCase - Error Handling', () => {
it('should throw error when sponsor does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent sponsor
// Given: No sponsor exists with the given ID
// When: SetDefaultPaymentMethodUseCase.execute() is called with non-existent sponsor ID
// Then: Should throw SponsorNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when payment method does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent payment method
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 2 payment methods
// When: SetDefaultPaymentMethodUseCase.execute() is called with non-existent payment method ID
// Then: Should throw PaymentMethodNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when payment method does not belong to sponsor', async () => {
// TODO: Implement test
// Scenario: Payment method belongs to different sponsor
// Given: Sponsor A exists with ID "sponsor-123"
// And: Sponsor B exists with ID "sponsor-456"
// And: Sponsor B has a payment method with ID "pm-789"
// When: SetDefaultPaymentMethodUseCase.execute() is called with sponsor ID "sponsor-123" and payment method ID "pm-789"
// Then: Should throw PaymentMethodNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('GetInvoicesUseCase - Success Path', () => {
it('should retrieve invoices for a sponsor', async () => {
// TODO: Implement test
// Scenario: Sponsor with multiple invoices
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 5 invoices (2 pending, 2 paid, 1 overdue)
// When: GetInvoicesUseCase.execute() is called with sponsor ID
// Then: The result should contain all 5 invoices
// And: Each invoice should display its details
// And: EventPublisher should emit InvoicesAccessedEvent
});
it('should retrieve invoices with minimal data', async () => {
// TODO: Implement test
// Scenario: Sponsor with single invoice
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 1 invoice
// When: GetInvoicesUseCase.execute() is called with sponsor ID
// Then: The result should contain the single invoice
// And: EventPublisher should emit InvoicesAccessedEvent
});
it('should retrieve invoices with empty result', async () => {
// TODO: Implement test
// Scenario: Sponsor with no invoices
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has no invoices
// When: GetInvoicesUseCase.execute() is called with sponsor ID
// Then: The result should be empty
// And: EventPublisher should emit InvoicesAccessedEvent
});
});
// When: GetSponsorBillingUseCase.execute() is called with sponsor ID
const result = await getSponsorBillingUseCase.execute({ sponsorId: 'sponsor-123' });
describe('GetInvoicesUseCase - Error Handling', () => {
it('should throw error when sponsor does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent sponsor
// Given: No sponsor exists with the given ID
// When: GetInvoicesUseCase.execute() is called with non-existent sponsor ID
// Then: Should throw SponsorNotFoundError
// And: EventPublisher should NOT emit any events
});
});
// Then: The result should contain billing data
expect(result.isOk()).toBe(true);
const billing = result.unwrap();
describe('DownloadInvoiceUseCase - Success Path', () => {
it('should download invoice for a sponsor', async () => {
// TODO: Implement test
// Scenario: Download invoice
// 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"
// And: The sponsor has an invoice with ID "inv-456"
// When: DownloadInvoiceUseCase.execute() is called with invoice ID
// Then: The invoice should be downloaded
// And: The invoice should be in PDF format
// And: EventPublisher should emit InvoiceDownloadedEvent
});
const sponsor = Sponsor.create({
id: 'sponsor-123',
name: 'Test Company',
contactEmail: 'test@example.com',
});
await sponsorRepository.create(sponsor);
it('should download invoice with correct content', async () => {
// TODO: Implement test
// Scenario: Download invoice with correct content
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has an invoice with ID "inv-456"
// When: DownloadInvoiceUseCase.execute() is called with invoice ID
// Then: The downloaded invoice should contain correct invoice number
// And: The downloaded invoice should contain correct date
// And: The downloaded invoice should contain correct amount
// And: EventPublisher should emit InvoiceDownloadedEvent
});
});
// 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);
describe('DownloadInvoiceUseCase - Error Handling', () => {
it('should throw error when invoice does not exist', async () => {
// TODO: Implement test
// Scenario: Non-existent invoice
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has no invoice with ID "inv-999"
// When: DownloadInvoiceUseCase.execute() is called with non-existent invoice ID
// Then: Should throw InvoiceNotFoundError
// And: EventPublisher should NOT emit any events
});
it('should throw error when invoice does not belong to sponsor', async () => {
// TODO: Implement test
// Scenario: Invoice belongs to different sponsor
// Given: Sponsor A exists with ID "sponsor-123"
// And: Sponsor B exists with ID "sponsor-456"
// And: Sponsor B has an invoice with ID "inv-789"
// When: DownloadInvoiceUseCase.execute() is called with invoice ID "inv-789"
// Then: Should throw InvoiceNotFoundError
// And: EventPublisher should NOT emit any events
});
});
describe('Billing Data Orchestration', () => {
it('should correctly aggregate billing statistics', async () => {
// TODO: Implement test
// Scenario: Billing statistics aggregation
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 3 invoices with amounts: $1000, $2000, $3000
// And: The sponsor has 1 pending invoice with amount: $500
// When: GetBillingStatisticsUseCase.execute() is called
// Then: Total spent should be $6000
// And: Pending payments should be $500
// And: EventPublisher should emit BillingStatisticsAccessedEvent
});
it('should correctly set default payment method', async () => {
// TODO: Implement test
// Scenario: Set default payment method
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has 3 payment methods
// When: SetDefaultPaymentMethodUseCase.execute() is called
// Then: Only one payment method should be default
// And: The default payment method should be marked correctly
// And: EventPublisher should emit PaymentMethodUpdatedEvent
});
it('should correctly retrieve invoices with status', async () => {
// TODO: Implement test
// Scenario: Invoice status retrieval
// Given: A sponsor exists with ID "sponsor-123"
// And: The sponsor has invoices with different statuses
// When: GetInvoicesUseCase.execute() is called
// Then: Each invoice should have correct status
// And: Pending invoices should be highlighted
// And: Overdue invoices should show warning
// And: EventPublisher should emit InvoicesAccessedEvent
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
});
});
});