225 lines
7.2 KiB
TypeScript
225 lines
7.2 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
|
|
import {
|
|
GetLeagueWalletUseCase,
|
|
type GetLeagueWalletResult,
|
|
type GetLeagueWalletInput,
|
|
type GetLeagueWalletErrorCode,
|
|
} from './GetLeagueWalletUseCase';
|
|
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
|
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
|
import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository';
|
|
import { LeagueWallet } from '../../domain/entities/league-wallet/LeagueWallet';
|
|
import { Money } from '../../domain/value-objects/Money';
|
|
import { Transaction } from '../../domain/entities/league-wallet/Transaction';
|
|
import { TransactionId } from '../../domain/entities/league-wallet/TransactionId';
|
|
import { LeagueWalletId } from '../../domain/entities/league-wallet/LeagueWalletId';
|
|
import type { UseCaseOutputPort } from '@core/shared/application';
|
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
|
|
describe('GetLeagueWalletUseCase', () => {
|
|
let leagueRepository: {
|
|
exists: Mock;
|
|
};
|
|
let leagueWalletRepository: {
|
|
findByLeagueId: Mock;
|
|
};
|
|
let transactionRepository: {
|
|
findByWalletId: Mock;
|
|
};
|
|
let output: UseCaseOutputPort<GetLeagueWalletResult> & { present: Mock };
|
|
let useCase: GetLeagueWalletUseCase;
|
|
|
|
beforeEach(() => {
|
|
leagueRepository = {
|
|
exists: vi.fn(),
|
|
};
|
|
|
|
leagueWalletRepository = {
|
|
findByLeagueId: vi.fn(),
|
|
};
|
|
|
|
transactionRepository = {
|
|
findByWalletId: vi.fn(),
|
|
};
|
|
|
|
output = {
|
|
present: vi.fn(),
|
|
} as unknown as UseCaseOutputPort<GetLeagueWalletResult> & { present: Mock };
|
|
|
|
useCase = new GetLeagueWalletUseCase(
|
|
leagueRepository as unknown as ILeagueRepository,
|
|
leagueWalletRepository as unknown as ILeagueWalletRepository,
|
|
transactionRepository as unknown as ITransactionRepository,
|
|
output,
|
|
);
|
|
});
|
|
|
|
it('returns mapped wallet data when wallet exists', async () => {
|
|
const leagueId = 'league-1';
|
|
|
|
const balance = Money.create(2450, 'USD');
|
|
const wallet = LeagueWallet.create({
|
|
id: 'wallet-1',
|
|
leagueId,
|
|
balance,
|
|
});
|
|
|
|
leagueRepository.exists.mockResolvedValue(true);
|
|
leagueWalletRepository.findByLeagueId.mockResolvedValue(wallet);
|
|
|
|
const sponsorshipTx = Transaction.create({
|
|
id: TransactionId.create('txn-1'),
|
|
walletId: LeagueWalletId.create(wallet.id.toString()),
|
|
type: 'sponsorship_payment',
|
|
amount: Money.create(1200, 'USD'),
|
|
description: 'Main Sponsor - TechCorp',
|
|
metadata: {},
|
|
}).complete();
|
|
|
|
const membershipTx = Transaction.create({
|
|
id: TransactionId.create('txn-2'),
|
|
walletId: LeagueWalletId.create(wallet.id.toString()),
|
|
type: 'membership_payment',
|
|
amount: Money.create(1600, 'USD'),
|
|
description: 'Season Fee - 32 drivers',
|
|
metadata: {},
|
|
}).complete();
|
|
|
|
const withdrawalTx = Transaction.create({
|
|
id: TransactionId.create('txn-3'),
|
|
walletId: LeagueWalletId.create(wallet.id.toString()),
|
|
type: 'withdrawal',
|
|
amount: Money.create(430, 'USD'),
|
|
description: 'Bank Transfer - Season 1 Payout',
|
|
metadata: {},
|
|
}).complete();
|
|
|
|
const pendingPrizeTx = Transaction.create({
|
|
id: TransactionId.create('txn-4'),
|
|
walletId: LeagueWalletId.create(wallet.id.toString()),
|
|
type: 'prize_payout',
|
|
amount: Money.create(150, 'USD'),
|
|
description: 'Championship Prize Pool (reserved)',
|
|
metadata: {},
|
|
});
|
|
|
|
const refundTx = Transaction.create({
|
|
id: TransactionId.create('txn-5'),
|
|
walletId: LeagueWalletId.create(wallet.id.toString()),
|
|
type: 'refund',
|
|
amount: Money.create(100, 'USD'),
|
|
description: 'Refund for cancelled sponsorship',
|
|
metadata: {},
|
|
});
|
|
|
|
const transactions = [
|
|
sponsorshipTx,
|
|
membershipTx,
|
|
withdrawalTx,
|
|
pendingPrizeTx,
|
|
refundTx,
|
|
];
|
|
|
|
transactionRepository.findByWalletId.mockResolvedValue(transactions);
|
|
|
|
const input: GetLeagueWalletInput = { leagueId };
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
expect(result.unwrap()).toBeUndefined();
|
|
expect(output.present).toHaveBeenCalledTimes(1);
|
|
|
|
const presented = (output.present as Mock).mock.calls[0]![0] as GetLeagueWalletResult;
|
|
|
|
expect(presented.wallet).toBe(wallet);
|
|
expect(presented.transactions).toHaveLength(transactions.length);
|
|
expect(presented.transactions[0]!.id).toEqual(
|
|
transactions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]!
|
|
.id,
|
|
);
|
|
|
|
const { aggregates } = presented;
|
|
|
|
const expectedTotalRevenue =
|
|
sponsorshipTx.amount.add(membershipTx.amount).add(pendingPrizeTx.amount);
|
|
|
|
const expectedTotalFees =
|
|
sponsorshipTx.platformFee
|
|
.add(membershipTx.platformFee)
|
|
.add(pendingPrizeTx.platformFee);
|
|
|
|
const expectedTotalWithdrawals = withdrawalTx.netAmount;
|
|
const expectedPendingPayouts = pendingPrizeTx.netAmount;
|
|
|
|
expect(aggregates.balance).toBe(balance);
|
|
expect(aggregates.totalRevenue.amount).toBe(expectedTotalRevenue.amount);
|
|
expect(aggregates.totalFees.amount).toBe(expectedTotalFees.amount);
|
|
expect(aggregates.totalWithdrawals.amount).toBe(
|
|
expectedTotalWithdrawals.amount,
|
|
);
|
|
expect(aggregates.pendingPayouts.amount).toBe(expectedPendingPayouts.amount);
|
|
});
|
|
|
|
it('returns error result when wallet is missing', async () => {
|
|
const leagueId = 'league-missing';
|
|
|
|
leagueRepository.exists.mockResolvedValue(true);
|
|
leagueWalletRepository.findByLeagueId.mockResolvedValue(null);
|
|
|
|
const input: GetLeagueWalletInput = { leagueId };
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
const err = result.unwrapErr() as ApplicationErrorCode<
|
|
GetLeagueWalletErrorCode,
|
|
{ message: string }
|
|
>;
|
|
|
|
expect(err.code).toBe('WALLET_NOT_FOUND');
|
|
expect(err.details.message).toBe('League wallet not found');
|
|
expect(output.present).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns league not found when league does not exist', async () => {
|
|
const leagueId = 'league-missing';
|
|
|
|
leagueRepository.exists.mockResolvedValue(false);
|
|
|
|
const input: GetLeagueWalletInput = { leagueId };
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
const err = result.unwrapErr() as ApplicationErrorCode<
|
|
GetLeagueWalletErrorCode,
|
|
{ message: string }
|
|
>;
|
|
|
|
expect(err.code).toBe('LEAGUE_NOT_FOUND');
|
|
expect(err.details.message).toBe('League not found');
|
|
expect(output.present).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns repository error when repository throws', async () => {
|
|
const leagueId = 'league-1';
|
|
|
|
leagueRepository.exists.mockRejectedValue(new Error('DB error'));
|
|
|
|
const input: GetLeagueWalletInput = { leagueId };
|
|
|
|
const result = await useCase.execute(input);
|
|
|
|
expect(result.isErr()).toBe(true);
|
|
const err = result.unwrapErr() as ApplicationErrorCode<
|
|
GetLeagueWalletErrorCode,
|
|
{ message: string }
|
|
>;
|
|
|
|
expect(err.code).toBe('REPOSITORY_ERROR');
|
|
expect(err.details.message).toBe('DB error');
|
|
expect(output.present).not.toHaveBeenCalled();
|
|
});
|
|
});
|