refactor
This commit is contained in:
@@ -89,6 +89,18 @@ export class EntityMappers {
|
||||
|
||||
static toRaceDTO(race: Race | null): RaceDTO | null {
|
||||
if (!race) return null;
|
||||
|
||||
const sessionTypeMap = {
|
||||
practice: 'practice' as const,
|
||||
qualifying: 'qualifying' as const,
|
||||
q1: 'qualifying' as const,
|
||||
q2: 'qualifying' as const,
|
||||
q3: 'qualifying' as const,
|
||||
sprint: 'race' as const,
|
||||
main: 'race' as const,
|
||||
timeTrial: 'practice' as const,
|
||||
};
|
||||
|
||||
return {
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
@@ -97,7 +109,7 @@ export class EntityMappers {
|
||||
trackId: race.trackId ?? '',
|
||||
car: race.car,
|
||||
carId: race.carId ?? '',
|
||||
sessionType: race.sessionType,
|
||||
sessionType: sessionTypeMap[race.sessionType.value],
|
||||
status: race.status,
|
||||
...(race.strengthOfField !== undefined
|
||||
? { strengthOfField: race.strengthOfField }
|
||||
@@ -112,6 +124,17 @@ export class EntityMappers {
|
||||
}
|
||||
|
||||
static toRaceDTOs(races: Race[]): RaceDTO[] {
|
||||
const sessionTypeMap = {
|
||||
practice: 'practice' as const,
|
||||
qualifying: 'qualifying' as const,
|
||||
q1: 'qualifying' as const,
|
||||
q2: 'qualifying' as const,
|
||||
q3: 'qualifying' as const,
|
||||
sprint: 'race' as const,
|
||||
main: 'race' as const,
|
||||
timeTrial: 'practice' as const,
|
||||
};
|
||||
|
||||
return races.map((race) => ({
|
||||
id: race.id,
|
||||
leagueId: race.leagueId,
|
||||
@@ -120,7 +143,7 @@ export class EntityMappers {
|
||||
trackId: race.trackId ?? '',
|
||||
car: race.car,
|
||||
carId: race.carId ?? '',
|
||||
sessionType: race.sessionType,
|
||||
sessionType: sessionTypeMap[race.sessionType.value],
|
||||
status: race.status,
|
||||
...(race.strengthOfField !== undefined
|
||||
? { strengthOfField: race.strengthOfField }
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { AcceptSponsorshipRequestUseCase } from './AcceptSponsorshipRequestUseCase';
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { INotificationService } from '@core/notifications/application/ports/INotificationService';
|
||||
import type { IPaymentGateway } from '../ports/IPaymentGateway';
|
||||
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
|
||||
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { SponsorshipRequest } from '../../domain/entities/SponsorshipRequest';
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import { LeagueWallet } from '../../domain/entities/LeagueWallet';
|
||||
import { Money } from '../../domain/value-objects/Money';
|
||||
|
||||
describe('AcceptSponsorshipRequestUseCase', () => {
|
||||
let mockSponsorshipRequestRepo: {
|
||||
findById: Mock;
|
||||
update: Mock;
|
||||
};
|
||||
let mockSeasonSponsorshipRepo: {
|
||||
create: Mock;
|
||||
};
|
||||
let mockSeasonRepo: {
|
||||
findById: Mock;
|
||||
};
|
||||
let mockNotificationService: {
|
||||
sendNotification: Mock;
|
||||
};
|
||||
let mockPaymentGateway: {
|
||||
processPayment: Mock;
|
||||
};
|
||||
let mockWalletRepo: {
|
||||
findById: Mock;
|
||||
update: Mock;
|
||||
};
|
||||
let mockLeagueWalletRepo: {
|
||||
findById: Mock;
|
||||
update: Mock;
|
||||
};
|
||||
let mockLogger: {
|
||||
debug: Mock;
|
||||
info: Mock;
|
||||
warn: Mock;
|
||||
error: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockSponsorshipRequestRepo = {
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
};
|
||||
mockSeasonSponsorshipRepo = {
|
||||
create: vi.fn(),
|
||||
};
|
||||
mockSeasonRepo = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
mockNotificationService = {
|
||||
sendNotification: vi.fn(),
|
||||
};
|
||||
mockPaymentGateway = {
|
||||
processPayment: vi.fn(),
|
||||
};
|
||||
mockWalletRepo = {
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
};
|
||||
mockLeagueWalletRepo = {
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
};
|
||||
mockLogger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should send notification to sponsor, process payment, and update wallets when accepting season sponsorship', async () => {
|
||||
const useCase = new AcceptSponsorshipRequestUseCase(
|
||||
mockSponsorshipRequestRepo as unknown as ISponsorshipRequestRepository,
|
||||
mockSeasonSponsorshipRepo as unknown as ISeasonSponsorshipRepository,
|
||||
mockSeasonRepo as unknown as ISeasonRepository,
|
||||
mockNotificationService as unknown as INotificationService,
|
||||
mockPaymentGateway as unknown as IPaymentGateway,
|
||||
mockWalletRepo as unknown as IWalletRepository,
|
||||
mockLeagueWalletRepo as unknown as ILeagueWalletRepository,
|
||||
mockLogger as unknown as Logger,
|
||||
);
|
||||
|
||||
const request = SponsorshipRequest.create({
|
||||
id: 'req1',
|
||||
sponsorId: 'sponsor1',
|
||||
entityId: 'season1',
|
||||
entityType: 'season',
|
||||
tier: 'main',
|
||||
offeredAmount: Money.create(1000),
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
const season = Season.create({
|
||||
id: 'season1',
|
||||
leagueId: 'league1',
|
||||
gameId: 'game1',
|
||||
name: 'Season 1',
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
});
|
||||
|
||||
mockSponsorshipRequestRepo.findById.mockResolvedValue(request);
|
||||
mockSeasonRepo.findById.mockResolvedValue(season);
|
||||
mockNotificationService.sendNotification.mockResolvedValue(undefined);
|
||||
mockPaymentGateway.processPayment.mockResolvedValue({
|
||||
success: true,
|
||||
transactionId: 'txn1',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
mockWalletRepo.findById.mockResolvedValue({
|
||||
id: 'sponsor1',
|
||||
leagueId: 'league1',
|
||||
balance: 2000,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
currency: 'USD',
|
||||
createdAt: new Date(),
|
||||
});
|
||||
const leagueWallet = LeagueWallet.create({
|
||||
id: 'league1',
|
||||
leagueId: 'league1',
|
||||
balance: Money.create(500),
|
||||
});
|
||||
mockLeagueWalletRepo.findById.mockResolvedValue(leagueWallet);
|
||||
|
||||
const result = await useCase.execute({
|
||||
requestId: 'req1',
|
||||
respondedBy: 'driver1',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(mockNotificationService.sendNotification).toHaveBeenCalledWith({
|
||||
recipientId: 'sponsor1',
|
||||
type: 'sponsorship_request_accepted',
|
||||
title: 'Sponsorship Accepted',
|
||||
body: 'Your sponsorship request for Season 1 has been accepted.',
|
||||
channel: 'in_app',
|
||||
urgency: 'toast',
|
||||
data: {
|
||||
requestId: 'req1',
|
||||
sponsorshipId: expect.any(String),
|
||||
},
|
||||
});
|
||||
expect(mockPaymentGateway.processPayment).toHaveBeenCalledWith(
|
||||
Money.create(1000),
|
||||
'sponsor1',
|
||||
'Sponsorship payment for season season1',
|
||||
{ requestId: 'req1' }
|
||||
);
|
||||
expect(mockWalletRepo.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'sponsor1',
|
||||
balance: 1000,
|
||||
})
|
||||
);
|
||||
expect(mockLeagueWalletRepo.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'league1',
|
||||
balance: expect.objectContaining({ amount: 1400 }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,10 @@ import type { Logger } from '@core/shared/application';
|
||||
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { INotificationService } from '@core/notifications/application/ports/INotificationService';
|
||||
import type { IPaymentGateway } from '../ports/IPaymentGateway';
|
||||
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
|
||||
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
||||
import { SeasonSponsorship } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
|
||||
@@ -32,6 +36,10 @@ export class AcceptSponsorshipRequestUseCase
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
private readonly notificationService: INotificationService,
|
||||
private readonly paymentGateway: IPaymentGateway,
|
||||
private readonly walletRepository: IWalletRepository,
|
||||
private readonly leagueWalletRepository: ILeagueWalletRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -79,12 +87,59 @@ export class AcceptSponsorshipRequestUseCase
|
||||
});
|
||||
await this.seasonSponsorshipRepo.create(sponsorship);
|
||||
this.logger.info(`Season sponsorship ${sponsorshipId} created for request ${dto.requestId}.`, { sponsorshipId, requestId: dto.requestId });
|
||||
}
|
||||
|
||||
// TODO: In a real implementation, we would:
|
||||
// 1. Create notification for the sponsor
|
||||
// 2. Process payment
|
||||
// 3. Update wallet balances
|
||||
// Notify the sponsor
|
||||
await this.notificationService.sendNotification({
|
||||
recipientId: request.sponsorId,
|
||||
type: 'sponsorship_request_accepted',
|
||||
title: 'Sponsorship Accepted',
|
||||
body: `Your sponsorship request for ${season.name} has been accepted.`,
|
||||
channel: 'in_app',
|
||||
urgency: 'toast',
|
||||
data: {
|
||||
requestId: request.id,
|
||||
sponsorshipId,
|
||||
},
|
||||
});
|
||||
|
||||
// Process payment
|
||||
const paymentResult = await this.paymentGateway.processPayment(
|
||||
request.offeredAmount,
|
||||
request.sponsorId,
|
||||
`Sponsorship payment for ${request.entityType} ${request.entityId}`,
|
||||
{ requestId: request.id }
|
||||
);
|
||||
if (!paymentResult.success) {
|
||||
this.logger.error(`Payment failed for sponsorship request ${request.id}: ${paymentResult.error}`, undefined, { requestId: request.id });
|
||||
throw new Error('Payment processing failed');
|
||||
}
|
||||
|
||||
// Update wallets
|
||||
const sponsorWallet = await this.walletRepository.findById(request.sponsorId);
|
||||
if (!sponsorWallet) {
|
||||
this.logger.error(`Sponsor wallet not found for ${request.sponsorId}`, undefined, { sponsorId: request.sponsorId });
|
||||
throw new Error('Sponsor wallet not found');
|
||||
}
|
||||
|
||||
const leagueWallet = await this.leagueWalletRepository.findById(season.leagueId);
|
||||
if (!leagueWallet) {
|
||||
this.logger.error(`League wallet not found for ${season.leagueId}`, undefined, { leagueId: season.leagueId });
|
||||
throw new Error('League wallet not found');
|
||||
}
|
||||
|
||||
const netAmount = acceptedRequest.getNetAmount();
|
||||
|
||||
// Deduct from sponsor wallet
|
||||
const updatedSponsorWallet = {
|
||||
...sponsorWallet,
|
||||
balance: sponsorWallet.balance - request.offeredAmount.amount,
|
||||
};
|
||||
await this.walletRepository.update(updatedSponsorWallet);
|
||||
|
||||
// Add to league wallet
|
||||
const updatedLeagueWallet = leagueWallet.addFunds(netAmount, paymentResult.transactionId!);
|
||||
await this.leagueWalletRepository.update(updatedLeagueWallet);
|
||||
}
|
||||
|
||||
this.logger.info(`Sponsorship request ${acceptedRequest.id} successfully accepted.`, { requestId: acceptedRequest.id, sponsorshipId });
|
||||
|
||||
@@ -97,8 +152,9 @@ export class AcceptSponsorshipRequestUseCase
|
||||
netAmount: acceptedRequest.getNetAmount().amount,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to accept sponsorship request ${dto.requestId}: ${error.message}`, { requestId: dto.requestId, error: error.message, stack: error.stack });
|
||||
throw error;
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error(`Failed to accept sponsorship request ${dto.requestId}: ${err.message}`, err, { requestId: dto.requestId });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class ApplyForSponsorshipUseCase
|
||||
// Validate sponsor exists
|
||||
const sponsor = await this.sponsorRepo.findById(dto.sponsorId);
|
||||
if (!sponsor) {
|
||||
this.logger.error('Sponsor not found', { sponsorId: dto.sponsorId });
|
||||
this.logger.error('Sponsor not found', undefined, { sponsorId: dto.sponsorId });
|
||||
throw new EntityNotFoundError({ entity: 'sponsor', id: dto.sponsorId });
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export class ApplyPenaltyUseCase
|
||||
|
||||
return { penaltyId: penalty.id };
|
||||
} catch (error) {
|
||||
this.logger.error('ApplyPenaltyUseCase: Failed to apply penalty', { command, error: error.message });
|
||||
this.logger.error('ApplyPenaltyUseCase: Failed to apply penalty', error, { command });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class ApproveTeamJoinRequestUseCase
|
||||
await this.membershipRepository.removeJoinRequest(requestId);
|
||||
this.logger.info(`Team join request with ID ${requestId} removed`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to approve team join request ${requestId}:`, error);
|
||||
this.logger.error(`Failed to approve team join request ${requestId}`, error instanceof Error ? error : new Error(String(error)));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class CancelRaceUseCase
|
||||
await this.raceRepository.update(cancelledRace);
|
||||
this.logger.info(`[CancelRaceUseCase] Race ${raceId} cancelled successfully.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`[CancelRaceUseCase] Error cancelling race ${raceId}:`, error);
|
||||
this.logger.error(`[CancelRaceUseCase] Error cancelling race ${raceId}`, error instanceof Error ? error : new Error(String(error)));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
|
||||
import type { IDomainEventPublisher } from '@core/shared/domain';
|
||||
import type { RaceEventStewardingClosedEvent } from '../../domain/events/RaceEventStewardingClosed';
|
||||
@@ -20,6 +21,8 @@ export class CloseRaceEventStewardingUseCase
|
||||
implements UseCase<CloseRaceEventStewardingCommand, void, void, void>
|
||||
{
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
|
||||
private readonly raceEventRepository: IRaceEventRepository,
|
||||
private readonly domainEventPublisher: IDomainEventPublisher,
|
||||
) {}
|
||||
@@ -58,7 +61,7 @@ export class CloseRaceEventStewardingUseCase
|
||||
await this.domainEventPublisher.publish(event);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to close stewarding for race event ${raceEvent.id}:`, error);
|
||||
this.logger.error(`Failed to close stewarding for race event ${raceEvent.id}`, error instanceof Error ? error : new Error(String(error)));
|
||||
// In production, this would trigger alerts/monitoring
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ export class CompleteRaceUseCaseWithRatings
|
||||
const completedRace = race.complete();
|
||||
await this.raceRepository.update(completedRace);
|
||||
this.logger.info(`Race ID: ${raceId} completed successfully.`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error completing race ${raceId}: ${error.message}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error completing race ${raceId}`, error instanceof Error ? error : new Error(String(error)));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Season } from '../../domain/entities/Season';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { LeagueScoringConfig } from '../../domain/entities/LeagueScoringConfig';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type {
|
||||
@@ -129,11 +128,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
|
||||
this.logger.debug('CreateLeagueWithSeasonAndScoringUseCase completed successfully.', { result });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', {
|
||||
command,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
this.logger.error('Error during CreateLeagueWithSeasonAndScoringUseCase execution.', error, { command });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
AllTeamsResultDTO,
|
||||
} from '../presenters/IAllTeamsPresenter';
|
||||
import type { UseCase } from '@core/shared/application';
|
||||
import type { Team } from '../../domain/entities/Team';
|
||||
import { Logger } from "@core/shared/application";
|
||||
|
||||
/**
|
||||
@@ -54,7 +53,7 @@ export class GetAllTeamsUseCase
|
||||
presenter.present(dto);
|
||||
this.logger.info('Successfully retrieved all teams.');
|
||||
} catch (error) {
|
||||
this.logger.error('Error retrieving all teams:', error);
|
||||
this.logger.error('Error retrieving all teams', error instanceof Error ? error : new Error(String(error)));
|
||||
throw error; // Re-throw the error after logging
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsResultDTO, GetLeagueAdminPermissionsViewModel } from '../presenters/IGetLeagueAdminPermissionsPresenter';
|
||||
import type { IGetLeagueAdminPermissionsPresenter, GetLeagueAdminPermissionsViewModel } from '../presenters/IGetLeagueAdminPermissionsPresenter';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
|
||||
export interface GetLeagueAdminPermissionsUseCaseParams {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { IGetLeagueAdminPresenter, GetLeagueAdminResultDTO, GetLeagueAdminViewModel } from '../presenters/IGetLeagueAdminPresenter';
|
||||
import { GetLeagueAdminPermissionsViewModel } from '../presenters/IGetLeagueAdminPermissionsPresenter';
|
||||
import type { IGetLeagueAdminPresenter } from '../presenters/IGetLeagueAdminPresenter';
|
||||
import type { UseCase } from '@core/shared/application/UseCase';
|
||||
|
||||
export interface GetLeagueAdminUseCaseParams {
|
||||
@@ -14,7 +15,7 @@ export interface GetLeagueAdminResultDTO {
|
||||
// Additional data would be populated by combining multiple use cases
|
||||
}
|
||||
|
||||
export class GetLeagueAdminUseCase implements UseCase<GetLeagueAdminUseCaseParams, GetLeagueAdminResultDTO, GetLeagueAdminViewModel, IGetLeagueAdminPresenter> {
|
||||
export class GetLeagueAdminUseCase implements UseCase<GetLeagueAdminUseCaseParams, GetLeagueAdminResultDTO, GetLeagueAdminPermissionsViewModel, IGetLeagueAdminPresenter> {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
) {}
|
||||
|
||||
Reference in New Issue
Block a user