resolve todos in core
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
export interface SponsorshipRejectionNotificationPayload {
|
||||
requestId: string;
|
||||
sponsorId: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
tier: string;
|
||||
offeredAmountCents: number;
|
||||
currency: string;
|
||||
rejectedAt: Date;
|
||||
rejectedBy: string;
|
||||
rejectionReason?: string;
|
||||
}
|
||||
|
||||
export interface SponsorshipRejectionNotificationPort {
|
||||
notifySponsorshipRequestRejected(payload: SponsorshipRejectionNotificationPayload): Promise<void>;
|
||||
}
|
||||
@@ -69,43 +69,43 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
return Promise.resolve({ avatarUrl: 'avatar-default' });
|
||||
});
|
||||
|
||||
const result = await useCase.execute();
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual({
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 10,
|
||||
wins: 5,
|
||||
podiums: 7,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'avatar-driver1',
|
||||
},
|
||||
{
|
||||
id: 'driver2',
|
||||
name: 'Driver Two',
|
||||
rating: 2400,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 8,
|
||||
wins: 3,
|
||||
podiums: 4,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'avatar-driver2',
|
||||
},
|
||||
],
|
||||
totalRaces: 18,
|
||||
totalWins: 8,
|
||||
activeCount: 2,
|
||||
});
|
||||
});
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual({
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'US',
|
||||
racesCompleted: 10,
|
||||
wins: 5,
|
||||
podiums: 7,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'avatar-driver1',
|
||||
},
|
||||
{
|
||||
id: 'driver2',
|
||||
name: 'Driver Two',
|
||||
rating: 2400,
|
||||
skillLevel: 'intermediate',
|
||||
nationality: 'US',
|
||||
racesCompleted: 8,
|
||||
wins: 3,
|
||||
podiums: 4,
|
||||
isActive: true,
|
||||
rank: 2,
|
||||
avatarUrl: 'avatar-driver2',
|
||||
},
|
||||
],
|
||||
totalRaces: 18,
|
||||
totalWins: 8,
|
||||
activeCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty result when no drivers', async () => {
|
||||
const useCase = new GetDriversLeaderboardUseCase(
|
||||
@@ -147,30 +147,30 @@ describe('GetDriversLeaderboardUseCase', () => {
|
||||
mockDriverStatsGetDriverStats.mockReturnValue(null);
|
||||
mockGetDriverAvatar.mockResolvedValue({ avatarUrl: 'avatar-driver1' });
|
||||
|
||||
const result = await useCase.execute();
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual({
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'Pro',
|
||||
nationality: 'US',
|
||||
racesCompleted: 0,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
isActive: true,
|
||||
rank: 1,
|
||||
avatarUrl: 'avatar-driver1',
|
||||
},
|
||||
],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 1,
|
||||
});
|
||||
});
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.value).toEqual({
|
||||
drivers: [
|
||||
{
|
||||
id: 'driver1',
|
||||
name: 'Driver One',
|
||||
rating: 2500,
|
||||
skillLevel: 'advanced',
|
||||
nationality: 'US',
|
||||
racesCompleted: 0,
|
||||
wins: 0,
|
||||
podiums: 0,
|
||||
isActive: false,
|
||||
rank: 1,
|
||||
avatarUrl: 'avatar-driver1',
|
||||
},
|
||||
],
|
||||
totalRaces: 0,
|
||||
totalWins: 0,
|
||||
activeCount: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when repository throws', async () => {
|
||||
const useCase = new GetDriversLeaderboardUseCase(
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { IDriverStatsService } from '../../domain/services/IDriverStatsServ
|
||||
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
|
||||
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
|
||||
import type { DriversLeaderboardOutputPort, DriverLeaderboardItemOutputPort } from '../ports/output/DriversLeaderboardOutputPort';
|
||||
import type { SkillLevel } from '../../domain/services/SkillLevelService';
|
||||
import { SkillLevelService, type SkillLevel } from '../../domain/services/SkillLevelService';
|
||||
import type { AsyncUseCase, Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
@@ -40,17 +40,20 @@ export class GetDriversLeaderboardUseCase
|
||||
const driverItems: DriverLeaderboardItemOutputPort[] = drivers.map(driver => {
|
||||
const ranking = rankings.find(r => r.driverId === driver.id);
|
||||
const stats = this.driverStatsService.getDriverStats(driver.id);
|
||||
const rating = ranking?.rating ?? 0;
|
||||
const racesCompleted = stats?.totalRaces ?? 0;
|
||||
const skillLevel: SkillLevel = SkillLevelService.getSkillLevel(rating);
|
||||
|
||||
return {
|
||||
id: driver.id,
|
||||
name: driver.name.value,
|
||||
rating: ranking?.rating ?? 0,
|
||||
skillLevel: 'Pro' as SkillLevel, // TODO: map from domain
|
||||
rating,
|
||||
skillLevel,
|
||||
nationality: driver.country.value,
|
||||
racesCompleted: stats?.totalRaces ?? 0,
|
||||
racesCompleted,
|
||||
wins: stats?.wins ?? 0,
|
||||
podiums: stats?.podiums ?? 0,
|
||||
isActive: true, // TODO: determine from domain
|
||||
isActive: racesCompleted > 0,
|
||||
rank: ranking?.overallRank ?? 0,
|
||||
avatarUrl: avatarUrls[driver.id],
|
||||
};
|
||||
|
||||
163
core/racing/application/use-cases/GetLeagueWalletUseCase.test.ts
Normal file
163
core/racing/application/use-cases/GetLeagueWalletUseCase.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { GetLeagueWalletUseCase } from './GetLeagueWalletUseCase';
|
||||
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
||||
import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository';
|
||||
import { LeagueWallet } from '../../domain/entities/league-wallet/LeagueWallet';
|
||||
import { Money } from '../../domain/value-objects/Money';
|
||||
import { Transaction } from '../../domain/entities/league-wallet/Transaction';
|
||||
import { TransactionId } from '../../domain/entities/league-wallet/TransactionId';
|
||||
import { LeagueWalletId } from '../../domain/entities/league-wallet/LeagueWalletId';
|
||||
|
||||
describe('GetLeagueWalletUseCase', () => {
|
||||
let leagueWalletRepository: {
|
||||
findByLeagueId: Mock;
|
||||
};
|
||||
let transactionRepository: {
|
||||
findByWalletId: Mock;
|
||||
};
|
||||
let useCase: GetLeagueWalletUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
leagueWalletRepository = {
|
||||
findByLeagueId: vi.fn(),
|
||||
};
|
||||
|
||||
transactionRepository = {
|
||||
findByWalletId: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new GetLeagueWalletUseCase(
|
||||
leagueWalletRepository as unknown as ILeagueWalletRepository,
|
||||
transactionRepository as unknown as ITransactionRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns mapped wallet data when wallet exists', async () => {
|
||||
const leagueId = 'league-1';
|
||||
|
||||
const balance = Money.create(2450, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet-1',
|
||||
leagueId,
|
||||
balance,
|
||||
});
|
||||
|
||||
leagueWalletRepository.findByLeagueId.mockResolvedValue(wallet);
|
||||
|
||||
const sponsorshipTx = Transaction.create({
|
||||
id: TransactionId.create('txn-1'),
|
||||
walletId: LeagueWalletId.create(wallet.id.toString()),
|
||||
type: 'sponsorship_payment',
|
||||
amount: Money.create(1200, 'USD'),
|
||||
description: 'Main Sponsor - TechCorp',
|
||||
metadata: {},
|
||||
}).complete();
|
||||
|
||||
const membershipTx = Transaction.create({
|
||||
id: TransactionId.create('txn-2'),
|
||||
walletId: LeagueWalletId.create(wallet.id.toString()),
|
||||
type: 'membership_payment',
|
||||
amount: Money.create(1600, 'USD'),
|
||||
description: 'Season Fee - 32 drivers',
|
||||
metadata: {},
|
||||
}).complete();
|
||||
|
||||
const withdrawalTx = Transaction.create({
|
||||
id: TransactionId.create('txn-3'),
|
||||
walletId: LeagueWalletId.create(wallet.id.toString()),
|
||||
type: 'withdrawal',
|
||||
amount: Money.create(430, 'USD'),
|
||||
description: 'Bank Transfer - Season 1 Payout',
|
||||
metadata: {},
|
||||
}).complete();
|
||||
|
||||
const pendingPrizeTx = Transaction.create({
|
||||
id: TransactionId.create('txn-4'),
|
||||
walletId: LeagueWalletId.create(wallet.id.toString()),
|
||||
type: 'prize_payout',
|
||||
amount: Money.create(150, 'USD'),
|
||||
description: 'Championship Prize Pool (reserved)',
|
||||
metadata: {},
|
||||
});
|
||||
|
||||
const refundTx = Transaction.create({
|
||||
id: TransactionId.create('txn-5'),
|
||||
walletId: LeagueWalletId.create(wallet.id.toString()),
|
||||
type: 'refund',
|
||||
amount: Money.create(100, 'USD'),
|
||||
description: 'Refund for cancelled sponsorship',
|
||||
metadata: {},
|
||||
});
|
||||
|
||||
const transactions = [
|
||||
sponsorshipTx,
|
||||
membershipTx,
|
||||
withdrawalTx,
|
||||
pendingPrizeTx,
|
||||
refundTx,
|
||||
];
|
||||
|
||||
transactionRepository.findByWalletId.mockResolvedValue(transactions);
|
||||
|
||||
const result = await useCase.execute({ leagueId });
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const viewModel = result.unwrap();
|
||||
|
||||
expect(viewModel.balance).toBe(balance.amount);
|
||||
expect(viewModel.currency).toBe(balance.currency);
|
||||
|
||||
const expectedTotalRevenue =
|
||||
sponsorshipTx.amount.amount +
|
||||
membershipTx.amount.amount +
|
||||
pendingPrizeTx.amount.amount;
|
||||
|
||||
const expectedTotalFees =
|
||||
sponsorshipTx.platformFee.amount +
|
||||
membershipTx.platformFee.amount +
|
||||
pendingPrizeTx.platformFee.amount;
|
||||
|
||||
const expectedTotalWithdrawals = withdrawalTx.netAmount.amount;
|
||||
const expectedPendingPayouts = pendingPrizeTx.netAmount.amount;
|
||||
|
||||
expect(viewModel.totalRevenue).toBe(expectedTotalRevenue);
|
||||
expect(viewModel.totalFees).toBe(expectedTotalFees);
|
||||
expect(viewModel.totalWithdrawals).toBe(expectedTotalWithdrawals);
|
||||
expect(viewModel.pendingPayouts).toBe(expectedPendingPayouts);
|
||||
|
||||
expect(viewModel.transactions).toHaveLength(transactions.length);
|
||||
expect(viewModel.transactions[0]!.id).toBe(transactions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]!.id.toString());
|
||||
expect(viewModel.transactions.find(t => t.type === 'sponsorship')).toBeTruthy();
|
||||
expect(viewModel.transactions.find(t => t.type === 'membership')).toBeTruthy();
|
||||
expect(viewModel.transactions.find(t => t.type === 'withdrawal')).toBeTruthy();
|
||||
expect(viewModel.transactions.find(t => t.type === 'prize')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns error result when wallet is missing', async () => {
|
||||
const leagueId = 'league-missing';
|
||||
|
||||
leagueWalletRepository.findByLeagueId.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute({ leagueId });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
message: 'Wallet not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns repository error when repository throws', async () => {
|
||||
const leagueId = 'league-1';
|
||||
|
||||
leagueWalletRepository.findByLeagueId.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
const result = await useCase.execute({ leagueId });
|
||||
|
||||
expect(result.isErr()).toBe(true);
|
||||
expect(result.unwrapErr()).toEqual({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
message: 'Failed to fetch league wallet',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
||||
import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository';
|
||||
import type { GetLeagueWalletOutputPort } from '../ports/output/GetLeagueWalletOutputPort';
|
||||
import type { GetLeagueWalletOutputPort, WalletTransactionOutputPort } from '../ports/output/GetLeagueWalletOutputPort';
|
||||
import type { TransactionType } from '../../domain/entities/league-wallet/Transaction';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
|
||||
export interface GetLeagueWalletUseCaseParams {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use Case for retrieving league wallet information.
|
||||
*/
|
||||
@@ -16,84 +17,89 @@ export class GetLeagueWalletUseCase {
|
||||
private readonly leagueWalletRepository: ILeagueWalletRepository,
|
||||
private readonly transactionRepository: ITransactionRepository,
|
||||
) {}
|
||||
|
||||
|
||||
async execute(
|
||||
params: GetLeagueWalletUseCaseParams,
|
||||
): Promise<Result<GetLeagueWalletOutputPort, ApplicationErrorCode<'REPOSITORY_ERROR'>>> {
|
||||
try {
|
||||
// For now, return mock data to emulate previous state
|
||||
// TODO: Implement full domain logic when wallet entities are properly seeded
|
||||
const mockWallet: GetLeagueWalletOutputPort = {
|
||||
balance: 2450.00,
|
||||
currency: 'USD',
|
||||
totalRevenue: 3200.00,
|
||||
totalFees: 320.00,
|
||||
totalWithdrawals: 430.00,
|
||||
pendingPayouts: 150.00,
|
||||
canWithdraw: false,
|
||||
withdrawalBlockReason: 'Season 2 is still active. Withdrawals are available after season completion.',
|
||||
transactions: [
|
||||
{
|
||||
id: 'txn-1',
|
||||
type: 'sponsorship',
|
||||
description: 'Main Sponsor - TechCorp',
|
||||
amount: 1200.00,
|
||||
fee: 120.00,
|
||||
netAmount: 1080.00,
|
||||
date: '2025-12-01T00:00:00.000Z',
|
||||
status: 'completed',
|
||||
reference: 'SP-2025-001',
|
||||
},
|
||||
{
|
||||
id: 'txn-2',
|
||||
type: 'sponsorship',
|
||||
description: 'Secondary Sponsor - RaceFuel',
|
||||
amount: 400.00,
|
||||
fee: 40.00,
|
||||
netAmount: 360.00,
|
||||
date: '2025-12-01T00:00:00.000Z',
|
||||
status: 'completed',
|
||||
reference: 'SP-2025-002',
|
||||
},
|
||||
{
|
||||
id: 'txn-3',
|
||||
type: 'membership',
|
||||
description: 'Season Fee - 32 drivers',
|
||||
amount: 1600.00,
|
||||
fee: 160.00,
|
||||
netAmount: 1440.00,
|
||||
date: '2025-11-15T00:00:00.000Z',
|
||||
status: 'completed',
|
||||
reference: 'MF-2025-032',
|
||||
},
|
||||
{
|
||||
id: 'txn-4',
|
||||
type: 'withdrawal',
|
||||
description: 'Bank Transfer - Season 1 Payout',
|
||||
amount: -430.00,
|
||||
fee: 0,
|
||||
netAmount: -430.00,
|
||||
date: '2025-10-30T00:00:00.000Z',
|
||||
status: 'completed',
|
||||
reference: 'WD-2025-001',
|
||||
},
|
||||
{
|
||||
id: 'txn-5',
|
||||
type: 'prize',
|
||||
description: 'Championship Prize Pool (reserved)',
|
||||
amount: -150.00,
|
||||
fee: 0,
|
||||
netAmount: -150.00,
|
||||
date: '2025-12-05T00:00:00.000Z',
|
||||
status: 'pending',
|
||||
reference: 'PZ-2025-001',
|
||||
},
|
||||
],
|
||||
const wallet = await this.leagueWalletRepository.findByLeagueId(params.leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Wallet not found' });
|
||||
}
|
||||
|
||||
const transactions = await this.transactionRepository.findByWalletId(wallet.id.toString());
|
||||
|
||||
let totalRevenue = 0;
|
||||
let totalFees = 0;
|
||||
let totalWithdrawals = 0;
|
||||
let pendingPayouts = 0;
|
||||
|
||||
const transactionViewModels: WalletTransactionOutputPort[] = transactions
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.map(transaction => {
|
||||
const amount = transaction.amount.amount;
|
||||
const fee = transaction.platformFee.amount;
|
||||
const netAmount = transaction.netAmount.amount;
|
||||
|
||||
if (
|
||||
transaction.type === 'sponsorship_payment' ||
|
||||
transaction.type === 'membership_payment' ||
|
||||
transaction.type === 'prize_payout'
|
||||
) {
|
||||
totalRevenue += amount;
|
||||
totalFees += fee;
|
||||
}
|
||||
|
||||
if (transaction.type === 'withdrawal' && transaction.status === 'completed') {
|
||||
totalWithdrawals += netAmount;
|
||||
}
|
||||
|
||||
if (transaction.type === 'prize_payout' && transaction.status === 'pending') {
|
||||
pendingPayouts += netAmount;
|
||||
}
|
||||
|
||||
return {
|
||||
id: transaction.id.toString(),
|
||||
type: this.mapTransactionType(transaction.type),
|
||||
description: transaction.description ?? '',
|
||||
amount,
|
||||
fee,
|
||||
netAmount,
|
||||
date: transaction.createdAt.toISOString(),
|
||||
status: transaction.status === 'cancelled' ? 'failed' : transaction.status,
|
||||
};
|
||||
});
|
||||
|
||||
const output: GetLeagueWalletOutputPort = {
|
||||
balance: wallet.balance.amount,
|
||||
currency: wallet.balance.currency,
|
||||
totalRevenue,
|
||||
totalFees,
|
||||
totalWithdrawals,
|
||||
pendingPayouts,
|
||||
canWithdraw: true,
|
||||
transactions: transactionViewModels,
|
||||
};
|
||||
|
||||
return Result.ok(mockWallet);
|
||||
|
||||
return Result.ok(output);
|
||||
} catch {
|
||||
return Result.err({ code: 'REPOSITORY_ERROR', message: 'Failed to fetch league wallet' });
|
||||
}
|
||||
}
|
||||
|
||||
private mapTransactionType(type: TransactionType): WalletTransactionOutputPort['type'] {
|
||||
switch (type) {
|
||||
case 'sponsorship_payment':
|
||||
return 'sponsorship';
|
||||
case 'membership_payment':
|
||||
return 'membership';
|
||||
case 'prize_payout':
|
||||
return 'prize';
|
||||
case 'withdrawal':
|
||||
return 'withdrawal';
|
||||
case 'refund':
|
||||
return 'sponsorship';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,14 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
describe('RejectSponsorshipRequestUseCase', () => {
|
||||
let useCase: RejectSponsorshipRequestUseCase;
|
||||
let sponsorshipRequestRepo: { findById: Mock; update: Mock };
|
||||
let notificationPort: { notifySponsorshipRequestRejected: Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
sponsorshipRequestRepo = { findById: vi.fn(), update: vi.fn() };
|
||||
notificationPort = { notifySponsorshipRequestRejected: vi.fn() };
|
||||
useCase = new RejectSponsorshipRequestUseCase(
|
||||
sponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
|
||||
notificationPort as any,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -48,14 +51,20 @@ describe('RejectSponsorshipRequestUseCase', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject the request successfully', async () => {
|
||||
it('should reject the request successfully and notify sponsor with reason', async () => {
|
||||
const respondedAt = new Date('2023-01-01T00:00:00Z');
|
||||
const mockRequest = {
|
||||
id: 'request-1',
|
||||
sponsorId: 'sponsor-1',
|
||||
entityType: 'season',
|
||||
entityId: 'season-1',
|
||||
tier: 'main',
|
||||
offeredAmount: { amount: 1000, currency: 'USD' },
|
||||
status: 'pending',
|
||||
isPending: vi.fn().mockReturnValue(true),
|
||||
reject: vi.fn().mockReturnValue({
|
||||
id: 'request-1',
|
||||
respondedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
respondedAt,
|
||||
rejectionReason: 'Not interested',
|
||||
}),
|
||||
};
|
||||
@@ -72,20 +81,39 @@ describe('RejectSponsorshipRequestUseCase', () => {
|
||||
expect(result.unwrap()).toEqual({
|
||||
requestId: 'request-1',
|
||||
status: 'rejected',
|
||||
rejectedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
rejectedAt: respondedAt,
|
||||
reason: 'Not interested',
|
||||
});
|
||||
expect(sponsorshipRequestRepo.update).toHaveBeenCalledWith(mockRequest.reject());
|
||||
expect(notificationPort.notifySponsorshipRequestRejected).toHaveBeenCalledTimes(1);
|
||||
expect(notificationPort.notifySponsorshipRequestRejected).toHaveBeenCalledWith({
|
||||
requestId: 'request-1',
|
||||
sponsorId: 'sponsor-1',
|
||||
entityType: 'season',
|
||||
entityId: 'season-1',
|
||||
tier: 'main',
|
||||
offeredAmountCents: 1000,
|
||||
currency: 'USD',
|
||||
rejectedAt: respondedAt,
|
||||
rejectedBy: 'driver-1',
|
||||
rejectionReason: 'Not interested',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject the request successfully without reason', async () => {
|
||||
it('should reject the request successfully and notify sponsor without reason', async () => {
|
||||
const respondedAt = new Date('2023-01-01T00:00:00Z');
|
||||
const mockRequest = {
|
||||
id: 'request-1',
|
||||
sponsorId: 'sponsor-1',
|
||||
entityType: 'season',
|
||||
entityId: 'season-1',
|
||||
tier: 'main',
|
||||
offeredAmount: { amount: 1000, currency: 'USD' },
|
||||
status: 'pending',
|
||||
isPending: vi.fn().mockReturnValue(true),
|
||||
reject: vi.fn().mockReturnValue({
|
||||
id: 'request-1',
|
||||
respondedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
respondedAt,
|
||||
rejectionReason: undefined,
|
||||
}),
|
||||
};
|
||||
@@ -101,8 +129,21 @@ describe('RejectSponsorshipRequestUseCase', () => {
|
||||
expect(result.unwrap()).toEqual({
|
||||
requestId: 'request-1',
|
||||
status: 'rejected',
|
||||
rejectedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
rejectedAt: respondedAt,
|
||||
});
|
||||
expect(sponsorshipRequestRepo.update).toHaveBeenCalledWith(mockRequest.reject());
|
||||
expect(notificationPort.notifySponsorshipRequestRejected).toHaveBeenCalledTimes(1);
|
||||
expect(notificationPort.notifySponsorshipRequestRejected).toHaveBeenCalledWith({
|
||||
requestId: 'request-1',
|
||||
sponsorId: 'sponsor-1',
|
||||
entityType: 'season',
|
||||
entityId: 'season-1',
|
||||
tier: 'main',
|
||||
offeredAmountCents: 1000,
|
||||
currency: 'USD',
|
||||
rejectedAt: respondedAt,
|
||||
rejectedBy: 'driver-1',
|
||||
rejectionReason: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { SponsorshipRejectionNotificationPort } from '../ports/output/SponsorshipRejectionNotificationPort';
|
||||
|
||||
export interface RejectSponsorshipRequestDTO {
|
||||
requestId: string;
|
||||
@@ -24,6 +25,7 @@ export interface RejectSponsorshipRequestResultDTO {
|
||||
export class RejectSponsorshipRequestUseCase {
|
||||
constructor(
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly sponsorshipRejectionNotificationPort: SponsorshipRejectionNotificationPort,
|
||||
) {}
|
||||
|
||||
async execute(dto: RejectSponsorshipRequestDTO): Promise<Result<RejectSponsorshipRequestResultDTO, ApplicationErrorCode<'SPONSORSHIP_REQUEST_NOT_FOUND' | 'SPONSORSHIP_REQUEST_NOT_PENDING'>>> {
|
||||
@@ -41,7 +43,18 @@ export class RejectSponsorshipRequestUseCase {
|
||||
const rejectedRequest = request.reject(dto.respondedBy, dto.reason);
|
||||
await this.sponsorshipRequestRepo.update(rejectedRequest);
|
||||
|
||||
// TODO: In a real implementation, notify the sponsor
|
||||
await this.sponsorshipRejectionNotificationPort.notifySponsorshipRequestRejected({
|
||||
requestId: request.id,
|
||||
sponsorId: request.sponsorId,
|
||||
entityType: request.entityType,
|
||||
entityId: request.entityId,
|
||||
tier: request.tier,
|
||||
offeredAmountCents: request.offeredAmount.amount,
|
||||
currency: request.offeredAmount.currency,
|
||||
rejectedAt: rejectedRequest.respondedAt!,
|
||||
rejectedBy: dto.respondedBy,
|
||||
rejectionReason: rejectedRequest.rejectionReason,
|
||||
});
|
||||
|
||||
return Result.ok({
|
||||
requestId: rejectedRequest.id,
|
||||
|
||||
Reference in New Issue
Block a user