import type { Logger } from '@core/shared/domain/Logger'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import { League } from '../../domain/entities/League'; import { LeagueWallet } from '../../domain/entities/league-wallet/LeagueWallet'; import { Money } from '../../domain/value-objects/Money'; import { WithdrawFromLeagueWalletUseCase, type WithdrawFromLeagueWalletErrorCode, type WithdrawFromLeagueWalletInput } from './WithdrawFromLeagueWalletUseCase'; import { Transaction } from 'electron'; import { LeagueRepository } from '../../domain/repositories/LeagueRepository'; import { LeagueWalletRepository } from '../../domain/repositories/LeagueWalletRepository'; import { TransactionRepository } from '../../domain/repositories/TransactionRepository'; describe('WithdrawFromLeagueWalletUseCase', () => { let leagueRepository: { findById: Mock }; let walletRepository: { findByLeagueId: Mock; update: Mock }; let transactionRepository: { create: Mock }; let logger: { debug: Mock; info: Mock; warn: Mock; error: Mock; }; let useCase: WithdrawFromLeagueWalletUseCase; beforeEach(() => { leagueRepository = { findById: vi.fn() }; walletRepository = { findByLeagueId: vi.fn(), update: vi.fn() }; transactionRepository = { create: vi.fn() }; logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }; useCase = new WithdrawFromLeagueWalletUseCase( leagueRepository as unknown as LeagueRepository, walletRepository as unknown as LeagueWalletRepository, transactionRepository as unknown as TransactionRepository, logger as unknown as Logger ); }); 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'); }); 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'); }); 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'); }); 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'); }); 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); const presented = result.unwrap(); expect(transactionRepository.create).toHaveBeenCalledTimes(1); const createdTx = (transactionRepository.create as Mock).mock.calls[0]![0] as Transaction; 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(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(logger.error).toHaveBeenCalledTimes(1); }); });