import { describe, it, expect, vi, beforeEach } from 'vitest'; import { PaymentService } from './PaymentService'; import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient'; import type { PaymentListPresenter } from '../../presenters/PaymentListPresenter'; import type { GetPaymentsOutputDto, CreatePaymentInputDto, CreatePaymentOutputDto, GetMembershipFeesOutputDto, GetPrizesOutputDto, GetWalletOutputDto, PaymentDto, MembershipFeeDto, PrizeDto, WalletDto, } from '../../dtos'; import type { PaymentViewModel, MembershipFeeViewModel, PrizeViewModel, WalletViewModel, } from '../../view-models'; describe('PaymentService', () => { let service: PaymentService; let mockApiClient: PaymentsApiClient; let mockPaymentListPresenter: PaymentListPresenter; let mockPresentPayment: (dto: any) => PaymentViewModel; let mockPresentMembershipFee: (dto: any) => MembershipFeeViewModel; let mockPresentPrize: (dto: any) => PrizeViewModel; let mockPresentWallet: (dto: any) => WalletViewModel; beforeEach(() => { mockApiClient = { getPayments: vi.fn(), createPayment: vi.fn(), getMembershipFees: vi.fn(), getPrizes: vi.fn(), getWallet: vi.fn(), } as unknown as PaymentsApiClient; mockPaymentListPresenter = { present: vi.fn(), } as unknown as PaymentListPresenter; mockPresentPayment = vi.fn(); mockPresentMembershipFee = vi.fn(); mockPresentPrize = vi.fn(); mockPresentWallet = vi.fn(); service = new PaymentService( mockApiClient, mockPaymentListPresenter, mockPresentPayment, mockPresentMembershipFee, mockPresentPrize, mockPresentWallet ); }); describe('constructor', () => { it('should create instance with injected dependencies', () => { expect(service).toBeInstanceOf(PaymentService); }); }); describe('getPayments', () => { it('should fetch all payments and transform via presenter', async () => { // Arrange const mockDto: GetPaymentsOutputDto = { payments: [ { id: 'payment-1', amount: 100, currency: 'USD', status: 'completed', createdAt: '2024-01-01', }, { id: 'payment-2', amount: 200, currency: 'EUR', status: 'pending', createdAt: '2024-01-02', }, ], }; const mockViewModels: PaymentViewModel[] = [ { id: 'payment-1', amount: 100, currency: 'USD', status: 'completed', createdAt: '2024-01-01', } as PaymentViewModel, { id: 'payment-2', amount: 200, currency: 'EUR', status: 'pending', createdAt: '2024-01-02', } as PaymentViewModel, ]; vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); // Act const result = await service.getPayments(); // Assert expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, undefined); expect(mockPaymentListPresenter.present).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModels); }); it('should filter payments by leagueId', async () => { // Arrange const leagueId = 'league-123'; const mockDto: GetPaymentsOutputDto = { payments: [] }; const mockViewModels: PaymentViewModel[] = []; vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); // Act await service.getPayments(leagueId); // Assert expect(mockApiClient.getPayments).toHaveBeenCalledWith(leagueId, undefined); }); it('should filter payments by driverId', async () => { // Arrange const driverId = 'driver-456'; const mockDto: GetPaymentsOutputDto = { payments: [] }; const mockViewModels: PaymentViewModel[] = []; vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); // Act await service.getPayments(undefined, driverId); // Assert expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, driverId); }); it('should propagate errors from API client', async () => { // Arrange const error = new Error('Failed to fetch payments'); vi.mocked(mockApiClient.getPayments).mockRejectedValue(error); // Act & Assert await expect(service.getPayments()).rejects.toThrow('Failed to fetch payments'); expect(mockApiClient.getPayments).toHaveBeenCalled(); expect(mockPaymentListPresenter.present).not.toHaveBeenCalled(); }); }); describe('getPayment', () => { it('should fetch single payment by ID', async () => { // Arrange const paymentId = 'payment-123'; const mockDto: GetPaymentsOutputDto = { payments: [ { id: paymentId, amount: 100, currency: 'USD', status: 'completed', createdAt: '2024-01-01', }, ], }; const mockViewModel: PaymentViewModel = { id: paymentId, amount: 100, currency: 'USD', status: 'completed', createdAt: '2024-01-01', } as PaymentViewModel; vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); vi.mocked(mockPresentPayment).mockReturnValue(mockViewModel); // Act const result = await service.getPayment(paymentId); // Assert expect(mockApiClient.getPayments).toHaveBeenCalled(); expect(mockPresentPayment).toHaveBeenCalledWith(mockDto.payments[0]); expect(result).toEqual(mockViewModel); }); it('should throw error when payment not found', async () => { // Arrange const paymentId = 'non-existent'; const mockDto: GetPaymentsOutputDto = { payments: [] }; vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); // Act & Assert await expect(service.getPayment(paymentId)).rejects.toThrow( `Payment with ID ${paymentId} not found` ); }); it('should propagate errors from API client', async () => { // Arrange const error = new Error('API error'); vi.mocked(mockApiClient.getPayments).mockRejectedValue(error); // Act & Assert await expect(service.getPayment('payment-123')).rejects.toThrow('API error'); }); }); describe('createPayment', () => { it('should create a new payment', async () => { // Arrange const input: CreatePaymentInputDto = { amount: 150, currency: 'USD', leagueId: 'league-123', driverId: 'driver-456', }; const mockOutput: CreatePaymentOutputDto = { paymentId: 'payment-new', success: true, }; vi.mocked(mockApiClient.createPayment).mockResolvedValue(mockOutput); // Act const result = await service.createPayment(input); // Assert expect(mockApiClient.createPayment).toHaveBeenCalledWith(input); expect(result).toEqual(mockOutput); }); it('should propagate errors from API client', async () => { // Arrange const input: CreatePaymentInputDto = { amount: 150, currency: 'USD', }; const error = new Error('Payment creation failed'); vi.mocked(mockApiClient.createPayment).mockRejectedValue(error); // Act & Assert await expect(service.createPayment(input)).rejects.toThrow('Payment creation failed'); }); }); describe('getMembershipFees', () => { it('should fetch membership fees for a league', async () => { // Arrange const leagueId = 'league-123'; const mockDto: GetMembershipFeesOutputDto = { fees: [ { leagueId, amount: 50, currency: 'USD', period: 'monthly', }, ], memberPayments: [], }; const mockViewModel: MembershipFeeViewModel = { leagueId, amount: 50, currency: 'USD', period: 'monthly', } as MembershipFeeViewModel; vi.mocked(mockApiClient.getMembershipFees).mockResolvedValue(mockDto); vi.mocked(mockPresentMembershipFee).mockReturnValue(mockViewModel); // Act const result = await service.getMembershipFees(leagueId); // Assert expect(mockApiClient.getMembershipFees).toHaveBeenCalledWith(leagueId); expect(mockPresentMembershipFee).toHaveBeenCalledWith(mockDto.fees[0]); expect(result).toEqual([mockViewModel]); }); it('should handle empty fees list', async () => { // Arrange const leagueId = 'league-123'; const mockDto: GetMembershipFeesOutputDto = { fees: [], memberPayments: [], }; vi.mocked(mockApiClient.getMembershipFees).mockResolvedValue(mockDto); // Act const result = await service.getMembershipFees(leagueId); // Assert expect(result).toEqual([]); expect(mockPresentMembershipFee).not.toHaveBeenCalled(); }); it('should propagate errors from API client', async () => { // Arrange const error = new Error('Failed to fetch fees'); vi.mocked(mockApiClient.getMembershipFees).mockRejectedValue(error); // Act & Assert await expect(service.getMembershipFees('league-123')).rejects.toThrow('Failed to fetch fees'); }); }); describe('getPrizes', () => { it('should fetch all prizes', async () => { // Arrange const mockDto: GetPrizesOutputDto = { prizes: [ { id: 'prize-1', name: 'First Place', amount: 1000, currency: 'USD', position: 1, }, ], }; const mockViewModel: PrizeViewModel = { id: 'prize-1', name: 'First Place', amount: 1000, currency: 'USD', position: 1, } as PrizeViewModel; vi.mocked(mockApiClient.getPrizes).mockResolvedValue(mockDto); vi.mocked(mockPresentPrize).mockReturnValue(mockViewModel); // Act const result = await service.getPrizes(); // Assert expect(mockApiClient.getPrizes).toHaveBeenCalledWith(undefined, undefined); expect(mockPresentPrize).toHaveBeenCalledWith(mockDto.prizes[0]); expect(result).toEqual([mockViewModel]); }); it('should filter prizes by leagueId and seasonId', async () => { // Arrange const leagueId = 'league-123'; const seasonId = 'season-456'; const mockDto: GetPrizesOutputDto = { prizes: [] }; vi.mocked(mockApiClient.getPrizes).mockResolvedValue(mockDto); // Act await service.getPrizes(leagueId, seasonId); // Assert expect(mockApiClient.getPrizes).toHaveBeenCalledWith(leagueId, seasonId); }); it('should propagate errors from API client', async () => { // Arrange const error = new Error('Failed to fetch prizes'); vi.mocked(mockApiClient.getPrizes).mockRejectedValue(error); // Act & Assert await expect(service.getPrizes()).rejects.toThrow('Failed to fetch prizes'); }); }); describe('getWallet', () => { it('should fetch wallet for a driver', async () => { // Arrange const driverId = 'driver-123'; const mockDto: GetWalletOutputDto = { driverId, balance: 500, currency: 'USD', transactions: [], }; const mockViewModel: WalletViewModel = { driverId, balance: 500, currency: 'USD', transactions: [], } as WalletViewModel; vi.mocked(mockApiClient.getWallet).mockResolvedValue(mockDto); vi.mocked(mockPresentWallet).mockReturnValue(mockViewModel); // Act const result = await service.getWallet(driverId); // Assert expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId); expect(mockPresentWallet).toHaveBeenCalledWith(mockDto); expect(result).toEqual(mockViewModel); }); it('should propagate errors from API client', async () => { // Arrange const error = new Error('Failed to fetch wallet'); vi.mocked(mockApiClient.getWallet).mockRejectedValue(error); // Act & Assert await expect(service.getWallet('driver-123')).rejects.toThrow('Failed to fetch wallet'); }); }); describe('processPayment', () => { it('should process payment using createPayment', async () => { // Arrange const input: CreatePaymentInputDto = { amount: 100, currency: 'USD', }; const mockOutput: CreatePaymentOutputDto = { paymentId: 'payment-123', success: true, }; vi.mocked(mockApiClient.createPayment).mockResolvedValue(mockOutput); // Act const result = await service.processPayment(input); // Assert expect(mockApiClient.createPayment).toHaveBeenCalledWith(input); expect(result).toEqual(mockOutput); }); it('should propagate errors', async () => { // Arrange const input: CreatePaymentInputDto = { amount: 100, currency: 'USD', }; const error = new Error('Processing failed'); vi.mocked(mockApiClient.createPayment).mockRejectedValue(error); // Act & Assert await expect(service.processPayment(input)).rejects.toThrow('Processing failed'); }); }); describe('getPaymentHistory', () => { it('should fetch payment history for a driver', async () => { // Arrange const driverId = 'driver-123'; const mockDto: GetPaymentsOutputDto = { payments: [ { id: 'payment-1', amount: 100, currency: 'USD', status: 'completed', createdAt: '2024-01-01', }, ], }; const mockViewModels: PaymentViewModel[] = [ { id: 'payment-1', amount: 100, currency: 'USD', status: 'completed', createdAt: '2024-01-01', } as PaymentViewModel, ]; vi.mocked(mockApiClient.getPayments).mockResolvedValue(mockDto); vi.mocked(mockPaymentListPresenter.present).mockReturnValue(mockViewModels); // Act const result = await service.getPaymentHistory(driverId); // Assert expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, driverId); expect(result).toEqual(mockViewModels); }); it('should propagate errors', async () => { // Arrange const error = new Error('History fetch failed'); vi.mocked(mockApiClient.getPayments).mockRejectedValue(error); // Act & Assert await expect(service.getPaymentHistory('driver-123')).rejects.toThrow('History fetch failed'); }); }); });