506 lines
15 KiB
TypeScript
506 lines
15 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
}); |