integration tests
This commit is contained in:
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user