This commit is contained in:
2025-12-16 15:42:38 +01:00
parent 29410708c8
commit 362894d1a5
147 changed files with 780 additions and 375 deletions

View File

@@ -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 }

View File

@@ -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 }),
})
);
});
});

View File

@@ -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;
}
}
}

View File

@@ -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 });
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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,
) {}