view models
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { MembershipFeeService, GetMembershipFeesOutputDto } from './MembershipFeeService';
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import { MembershipFeeViewModel } from '../../view-models';
|
||||
import type { MembershipFeeDto } from '../../types/generated';
|
||||
|
||||
describe('MembershipFeeService', () => {
|
||||
let mockApiClient: Mocked<PaymentsApiClient>;
|
||||
let service: MembershipFeeService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getMembershipFees: vi.fn(),
|
||||
} as Mocked<PaymentsApiClient>;
|
||||
|
||||
service = new MembershipFeeService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getMembershipFees', () => {
|
||||
it('should call apiClient.getMembershipFees with correct leagueId and return mapped view models', async () => {
|
||||
const leagueId = 'league-123';
|
||||
const mockFees: MembershipFeeDto[] = [
|
||||
{ id: 'fee-1', leagueId: 'league-123' },
|
||||
{ id: 'fee-2', leagueId: 'league-123' },
|
||||
];
|
||||
const mockOutput: GetMembershipFeesOutputDto = { fees: mockFees };
|
||||
mockApiClient.getMembershipFees.mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await service.getMembershipFees(leagueId);
|
||||
|
||||
expect(mockApiClient.getMembershipFees).toHaveBeenCalledWith(leagueId);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toBeInstanceOf(MembershipFeeViewModel);
|
||||
expect(result[0].id).toEqual('fee-1');
|
||||
expect(result[0].leagueId).toEqual('league-123');
|
||||
expect(result[1]).toBeInstanceOf(MembershipFeeViewModel);
|
||||
expect(result[1].id).toEqual('fee-2');
|
||||
expect(result[1].leagueId).toEqual('league-123');
|
||||
});
|
||||
|
||||
it('should return empty array when no fees are returned', async () => {
|
||||
const leagueId = 'league-456';
|
||||
const mockOutput: GetMembershipFeesOutputDto = { fees: [] };
|
||||
mockApiClient.getMembershipFees.mockResolvedValue(mockOutput);
|
||||
|
||||
const result = await service.getMembershipFees(leagueId);
|
||||
|
||||
expect(mockApiClient.getMembershipFees).toHaveBeenCalledWith(leagueId);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
import { MembershipFeeDto } from '@/lib/types/generated/MembershipFeeDto';
|
||||
import { MembershipFeeViewModel } from '@/lib/view-models/MembershipFeeViewModel';
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import { MembershipFeeViewModel } from '../../view-models';
|
||||
import type { MembershipFeeDto } from '../../types/generated';
|
||||
|
||||
// TODO: This DTO should be generated from OpenAPI spec when the endpoint is added
|
||||
export interface GetMembershipFeesOutputDto {
|
||||
fees: MembershipFeeDto[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Membership Fee Service
|
||||
|
||||
244
apps/website/lib/services/payments/PaymentService.test.ts
Normal file
244
apps/website/lib/services/payments/PaymentService.test.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { PaymentService } from './PaymentService';
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import { PaymentViewModel, MembershipFeeViewModel, PrizeViewModel, WalletViewModel } from '../../view-models';
|
||||
|
||||
describe('PaymentService', () => {
|
||||
let mockApiClient: Mocked<PaymentsApiClient>;
|
||||
let service: PaymentService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getPayments: vi.fn(),
|
||||
createPayment: vi.fn(),
|
||||
getMembershipFees: vi.fn(),
|
||||
getPrizes: vi.fn(),
|
||||
getWallet: vi.fn(),
|
||||
} as Mocked<PaymentsApiClient>;
|
||||
|
||||
service = new PaymentService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getPayments', () => {
|
||||
it('should call apiClient.getPayments and return PaymentViewModel array', async () => {
|
||||
const mockResponse = {
|
||||
payments: [
|
||||
{
|
||||
id: 'payment-1',
|
||||
type: 'sponsorship' as const,
|
||||
amount: 100,
|
||||
platformFee: 10,
|
||||
netAmount: 90,
|
||||
payerId: 'user-1',
|
||||
payerType: 'sponsor' as const,
|
||||
leagueId: 'league-1',
|
||||
status: 'completed' as const,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.getPayments.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPayments();
|
||||
|
||||
expect(mockApiClient.getPayments).toHaveBeenCalledWith(undefined, undefined);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBeInstanceOf(PaymentViewModel);
|
||||
expect(result[0].id).toBe('payment-1');
|
||||
});
|
||||
|
||||
it('should call apiClient.getPayments with filters', async () => {
|
||||
const mockResponse = { payments: [] };
|
||||
mockApiClient.getPayments.mockResolvedValue(mockResponse);
|
||||
|
||||
await service.getPayments('league-1', 'user-1');
|
||||
|
||||
expect(mockApiClient.getPayments).toHaveBeenCalledWith('league-1', 'user-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPayment', () => {
|
||||
it('should return PaymentViewModel when payment exists', async () => {
|
||||
const mockResponse = {
|
||||
payments: [
|
||||
{
|
||||
id: 'payment-1',
|
||||
type: 'sponsorship' as const,
|
||||
amount: 100,
|
||||
platformFee: 10,
|
||||
netAmount: 90,
|
||||
payerId: 'user-1',
|
||||
payerType: 'sponsor' as const,
|
||||
leagueId: 'league-1',
|
||||
status: 'completed' as const,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.getPayments.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPayment('payment-1');
|
||||
|
||||
expect(result).toBeInstanceOf(PaymentViewModel);
|
||||
expect(result.id).toBe('payment-1');
|
||||
});
|
||||
|
||||
it('should throw error when payment does not exist', async () => {
|
||||
const mockResponse = { payments: [] };
|
||||
mockApiClient.getPayments.mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(service.getPayment('non-existent')).rejects.toThrow(
|
||||
'Payment with ID non-existent not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPayment', () => {
|
||||
it('should call apiClient.createPayment and return PaymentViewModel', async () => {
|
||||
const input = {
|
||||
type: 'sponsorship' as const,
|
||||
amount: 100,
|
||||
payerId: 'user-1',
|
||||
payerType: 'sponsor' as const,
|
||||
leagueId: 'league-1',
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
payment: {
|
||||
id: 'payment-1',
|
||||
type: 'sponsorship' as const,
|
||||
amount: 100,
|
||||
platformFee: 10,
|
||||
netAmount: 90,
|
||||
payerId: 'user-1',
|
||||
payerType: 'sponsor' as const,
|
||||
leagueId: 'league-1',
|
||||
status: 'pending' as const,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
mockApiClient.createPayment.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.createPayment(input);
|
||||
|
||||
expect(mockApiClient.createPayment).toHaveBeenCalledWith(input);
|
||||
expect(result).toBeInstanceOf(PaymentViewModel);
|
||||
expect(result.id).toBe('payment-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMembershipFees', () => {
|
||||
it('should return MembershipFeeViewModel when fee exists', async () => {
|
||||
const mockResponse = {
|
||||
fee: {
|
||||
id: 'fee-1',
|
||||
leagueId: 'league-1',
|
||||
type: 'season' as const,
|
||||
amount: 50,
|
||||
enabled: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
payments: [],
|
||||
};
|
||||
|
||||
mockApiClient.getMembershipFees.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getMembershipFees('league-1');
|
||||
|
||||
expect(result).toBeInstanceOf(MembershipFeeViewModel);
|
||||
expect(result!.id).toBe('fee-1');
|
||||
});
|
||||
|
||||
it('should return null when fee does not exist', async () => {
|
||||
const mockResponse = {
|
||||
fee: null,
|
||||
payments: [],
|
||||
};
|
||||
|
||||
mockApiClient.getMembershipFees.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getMembershipFees('league-1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrizes', () => {
|
||||
it('should call apiClient.getPrizes and return PrizeViewModel array', async () => {
|
||||
const mockResponse = {
|
||||
prizes: [
|
||||
{
|
||||
id: 'prize-1',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
name: 'First Place',
|
||||
amount: 100,
|
||||
type: 'cash' as const,
|
||||
awarded: false,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.getPrizes.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getPrizes();
|
||||
|
||||
expect(mockApiClient.getPrizes).toHaveBeenCalledWith(undefined, undefined);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBeInstanceOf(PrizeViewModel);
|
||||
expect(result[0].id).toBe('prize-1');
|
||||
});
|
||||
|
||||
it('should call apiClient.getPrizes with filters', async () => {
|
||||
const mockResponse = { prizes: [] };
|
||||
mockApiClient.getPrizes.mockResolvedValue(mockResponse);
|
||||
|
||||
await service.getPrizes('league-1', 'season-1');
|
||||
|
||||
expect(mockApiClient.getPrizes).toHaveBeenCalledWith('league-1', 'season-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWallet', () => {
|
||||
it('should call apiClient.getWallet and return WalletViewModel', async () => {
|
||||
const mockResponse = {
|
||||
wallet: {
|
||||
id: 'wallet-1',
|
||||
leagueId: 'league-1',
|
||||
balance: 1000,
|
||||
totalRevenue: 1500,
|
||||
totalPlatformFees: 100,
|
||||
totalWithdrawn: 400,
|
||||
createdAt: new Date(),
|
||||
currency: 'EUR',
|
||||
},
|
||||
transactions: [
|
||||
{
|
||||
id: 'tx-1',
|
||||
walletId: 'wallet-1',
|
||||
type: 'deposit' as const,
|
||||
amount: 500,
|
||||
description: 'Deposit',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.getWallet.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getWallet('user-1');
|
||||
|
||||
expect(mockApiClient.getWallet).toHaveBeenCalledWith('user-1');
|
||||
expect(result).toBeInstanceOf(WalletViewModel);
|
||||
expect(result.id).toBe('wallet-1');
|
||||
expect(result.balance).toBe(1000);
|
||||
expect(result.transactions).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,21 @@
|
||||
import { MembershipFeeViewModel } from '@/lib/view-models/MembershipFeeViewModel';
|
||||
import { PaymentViewModel } from '@/lib/view-models/PaymentViewModel';
|
||||
import { PrizeViewModel } from '@/lib/view-models/PrizeViewModel';
|
||||
import { WalletViewModel } from '@/lib/view-models/WalletViewModel';
|
||||
import type { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import type { PaymentDto, MembershipFeeDto, PrizeDto } from '../../types/generated';
|
||||
import type { PaymentDTO } from '../../types/generated/PaymentDto';
|
||||
import type { PrizeDto } from '../../types/generated/PrizeDto';
|
||||
|
||||
// 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,
|
||||
WalletViewModel,
|
||||
} from '../../view-models';
|
||||
type CreatePaymentInputDto = {
|
||||
type: 'sponsorship' | 'membership_fee';
|
||||
amount: number;
|
||||
payerId: string;
|
||||
payerType: 'sponsor' | 'driver';
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Payment Service
|
||||
@@ -27,7 +33,7 @@ export class PaymentService {
|
||||
*/
|
||||
async getPayments(leagueId?: string, driverId?: string): Promise<PaymentViewModel[]> {
|
||||
const dto = await this.apiClient.getPayments(leagueId, driverId);
|
||||
return dto.payments.map((payment: PaymentDto) => new PaymentViewModel(payment));
|
||||
return dto.payments.map((payment: PaymentDTO) => new PaymentViewModel(payment));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +42,7 @@ export class PaymentService {
|
||||
async getPayment(paymentId: string): Promise<PaymentViewModel> {
|
||||
// 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);
|
||||
const payment = dto.payments.find((p: PaymentDTO) => p.id === paymentId);
|
||||
if (!payment) {
|
||||
throw new Error(`Payment with ID ${paymentId} not found`);
|
||||
}
|
||||
@@ -46,16 +52,17 @@ export class PaymentService {
|
||||
/**
|
||||
* Create a new payment
|
||||
*/
|
||||
async createPayment(input: CreatePaymentInputDto): Promise<CreatePaymentOutputDto> {
|
||||
return await this.apiClient.createPayment(input);
|
||||
async createPayment(input: CreatePaymentInputDto): Promise<PaymentViewModel> {
|
||||
const dto = await this.apiClient.createPayment(input);
|
||||
return new PaymentViewModel(dto.payment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get membership fees for a league
|
||||
*/
|
||||
async getMembershipFees(leagueId: string): Promise<MembershipFeeViewModel[]> {
|
||||
async getMembershipFees(leagueId: string): Promise<MembershipFeeViewModel | null> {
|
||||
const dto = await this.apiClient.getMembershipFees(leagueId);
|
||||
return dto.fees.map((fee: MembershipFeeDto) => new MembershipFeeViewModel(fee));
|
||||
return dto.fee ? new MembershipFeeViewModel(dto.fee) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,14 +78,7 @@ export class PaymentService {
|
||||
*/
|
||||
async getWallet(driverId: string): Promise<WalletViewModel> {
|
||||
const dto = await this.apiClient.getWallet(driverId);
|
||||
return new WalletViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a payment (alias for createPayment)
|
||||
*/
|
||||
async processPayment(input: CreatePaymentInputDto): Promise<CreatePaymentOutputDto> {
|
||||
return await this.createPayment(input);
|
||||
return new WalletViewModel({ ...dto.wallet, transactions: dto.transactions });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
77
apps/website/lib/services/payments/WalletService.test.ts
Normal file
77
apps/website/lib/services/payments/WalletService.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, Mocked } from 'vitest';
|
||||
import { WalletService } from './WalletService';
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import { WalletViewModel } from '../../view-models';
|
||||
|
||||
describe('WalletService', () => {
|
||||
let mockApiClient: Mocked<PaymentsApiClient>;
|
||||
let service: WalletService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getWallet: vi.fn(),
|
||||
} as Mocked<PaymentsApiClient>;
|
||||
|
||||
service = new WalletService(mockApiClient);
|
||||
});
|
||||
|
||||
describe('getWallet', () => {
|
||||
it('should call apiClient.getWallet and return WalletViewModel', async () => {
|
||||
const mockResponse = {
|
||||
wallet: {
|
||||
id: 'wallet-1',
|
||||
leagueId: 'league-1',
|
||||
balance: 1000,
|
||||
totalRevenue: 1500,
|
||||
totalPlatformFees: 100,
|
||||
totalWithdrawn: 400,
|
||||
createdAt: '2023-01-01T00:00:00Z',
|
||||
currency: 'EUR',
|
||||
},
|
||||
transactions: [
|
||||
{
|
||||
id: 'tx-1',
|
||||
walletId: 'wallet-1',
|
||||
amount: 500,
|
||||
description: 'Deposit',
|
||||
createdAt: '2023-01-01T00:00:00Z',
|
||||
type: 'deposit' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.getWallet.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getWallet('user-1');
|
||||
|
||||
expect(mockApiClient.getWallet).toHaveBeenCalledWith('user-1');
|
||||
expect(result).toBeInstanceOf(WalletViewModel);
|
||||
expect(result.id).toBe('wallet-1');
|
||||
expect(result.balance).toBe(1000);
|
||||
expect(result.transactions).toHaveLength(1);
|
||||
expect(result.transactions[0].id).toBe('tx-1');
|
||||
});
|
||||
|
||||
it('should handle wallet without transactions', async () => {
|
||||
const mockResponse = {
|
||||
wallet: {
|
||||
id: 'wallet-1',
|
||||
leagueId: 'league-1',
|
||||
balance: 1000,
|
||||
totalRevenue: 1500,
|
||||
totalPlatformFees: 100,
|
||||
totalWithdrawn: 400,
|
||||
createdAt: '2023-01-01T00:00:00Z',
|
||||
currency: 'EUR',
|
||||
},
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
mockApiClient.getWallet.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.getWallet('user-1');
|
||||
|
||||
expect(result.transactions).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { WalletViewModel } from '@/lib/view-models/WalletViewModel';
|
||||
import { PaymentsApiClient } from '../../api/payments/PaymentsApiClient';
|
||||
import { WalletViewModel } from '../../view-models';
|
||||
import { FullTransactionDto } from '../../view-models/WalletTransactionViewModel';
|
||||
|
||||
/**
|
||||
* Wallet Service
|
||||
@@ -16,7 +17,7 @@ export class WalletService {
|
||||
* Get wallet by driver ID with view model transformation
|
||||
*/
|
||||
async getWallet(driverId: string): Promise<WalletViewModel> {
|
||||
const dto = await this.apiClient.getWallet(driverId);
|
||||
return new WalletViewModel(dto);
|
||||
const { wallet, transactions } = await this.apiClient.getWallet(driverId);
|
||||
return new WalletViewModel({ ...wallet, transactions: transactions as FullTransactionDto[] });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user