view models
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import { presentMembershipFee } from '../../presenters/MembershipFeePresenter';
|
||||
import type { MembershipFeeViewModel } from '../../view-models';
|
||||
import { MembershipFeeViewModel } from '../../view-models';
|
||||
import type { MembershipFeeDto } from '../../types/generated';
|
||||
|
||||
/**
|
||||
* Membership Fee Service
|
||||
*
|
||||
* Orchestrates membership fee operations by coordinating API calls and presentation logic.
|
||||
* Orchestrates membership fee operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class MembershipFeeService {
|
||||
@@ -14,14 +14,10 @@ export class MembershipFeeService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get membership fees by league ID with presentation transformation
|
||||
* Get membership fees by league ID with view model transformation
|
||||
*/
|
||||
async getMembershipFees(leagueId: string): Promise<MembershipFeeViewModel[]> {
|
||||
try {
|
||||
const dto = await this.apiClient.getMembershipFees(leagueId);
|
||||
return dto.fees.map(presentMembershipFee);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getMembershipFees(leagueId);
|
||||
return dto.fees.map((fee: MembershipFeeDto) => new MembershipFeeViewModel(fee));
|
||||
}
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import type { PaymentListPresenter } from '../../presenters/PaymentListPresenter';
|
||||
import type {
|
||||
CreatePaymentInputDto,
|
||||
CreatePaymentOutputDto,
|
||||
} from '../../dtos';
|
||||
import type {
|
||||
import type { PaymentDto, MembershipFeeDto, PrizeDto } from '../../types/generated';
|
||||
|
||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
||||
type CreatePaymentInputDto = { amount: number; leagueId: string; driverId: string; description: string };
|
||||
type CreatePaymentOutputDto = { id: string; success: boolean };
|
||||
import {
|
||||
PaymentViewModel,
|
||||
MembershipFeeViewModel,
|
||||
PrizeViewModel,
|
||||
@@ -14,114 +14,77 @@ import type {
|
||||
/**
|
||||
* Payment Service
|
||||
*
|
||||
* Orchestrates payment operations by coordinating API calls and presentation logic.
|
||||
* Orchestrates payment operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class PaymentService {
|
||||
constructor(
|
||||
private readonly apiClient: PaymentsApiClient,
|
||||
private readonly paymentListPresenter: PaymentListPresenter,
|
||||
private readonly presentPayment: (dto: any) => PaymentViewModel,
|
||||
private readonly presentMembershipFee: (dto: any) => MembershipFeeViewModel,
|
||||
private readonly presentPrize: (dto: any) => PrizeViewModel,
|
||||
private readonly presentWallet: (dto: any) => WalletViewModel
|
||||
private readonly apiClient: PaymentsApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get all payments with optional filters
|
||||
*/
|
||||
async getPayments(leagueId?: string, driverId?: string): Promise<PaymentViewModel[]> {
|
||||
try {
|
||||
const dto = await this.apiClient.getPayments(leagueId, driverId);
|
||||
return this.paymentListPresenter.present(dto);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getPayments(leagueId, driverId);
|
||||
return dto.payments.map((payment: PaymentDto) => new PaymentViewModel(payment));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single payment by ID
|
||||
*/
|
||||
async getPayment(paymentId: string): Promise<PaymentViewModel> {
|
||||
try {
|
||||
// Note: Assuming the API returns a single payment from the list
|
||||
const dto = await this.apiClient.getPayments();
|
||||
const payment = dto.payments.find(p => p.id === paymentId);
|
||||
if (!payment) {
|
||||
throw new Error(`Payment with ID ${paymentId} not found`);
|
||||
}
|
||||
return this.presentPayment(payment);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
// Note: Assuming the API returns a single payment from the list
|
||||
const dto = await this.apiClient.getPayments();
|
||||
const payment = dto.payments.find((p: PaymentDto) => p.id === paymentId);
|
||||
if (!payment) {
|
||||
throw new Error(`Payment with ID ${paymentId} not found`);
|
||||
}
|
||||
return new PaymentViewModel(payment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new payment
|
||||
*/
|
||||
async createPayment(input: CreatePaymentInputDto): Promise<CreatePaymentOutputDto> {
|
||||
try {
|
||||
return await this.apiClient.createPayment(input);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return await this.apiClient.createPayment(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get membership fees for a league
|
||||
*/
|
||||
async getMembershipFees(leagueId: string): Promise<MembershipFeeViewModel[]> {
|
||||
try {
|
||||
const dto = await this.apiClient.getMembershipFees(leagueId);
|
||||
return dto.fees.map(fee => this.presentMembershipFee(fee));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getMembershipFees(leagueId);
|
||||
return dto.fees.map((fee: MembershipFeeDto) => new MembershipFeeViewModel(fee));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prizes with optional filters
|
||||
*/
|
||||
async getPrizes(leagueId?: string, seasonId?: string): Promise<PrizeViewModel[]> {
|
||||
try {
|
||||
const dto = await this.apiClient.getPrizes(leagueId, seasonId);
|
||||
return dto.prizes.map(prize => this.presentPrize(prize));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getPrizes(leagueId, seasonId);
|
||||
return dto.prizes.map((prize: PrizeDto) => new PrizeViewModel(prize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wallet for a driver
|
||||
*/
|
||||
async getWallet(driverId: string): Promise<WalletViewModel> {
|
||||
try {
|
||||
const dto = await this.apiClient.getWallet(driverId);
|
||||
return this.presentWallet(dto);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getWallet(driverId);
|
||||
return new WalletViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a payment (alias for createPayment)
|
||||
*/
|
||||
async processPayment(input: CreatePaymentInputDto): Promise<CreatePaymentOutputDto> {
|
||||
try {
|
||||
return await this.createPayment(input);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return await this.createPayment(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get payment history for a user (driver)
|
||||
*/
|
||||
async getPaymentHistory(driverId: string): Promise<PaymentViewModel[]> {
|
||||
try {
|
||||
return await this.getPayments(undefined, driverId);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return await this.getPayments(undefined, driverId);
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { WalletService } from './WalletService';
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import type { GetWalletOutputDto } from '../../dtos';
|
||||
|
||||
import { presentWallet } from '../../presenters/WalletPresenter';
|
||||
|
||||
// Mock the presenter
|
||||
vi.mock('../../presenters/WalletPresenter', () => ({
|
||||
presentWallet: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('WalletService', () => {
|
||||
let mockApiClient: PaymentsApiClient;
|
||||
let service: WalletService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getWallet: vi.fn(),
|
||||
getMembershipFees: vi.fn(),
|
||||
getPayments: vi.fn(),
|
||||
createPayment: vi.fn(),
|
||||
getPrizes: vi.fn(),
|
||||
} as unknown as PaymentsApiClient;
|
||||
|
||||
service = new WalletService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getWallet', () => {
|
||||
it('should get wallet via API client and present it', async () => {
|
||||
// Arrange
|
||||
const driverId = 'driver-1';
|
||||
const dto: GetWalletOutputDto = {
|
||||
balance: 1000,
|
||||
currency: 'USD',
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
const expectedViewModel = {
|
||||
balance: 1000,
|
||||
currency: 'USD',
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.getWallet).mockResolvedValue(dto);
|
||||
|
||||
vi.mocked(presentWallet).mockReturnValue(expectedViewModel);
|
||||
|
||||
// Act
|
||||
const result = await service.getWallet(driverId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId);
|
||||
expect(mockApiClient.getWallet).toHaveBeenCalledTimes(1);
|
||||
expect(presentWallet).toHaveBeenCalledWith(dto);
|
||||
expect(result).toBe(expectedViewModel);
|
||||
});
|
||||
|
||||
it('should propagate API client errors', async () => {
|
||||
// Arrange
|
||||
const driverId = 'driver-1';
|
||||
const error = new Error('API Error: Failed to get wallet');
|
||||
vi.mocked(mockApiClient.getWallet).mockRejectedValue(error);
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.getWallet(driverId)).rejects.toThrow(
|
||||
'API Error: Failed to get wallet'
|
||||
);
|
||||
|
||||
expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId);
|
||||
expect(mockApiClient.getWallet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle different driver IDs', async () => {
|
||||
// Arrange
|
||||
const driverId = 'driver-2';
|
||||
const dto: GetWalletOutputDto = {
|
||||
balance: 500,
|
||||
currency: 'EUR',
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
const expectedViewModel = {
|
||||
balance: 500,
|
||||
currency: 'EUR',
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
vi.mocked(mockApiClient.getWallet).mockResolvedValue(dto);
|
||||
|
||||
vi.mocked(presentWallet).mockReturnValue(expectedViewModel);
|
||||
|
||||
// Act
|
||||
const result = await service.getWallet(driverId);
|
||||
|
||||
// Assert
|
||||
expect(mockApiClient.getWallet).toHaveBeenCalledWith(driverId);
|
||||
expect(result).toBe(expectedViewModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Constructor Dependency Injection', () => {
|
||||
it('should require apiClient', () => {
|
||||
// This test verifies the constructor signature
|
||||
expect(() => {
|
||||
new WalletService(mockApiClient);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should use injected apiClient', async () => {
|
||||
// Arrange
|
||||
const customApiClient = {
|
||||
getWallet: vi.fn().mockResolvedValue({ balance: 200, currency: 'USD', transactions: [] }),
|
||||
getMembershipFees: vi.fn(),
|
||||
getPayments: vi.fn(),
|
||||
createPayment: vi.fn(),
|
||||
getPrizes: vi.fn(),
|
||||
} as unknown as PaymentsApiClient;
|
||||
|
||||
const customService = new WalletService(customApiClient);
|
||||
|
||||
vi.mocked(presentWallet).mockReturnValue({ balance: 200, currency: 'USD', transactions: [] });
|
||||
|
||||
// Act
|
||||
await customService.getWallet('driver-1');
|
||||
|
||||
// Assert
|
||||
expect(customApiClient.getWallet).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import { presentWallet } from '../../presenters/WalletPresenter';
|
||||
import type { WalletViewModel } from '../../view-models';
|
||||
import { WalletViewModel } from '../../view-models';
|
||||
|
||||
/**
|
||||
* Wallet Service
|
||||
*
|
||||
* Orchestrates wallet operations by coordinating API calls and presentation logic.
|
||||
* Orchestrates wallet operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class WalletService {
|
||||
@@ -14,14 +13,10 @@ export class WalletService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get wallet by driver ID with presentation transformation
|
||||
* Get wallet by driver ID with view model transformation
|
||||
*/
|
||||
async getWallet(driverId: string): Promise<WalletViewModel> {
|
||||
try {
|
||||
const dto = await this.apiClient.getWallet(driverId);
|
||||
return presentWallet(dto);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const dto = await this.apiClient.getWallet(driverId);
|
||||
return new WalletViewModel(dto);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user