import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import { WithdrawFromLeagueWalletUseCase, type WithdrawFromLeagueWalletErrorCode, type WithdrawFromLeagueWalletInput, type WithdrawFromLeagueWalletResult, } from './WithdrawFromLeagueWalletUseCase'; import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository'; import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository'; import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { Logger, UseCaseOutputPort } from '@core/shared/application'; import { League } from '../../domain/entities/League'; import { LeagueWallet } from '../../domain/entities/league-wallet/LeagueWallet'; import { Money } from '../../domain/value-objects/Money'; describe('WithdrawFromLeagueWalletUseCase', () => { let leagueRepository: { findById: Mock }; let walletRepository: { findByLeagueId: Mock; update: Mock }; let transactionRepository: { create: Mock }; let logger: Logger & { error: Mock }; let output: UseCaseOutputPort & { present: Mock }; let useCase: WithdrawFromLeagueWalletUseCase; beforeEach(() => { leagueRepository = { findById: vi.fn() }; walletRepository = { findByLeagueId: vi.fn(), update: vi.fn() }; transactionRepository = { create: vi.fn() }; logger = { error: vi.fn() } as unknown as Logger & { error: Mock }; output = { present: vi.fn() } as unknown as UseCaseOutputPort & { present: Mock; }; useCase = new WithdrawFromLeagueWalletUseCase( leagueRepository as unknown as ILeagueRepository, walletRepository as unknown as ILeagueWalletRepository, transactionRepository as unknown as ITransactionRepository, logger, output, ); }); it('returns LEAGUE_NOT_FOUND when league is missing', async () => { leagueRepository.findById.mockResolvedValue(null); const input: WithdrawFromLeagueWalletInput = { leagueId: 'league-1', requestedById: 'owner-1', amount: 100, currency: 'USD', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const err = result.unwrapErr() as ApplicationErrorCode< WithdrawFromLeagueWalletErrorCode, { message: string } >; expect(err.code).toBe('LEAGUE_NOT_FOUND'); expect(err.details.message).toBe('League with id league-1 not found'); expect(output.present).not.toHaveBeenCalled(); }); it('returns WALLET_NOT_FOUND when wallet is missing', async () => { const league = League.create({ id: 'league-1', name: 'Test League', description: 'desc', ownerId: 'owner-1', }); leagueRepository.findById.mockResolvedValue(league); walletRepository.findByLeagueId.mockResolvedValue(null); const input: WithdrawFromLeagueWalletInput = { leagueId: 'league-1', requestedById: 'owner-1', amount: 100, currency: 'USD', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const err = result.unwrapErr() as ApplicationErrorCode< WithdrawFromLeagueWalletErrorCode, { message: string } >; expect(err.code).toBe('WALLET_NOT_FOUND'); expect(err.details.message).toBe('Wallet for league league-1 not found'); expect(output.present).not.toHaveBeenCalled(); }); it('returns UNAUTHORIZED_WITHDRAWAL when requester is not owner', async () => { const league = League.create({ id: 'league-1', name: 'Test League', description: 'desc', ownerId: 'owner-1', }); const wallet = LeagueWallet.create({ id: 'wallet-1', leagueId: 'league-1', balance: Money.create(1000, 'USD'), }); leagueRepository.findById.mockResolvedValue(league); walletRepository.findByLeagueId.mockResolvedValue(wallet); const input: WithdrawFromLeagueWalletInput = { leagueId: 'league-1', requestedById: 'not-owner', amount: 100, currency: 'USD', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const err = result.unwrapErr() as ApplicationErrorCode< WithdrawFromLeagueWalletErrorCode, { message: string } >; expect(err.code).toBe('UNAUTHORIZED_WITHDRAWAL'); expect(err.details.message).toBe('Only the league owner can withdraw from the league wallet'); expect(output.present).not.toHaveBeenCalled(); }); it('returns INSUFFICIENT_FUNDS when wallet cannot withdraw amount', async () => { const league = League.create({ id: 'league-1', name: 'Test League', description: 'desc', ownerId: 'owner-1', }); const wallet = LeagueWallet.create({ id: 'wallet-1', leagueId: 'league-1', balance: Money.create(100, 'USD'), }); leagueRepository.findById.mockResolvedValue(league); walletRepository.findByLeagueId.mockResolvedValue(wallet); const input: WithdrawFromLeagueWalletInput = { leagueId: 'league-1', requestedById: 'owner-1', amount: 200, currency: 'USD', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const err = result.unwrapErr() as ApplicationErrorCode< WithdrawFromLeagueWalletErrorCode, { message: string } >; expect(err.code).toBe('INSUFFICIENT_FUNDS'); expect(err.details.message).toBe('Insufficient balance for withdrawal'); expect(output.present).not.toHaveBeenCalled(); }); it('creates withdrawal transaction and updates wallet on success', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2025-01-01T00:00:00.000Z')); const league = League.create({ id: 'league-1', name: 'Test League', description: 'desc', ownerId: 'owner-1', }); const startingBalance = Money.create(1000, 'USD'); const wallet = LeagueWallet.create({ id: 'wallet-1', leagueId: 'league-1', balance: startingBalance, }); leagueRepository.findById.mockResolvedValue(league); walletRepository.findByLeagueId.mockResolvedValue(wallet); transactionRepository.create.mockResolvedValue(undefined); walletRepository.update.mockResolvedValue(undefined); const input: WithdrawFromLeagueWalletInput = { leagueId: 'league-1', requestedById: 'owner-1', amount: 250, currency: 'USD', reason: 'Payout', }; const result = await useCase.execute(input); expect(result.isOk()).toBe(true); expect(result.unwrap()).toBeUndefined(); expect(transactionRepository.create).toHaveBeenCalledTimes(1); const createdTx = (transactionRepository.create as Mock).mock.calls[0]![0] as { id: { toString(): string }; type: string; amount: { amount: number; currency: string }; description: string | undefined; metadata: Record | undefined; walletId: { toString(): string }; }; const expectedTransactionId = `txn-${new Date('2025-01-01T00:00:00.000Z').getTime()}`; expect(createdTx.id.toString()).toBe(expectedTransactionId); expect(createdTx.type).toBe('withdrawal'); expect(createdTx.amount.amount).toBe(250); expect(createdTx.amount.currency).toBe('USD'); expect(createdTx.description).toBe('Payout'); expect(createdTx.metadata).toEqual({ reason: 'Payout', requestedById: 'owner-1' }); expect(createdTx.walletId.toString()).toBe(wallet.id.toString()); expect(walletRepository.update).toHaveBeenCalledTimes(1); const updatedWallet = (walletRepository.update as Mock).mock.calls[0]![0] as LeagueWallet; expect(updatedWallet.balance.amount).toBe(750); expect(updatedWallet.balance.currency).toBe('USD'); expect(updatedWallet.getTransactionIds()).toContain(expectedTransactionId); expect(output.present).toHaveBeenCalledTimes(1); const presented = (output.present as Mock).mock.calls[0]![0] as WithdrawFromLeagueWalletResult; expect(presented.leagueId).toBe('league-1'); expect(presented.amount.amount).toBe(250); expect(presented.amount.currency).toBe('USD'); expect(presented.transactionId).toBe(expectedTransactionId); expect(presented.walletBalanceAfter.amount).toBe(750); expect(presented.walletBalanceAfter.currency).toBe('USD'); const createOrder = (transactionRepository.create as Mock).mock.invocationCallOrder[0]!; const updateOrder = (walletRepository.update as Mock).mock.invocationCallOrder[0]!; expect(createOrder).toBeLessThan(updateOrder); vi.useRealTimers(); }); it('returns REPOSITORY_ERROR and logs when repository throws', async () => { const league = League.create({ id: 'league-1', name: 'Test League', description: 'desc', ownerId: 'owner-1', }); const wallet = LeagueWallet.create({ id: 'wallet-1', leagueId: 'league-1', balance: Money.create(1000, 'USD'), }); leagueRepository.findById.mockResolvedValue(league); walletRepository.findByLeagueId.mockResolvedValue(wallet); transactionRepository.create.mockRejectedValue(new Error('DB down')); const input: WithdrawFromLeagueWalletInput = { leagueId: 'league-1', requestedById: 'owner-1', amount: 100, currency: 'USD', }; const result = await useCase.execute(input); expect(result.isErr()).toBe(true); const err = result.unwrapErr() as ApplicationErrorCode< WithdrawFromLeagueWalletErrorCode, { message: string } >; expect(err.code).toBe('REPOSITORY_ERROR'); expect(err.details.message).toBe('DB down'); expect(output.present).not.toHaveBeenCalled(); expect(logger.error).toHaveBeenCalledTimes(1); }); });