569 lines
21 KiB
TypeScript
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
|
|
});
|
|
});
|
|
});
|