refactor dtos to ports

This commit is contained in:
2025-12-19 14:08:27 +01:00
parent 2ab86ec9bd
commit 499562c456
106 changed files with 386 additions and 1009 deletions

View File

@@ -10,7 +10,6 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
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';
@@ -18,22 +17,24 @@ import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { AcceptSponsorshipRequestDTO } from '../dto/AcceptSponsorshipRequestDTO';
import type { AcceptSponsorshipRequestResultDTO } from '../dto/AcceptSponsorshipRequestResultDTO';
import type { AcceptSponsorshipOutputPort } from '../ports/output/AcceptSponsorshipOutputPort';
import type { ProcessPaymentInputPort } from '../ports/input/ProcessPaymentInputPort';
import type { ProcessPaymentOutputPort } from '../ports/output/ProcessPaymentOutputPort';
export class AcceptSponsorshipRequestUseCase
implements AsyncUseCase<AcceptSponsorshipRequestDTO, AcceptSponsorshipRequestResultDTO, string> {
implements AsyncUseCase<AcceptSponsorshipRequestDTO, AcceptSponsorshipOutputPort, string> {
constructor(
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
private readonly seasonRepository: ISeasonRepository,
private readonly notificationService: INotificationService,
private readonly paymentGateway: IPaymentGateway,
private readonly paymentProcessor: (input: ProcessPaymentInputPort) => Promise<ProcessPaymentOutputPort>,
private readonly walletRepository: IWalletRepository,
private readonly leagueWalletRepository: ILeagueWalletRepository,
private readonly logger: Logger,
) {}
async execute(dto: AcceptSponsorshipRequestDTO): Promise<Result<AcceptSponsorshipRequestResultDTO, ApplicationErrorCode<string>>> {
async execute(dto: AcceptSponsorshipRequestDTO): Promise<Result<AcceptSponsorshipOutputPort, ApplicationErrorCode<string>>> {
this.logger.debug(`Attempting to accept sponsorship request: ${dto.requestId}`, { requestId: dto.requestId, respondedBy: dto.respondedBy });
// Find the request
@@ -92,13 +93,15 @@ export class AcceptSponsorshipRequestUseCase
},
});
// Process payment
const paymentResult = await this.paymentGateway.processPayment(
request.offeredAmount,
request.sponsorId,
`Sponsorship payment for ${request.entityType} ${request.entityId}`,
{ requestId: request.id }
);
// Process payment using clean input/output ports with primitive types
const paymentInput: ProcessPaymentInputPort = {
amount: request.offeredAmount.amount, // Extract primitive number from value object
payerId: request.sponsorId,
description: `Sponsorship payment for ${request.entityType} ${request.entityId}`,
metadata: { requestId: request.id }
};
const paymentResult = await this.paymentProcessor(paymentInput);
if (!paymentResult.success) {
this.logger.error(`Payment failed for sponsorship request ${request.id}: ${paymentResult.error}`, undefined, { requestId: request.id });
return Result.err({ code: 'PAYMENT_PROCESSING_FAILED' });
@@ -142,4 +145,4 @@ export class AcceptSponsorshipRequestUseCase
netAmount: acceptedRequest.getNetAmount().amount,
});
}
}
}

View File

@@ -14,11 +14,11 @@ import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { ApplyForSponsorshipDTO } from '../dto/ApplyForSponsorshipDTO';
import type { ApplyForSponsorshipResultDTO } from '../dto/ApplyForSponsorshipResultDTO';
import type { ApplyForSponsorshipPort } from '../ports/input/ApplyForSponsorshipPort';
import type { ApplyForSponsorshipResultPort } from '../ports/output/ApplyForSponsorshipResultPort';
export class ApplyForSponsorshipUseCase
implements AsyncUseCase<ApplyForSponsorshipDTO, ApplyForSponsorshipResultDTO, string>
implements AsyncUseCase<ApplyForSponsorshipPort, ApplyForSponsorshipResultPort, string>
{
constructor(
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
@@ -27,7 +27,7 @@ export class ApplyForSponsorshipUseCase
private readonly logger: Logger,
) {}
async execute(dto: ApplyForSponsorshipDTO): Promise<Result<ApplyForSponsorshipResultDTO, ApplicationErrorCode<string>>> {
async execute(dto: ApplyForSponsorshipPort): Promise<Result<ApplyForSponsorshipResultPort, ApplicationErrorCode<string>>> {
this.logger.debug('Attempting to apply for sponsorship', { dto });
// Validate sponsor exists

View File

@@ -15,10 +15,10 @@ import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { ApplyPenaltyCommand } from '../dto/ApplyPenaltyCommand';
import type { ApplyPenaltyCommandPort } from '../ports/input/ApplyPenaltyCommandPort';
export class ApplyPenaltyUseCase
implements AsyncUseCase<ApplyPenaltyCommand, { penaltyId: string }, string> {
implements AsyncUseCase<ApplyPenaltyCommandPort, { penaltyId: string }, string> {
constructor(
private readonly penaltyRepository: IPenaltyRepository,
private readonly protestRepository: IProtestRepository,
@@ -27,7 +27,7 @@ export class ApplyPenaltyUseCase
private readonly logger: Logger,
) {}
async execute(command: ApplyPenaltyCommand): Promise<Result<{ penaltyId: string }, ApplicationErrorCode<string>>> {
async execute(command: ApplyPenaltyCommandPort): Promise<Result<{ penaltyId: string }, ApplicationErrorCode<string>>> {
this.logger.debug('ApplyPenaltyUseCase: Executing with command', command);
// Validate race exists

View File

@@ -4,13 +4,13 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
import type { AsyncUseCase } from '@core/shared/application';
import { randomUUID } from 'crypto';
import type { ApproveLeagueJoinRequestUseCaseParams } from '../dto/ApproveLeagueJoinRequestUseCaseParams';
import type { ApproveLeagueJoinRequestResultDTO } from '../dto/ApproveLeagueJoinRequestResultDTO';
import type { ApproveLeagueJoinRequestResultPort } from '../ports/output/ApproveLeagueJoinRequestResultPort';
import { JoinedAt } from '../../domain/value-objects/JoinedAt';
export class ApproveLeagueJoinRequestUseCase implements AsyncUseCase<ApproveLeagueJoinRequestUseCaseParams, ApproveLeagueJoinRequestResultDTO, string> {
export class ApproveLeagueJoinRequestUseCase implements AsyncUseCase<ApproveLeagueJoinRequestUseCaseParams, ApproveLeagueJoinRequestResultPort, string> {
constructor(private readonly leagueMembershipRepository: ILeagueMembershipRepository) {}
async execute(params: ApproveLeagueJoinRequestUseCaseParams): Promise<Result<ApproveLeagueJoinRequestResultDTO, ApplicationErrorCode<string>>> {
async execute(params: ApproveLeagueJoinRequestUseCaseParams): Promise<Result<ApproveLeagueJoinRequestResultPort, ApplicationErrorCode<string>>> {
const requests = await this.leagueMembershipRepository.getJoinRequests(params.leagueId);
const request = requests.find(r => r.id === params.requestId);
if (!request) {
@@ -25,7 +25,7 @@ export class ApproveLeagueJoinRequestUseCase implements AsyncUseCase<ApproveLeag
status: 'active',
joinedAt: JoinedAt.create(new Date()),
});
const dto: ApproveLeagueJoinRequestResultDTO = { success: true, message: 'Join request approved.' };
const dto: ApproveLeagueJoinRequestResultPort = { success: true, message: 'Join request approved.' };
return Result.ok(dto);
}
}

View File

@@ -17,7 +17,7 @@ import {
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { LeagueVisibilityInput } from '../dto/LeagueVisibilityInput';
import type { CreateLeagueWithSeasonAndScoringResultDTO } from '../dto/CreateLeagueWithSeasonAndScoringResultDTO';
import type { CreateLeagueWithSeasonAndScoringOutputPort } from '../ports/output/CreateLeagueWithSeasonAndScoringOutputPort';
export interface CreateLeagueWithSeasonAndScoringCommand {
name: string;
@@ -40,7 +40,7 @@ export interface CreateLeagueWithSeasonAndScoringCommand {
}
export class CreateLeagueWithSeasonAndScoringUseCase
implements AsyncUseCase<CreateLeagueWithSeasonAndScoringCommand, CreateLeagueWithSeasonAndScoringResultDTO, 'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR'> {
implements AsyncUseCase<CreateLeagueWithSeasonAndScoringCommand, CreateLeagueWithSeasonAndScoringOutputPort, 'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR'> {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly seasonRepository: ISeasonRepository,
@@ -51,7 +51,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
async execute(
command: CreateLeagueWithSeasonAndScoringCommand,
): Promise<Result<CreateLeagueWithSeasonAndScoringResultDTO, ApplicationErrorCode<'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR', { message: string }>>> {
): Promise<Result<CreateLeagueWithSeasonAndScoringOutputPort, ApplicationErrorCode<'VALIDATION_ERROR' | 'UNKNOWN_PRESET' | 'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug('Executing CreateLeagueWithSeasonAndScoringUseCase', { command });
const validation = this.validate(command);
if (validation.isErr()) {
@@ -112,7 +112,7 @@ export class CreateLeagueWithSeasonAndScoringUseCase
await this.leagueScoringConfigRepository.save(finalConfig);
this.logger.info(`Scoring configuration saved for season ${seasonId}.`);
const result: CreateLeagueWithSeasonAndScoringResultDTO = {
const result: CreateLeagueWithSeasonAndScoringOutputPort = {
leagueId: league.id.toString(),
seasonId,
scoringPresetId: preset.id,

View File

@@ -10,7 +10,7 @@ import type { AsyncUseCase } from '@core/shared/application';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { CreateSponsorResultDTO } from '../dto/CreateSponsorResultDTO';
import type { CreateSponsorOutputPort } from '../ports/output/CreateSponsorOutputPort';
export interface CreateSponsorCommand {
name: string;
@@ -20,7 +20,7 @@ export interface CreateSponsorCommand {
}
export class CreateSponsorUseCase
implements AsyncUseCase<CreateSponsorCommand, CreateSponsorResultDTO, 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'>
implements AsyncUseCase<CreateSponsorCommand, CreateSponsorOutputPort, 'VALIDATION_ERROR' | 'REPOSITORY_ERROR'>
{
constructor(
private readonly sponsorRepository: ISponsorRepository,
@@ -29,7 +29,7 @@ export class CreateSponsorUseCase
async execute(
command: CreateSponsorCommand,
): Promise<Result<CreateSponsorResultDTO, ApplicationErrorCode<'VALIDATION_ERROR' | 'REPOSITORY_ERROR', { message: string }>>> {
): Promise<Result<CreateSponsorOutputPort, ApplicationErrorCode<'VALIDATION_ERROR' | 'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug('Executing CreateSponsorUseCase', { command });
const validation = this.validate(command);
if (validation.isErr()) {
@@ -51,7 +51,7 @@ export class CreateSponsorUseCase
await this.sponsorRepository.create(sponsor);
this.logger.info(`Sponsor ${sponsor.name} (${sponsor.id}) created successfully.`);
const result: CreateSponsorResultDTO = {
const result: CreateSponsorOutputPort = {
sponsor: {
id: sponsor.id,
name: sponsor.name,

View File

@@ -16,6 +16,7 @@ import type { AsyncUseCase } from '@core/shared/application';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { CreateTeamOutputPort } from '../ports/output/CreateTeamOutputPort';
export interface CreateTeamCommandDTO {
name: string;
@@ -25,12 +26,8 @@ export interface CreateTeamCommandDTO {
leagues: string[];
}
export interface CreateTeamResultDTO {
team: Team;
}
export class CreateTeamUseCase
implements AsyncUseCase<CreateTeamCommandDTO, CreateTeamResultDTO, 'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR'>
implements AsyncUseCase<CreateTeamCommandDTO, CreateTeamOutputPort, 'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR'>
{
constructor(
private readonly teamRepository: ITeamRepository,
@@ -40,7 +37,7 @@ export class CreateTeamUseCase
async execute(
command: CreateTeamCommandDTO,
): Promise<Result<CreateTeamResultDTO, ApplicationErrorCode<'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR', { message: string }>>> {
): Promise<Result<CreateTeamOutputPort, ApplicationErrorCode<'ALREADY_IN_TEAM' | 'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug('Executing CreateTeamUseCase', { command });
const { name, tag, description, ownerId, leagues } = command;
@@ -80,7 +77,7 @@ export class CreateTeamUseCase
await this.membershipRepository.saveMembership(membership);
this.logger.debug('Team membership created successfully.');
const result: CreateTeamResultDTO = { team: createdTeam };
const result: CreateTeamOutputPort = { team: createdTeam };
this.logger.debug('CreateTeamUseCase completed successfully.', { result });
return Result.ok(result);
} catch (error) {

View File

@@ -11,11 +11,11 @@ import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISe
import type { AsyncUseCase, Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetEntitySponsorshipPricingDTO } from '../dto/GetEntitySponsorshipPricingDTO';
import type { GetEntitySponsorshipPricingResultDTO } from '../dto/GetEntitySponsorshipPricingResultDTO';
import type { GetEntitySponsorshipPricingInputPort } from '../ports/input/GetEntitySponsorshipPricingInputPort';
import type { GetEntitySponsorshipPricingOutputPort } from '../ports/output/GetEntitySponsorshipPricingOutputPort';
export class GetEntitySponsorshipPricingUseCase
implements AsyncUseCase<GetEntitySponsorshipPricingDTO, GetEntitySponsorshipPricingResultDTO | null, 'REPOSITORY_ERROR'>
implements AsyncUseCase<GetEntitySponsorshipPricingInputPort, GetEntitySponsorshipPricingOutputPort | null, 'REPOSITORY_ERROR'>
{
constructor(
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
@@ -24,7 +24,7 @@ export class GetEntitySponsorshipPricingUseCase
private readonly logger: Logger,
) {}
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<Result<GetEntitySponsorshipPricingResultDTO | null, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
async execute(dto: GetEntitySponsorshipPricingInputPort): Promise<Result<GetEntitySponsorshipPricingOutputPort | null, ApplicationErrorCode<'REPOSITORY_ERROR', { message: string }>>> {
this.logger.debug(`Executing GetEntitySponsorshipPricingUseCase for entityType: ${dto.entityType}, entityId: ${dto.entityId}`);
try {
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
@@ -53,7 +53,7 @@ export class GetEntitySponsorshipPricingUseCase
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length;
}
const result: GetEntitySponsorshipPricingResultDTO = {
const result: GetEntitySponsorshipPricingOutputPort = {
entityType: dto.entityType,
entityId: dto.entityId,
acceptingApplications: pricing.acceptingApplications,

View File

@@ -2,15 +2,15 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { GetLeagueAdminPermissionsResultDTO } from '../dto/GetLeagueAdminPermissionsResultDTO';
import type { GetLeagueAdminPermissionsOutputPort } from '../ports/output/GetLeagueAdminPermissionsOutputPort';
export class GetLeagueAdminPermissionsUseCase implements AsyncUseCase<{ leagueId: string; performerDriverId: string }, GetLeagueAdminPermissionsResultDTO, 'NO_ERROR'> {
export class GetLeagueAdminPermissionsUseCase implements AsyncUseCase<{ leagueId: string; performerDriverId: string }, GetLeagueAdminPermissionsOutputPort, 'NO_ERROR'> {
constructor(
private readonly leagueRepository: ILeagueRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
) {}
async execute(params: { leagueId: string; performerDriverId: string }): Promise<Result<GetLeagueAdminPermissionsResultDTO, never>> {
async execute(params: { leagueId: string; performerDriverId: string }): Promise<Result<GetLeagueAdminPermissionsOutputPort, never>> {
const league = await this.leagueRepository.findById(params.leagueId);
if (!league) {
return Result.ok({ canRemoveMember: false, canUpdateRoles: false });

View File

@@ -2,20 +2,20 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueAdminResultDTO } from '../dto/GetLeagueAdminResultDTO';
import type { GetLeagueAdminOutputPort } from '../ports/output/GetLeagueAdminOutputPort';
export class GetLeagueAdminUseCase implements AsyncUseCase<{ leagueId: string }, GetLeagueAdminResultDTO, 'LEAGUE_NOT_FOUND'> {
export class GetLeagueAdminUseCase implements AsyncUseCase<{ leagueId: string }, GetLeagueAdminOutputPort, 'LEAGUE_NOT_FOUND'> {
constructor(
private readonly leagueRepository: ILeagueRepository,
) {}
async execute(params: { leagueId: string }): Promise<Result<GetLeagueAdminResultDTO, ApplicationErrorCode<'LEAGUE_NOT_FOUND', { message: string }>>> {
async execute(params: { leagueId: string }): Promise<Result<GetLeagueAdminOutputPort, ApplicationErrorCode<'LEAGUE_NOT_FOUND', { message: string }>>> {
const league = await this.leagueRepository.findById(params.leagueId);
if (!league) {
return Result.err({ code: 'LEAGUE_NOT_FOUND', details: { message: 'League not found' } });
}
const dto: GetLeagueAdminResultDTO = {
const dto: GetLeagueAdminOutputPort = {
league: {
id: league.id,
ownerId: league.ownerId,

View File

@@ -3,19 +3,19 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueJoinRequestsResultDTO } from '../dto/GetLeagueJoinRequestsResultDTO';
import type { GetLeagueJoinRequestsOutputPort } from '../ports/output/GetLeagueJoinRequestsOutputPort';
export interface GetLeagueJoinRequestsUseCaseParams {
leagueId: string;
}
export class GetLeagueJoinRequestsUseCase implements AsyncUseCase<GetLeagueJoinRequestsUseCaseParams, GetLeagueJoinRequestsResultDTO, 'NO_ERROR'> {
export class GetLeagueJoinRequestsUseCase implements AsyncUseCase<GetLeagueJoinRequestsUseCaseParams, GetLeagueJoinRequestsOutputPort, 'NO_ERROR'> {
constructor(
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(params: GetLeagueJoinRequestsUseCaseParams): Promise<Result<GetLeagueJoinRequestsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
async execute(params: GetLeagueJoinRequestsUseCaseParams): Promise<Result<GetLeagueJoinRequestsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
const joinRequests = await this.leagueMembershipRepository.getJoinRequests(params.leagueId);
const driverIds = [...new Set(joinRequests.map(r => r.driverId))];
const drivers = await Promise.all(driverIds.map(id => this.driverRepository.findById(id)));

View File

@@ -3,19 +3,19 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueMembershipsResultDTO } from '../dto/GetLeagueMembershipsResultDTO';
import type { GetLeagueMembershipsOutputPort } from '../ports/output/GetLeagueMembershipsOutputPort';
export interface GetLeagueMembershipsUseCaseParams {
leagueId: string;
}
export class GetLeagueMembershipsUseCase implements AsyncUseCase<GetLeagueMembershipsUseCaseParams, GetLeagueMembershipsResultDTO, 'NO_ERROR'> {
export class GetLeagueMembershipsUseCase implements AsyncUseCase<GetLeagueMembershipsUseCaseParams, GetLeagueMembershipsOutputPort, 'NO_ERROR'> {
constructor(
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRepository: IDriverRepository,
) {}
async execute(params: GetLeagueMembershipsUseCaseParams): Promise<Result<GetLeagueMembershipsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
async execute(params: GetLeagueMembershipsUseCaseParams): Promise<Result<GetLeagueMembershipsOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
const memberships = await this.leagueMembershipRepository.getLeagueMembers(params.leagueId);
const drivers: { id: string; name: string }[] = [];
@@ -27,7 +27,7 @@ export class GetLeagueMembershipsUseCase implements AsyncUseCase<GetLeagueMember
}
}
const dto: GetLeagueMembershipsResultDTO = {
const dto: GetLeagueMembershipsOutputPort = {
memberships,
drivers,
};

View File

@@ -2,16 +2,16 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { AsyncUseCase } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { GetLeagueOwnerSummaryResultDTO } from '../dto/GetLeagueOwnerSummaryResultDTO';
import type { GetLeagueOwnerSummaryOutputPort } from '../ports/output/GetLeagueOwnerSummaryOutputPort';
export interface GetLeagueOwnerSummaryUseCaseParams {
ownerId: string;
}
export class GetLeagueOwnerSummaryUseCase implements AsyncUseCase<GetLeagueOwnerSummaryUseCaseParams, GetLeagueOwnerSummaryResultDTO, 'NO_ERROR'> {
export class GetLeagueOwnerSummaryUseCase implements AsyncUseCase<GetLeagueOwnerSummaryUseCaseParams, GetLeagueOwnerSummaryOutputPort, 'NO_ERROR'> {
constructor(private readonly driverRepository: IDriverRepository) {}
async execute(params: GetLeagueOwnerSummaryUseCaseParams): Promise<Result<GetLeagueOwnerSummaryResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
async execute(params: GetLeagueOwnerSummaryUseCaseParams): Promise<Result<GetLeagueOwnerSummaryOutputPort, ApplicationErrorCode<'NO_ERROR'>>> {
const driver = await this.driverRepository.findById(params.ownerId);
const summary = driver ? { driver: { id: driver.id, name: driver.name }, rating: 0, rank: 0 } : null;
return Result.ok({ summary });

View File

@@ -4,8 +4,10 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
import type { IResultRepository } from '../../domain/repositories/IResultRepository';
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
import type { DriverRatingProvider } from '../ports/DriverRatingProvider';
import type { IImageServicePort } from '../ports/IImageServicePort';
import type { GetDriverRatingInputPort } from '../ports/input/GetDriverRatingInputPort';
import type { GetDriverRatingOutputPort } from '../ports/output/GetDriverRatingOutputPort';
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
import type {
RaceDetailViewModel,
RaceDetailRaceViewModel,
@@ -44,8 +46,8 @@ export class GetRaceDetailUseCase
private readonly raceRegistrationRepository: IRaceRegistrationRepository,
private readonly resultRepository: IResultRepository,
private readonly leagueMembershipRepository: ILeagueMembershipRepository,
private readonly driverRatingProvider: DriverRatingProvider,
private readonly imageService: IImageServicePort,
private readonly getDriverRating: (input: GetDriverRatingInputPort) => Promise<GetDriverRatingOutputPort>,
private readonly getDriverAvatar: (input: GetDriverAvatarInputPort) => Promise<GetDriverAvatarOutputPort>,
) {}
async execute(params: GetRaceDetailQueryParams): Promise<Result<RaceDetailViewModel, ApplicationErrorCode<GetRaceDetailErrorCode>>> {
@@ -62,22 +64,26 @@ export class GetRaceDetailUseCase
this.leagueMembershipRepository.getMembership(race.leagueId, driverId),
]);
const ratings = this.driverRatingProvider.getRatings(registeredDriverIds);
const drivers = await Promise.all(
registeredDriverIds.map(id => this.driverRepository.findById(id)),
);
const entryList: RaceDetailEntryViewModel[] = drivers
.filter((d): d is NonNullable<typeof d> => d !== null)
.map(driver => ({
id: driver.id,
name: driver.name,
country: driver.country,
avatarUrl: this.imageService.getDriverAvatar(driver.id),
rating: ratings.get(driver.id) ?? null,
isCurrentUser: driver.id === driverId,
}));
const entryList: RaceDetailEntryViewModel[] = [];
for (const driver of drivers) {
if (driver) {
const ratingResult = await this.getDriverRating({ driverId: driver.id });
const avatarResult = await this.getDriverAvatar({ driverId: driver.id });
entryList.push({
id: driver.id,
name: driver.name,
country: driver.country,
avatarUrl: avatarResult.avatarUrl,
rating: ratingResult.rating,
isCurrentUser: driver.id === driverId,
});
}
}
const isUserRegistered = registeredDriverIds.includes(driverId);
const isUpcoming = race.status === 'scheduled' && race.scheduledAt > new Date();

View File

@@ -1,6 +1,7 @@
import type { ITeamMembershipRepository } from '../../domain/repositories/ITeamMembershipRepository';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { IImageServicePort } from '../ports/IImageServicePort';
import type { GetDriverAvatarInputPort } from '../ports/input/GetDriverAvatarInputPort';
import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarOutputPort';
import type { TeamMembersResultDTO } from '../presenters/ITeamMembersPresenter';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
@@ -15,7 +16,7 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T
constructor(
private readonly membershipRepository: ITeamMembershipRepository,
private readonly driverRepository: IDriverRepository,
private readonly imageService: IImageServicePort,
private readonly getDriverAvatar: (input: GetDriverAvatarInputPort) => Promise<GetDriverAvatarOutputPort>,
private readonly logger: Logger,
) {}
@@ -37,7 +38,9 @@ export class GetTeamMembersUseCase implements AsyncUseCase<{ teamId: string }, T
} else {
this.logger.warn(`Driver with ID ${membership.driverId} not found while fetching team members for team ${input.teamId}.`);
}
avatarUrls[membership.driverId] = this.imageService.getDriverAvatar(membership.driverId);
const avatarResult = await this.getDriverAvatar({ driverId: membership.driverId });
avatarUrls[membership.driverId] = avatarResult.avatarUrl;
}
const dto: TeamMembersResultDTO = {

View File

@@ -1,4 +1,4 @@
import type { LeagueScoringPresetProvider } from '../ports/LeagueScoringPresetProvider';
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
import type { LeagueScoringPresetsResultDTO } from '../presenters/ILeagueScoringPresetsPresenter';
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
import { Result } from '@core/shared/application/Result';
@@ -6,18 +6,16 @@ import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorC
/**
* Use Case for listing league scoring presets.
* Orchestrates domain logic and returns result.
* Returns preset data without business logic.
*/
export class ListLeagueScoringPresetsUseCase
implements AsyncUseCase<void, LeagueScoringPresetsResultDTO, 'NO_ERROR'>
{
constructor(private readonly presetProvider: LeagueScoringPresetProvider) {}
constructor(private readonly presets: LeagueScoringPresetOutputPort[]) {}
async execute(): Promise<Result<LeagueScoringPresetsResultDTO, ApplicationErrorCode<'NO_ERROR'>>> {
const presets = await this.presetProvider.listPresets();
const dto: LeagueScoringPresetsResultDTO = {
presets,
presets: this.presets,
};
return Result.ok(dto);

View File

@@ -11,10 +11,8 @@ import type { ChampionshipStanding } from '@core/racing/domain/entities/champion
import { EventScoringService } from '@core/racing/domain/services/EventScoringService';
import { ChampionshipAggregator } from '@core/racing/domain/services/ChampionshipAggregator';
import type {
ChampionshipStandingsDTO,
ChampionshipStandingsRowDTO,
} from '../dto/ChampionshipStandingsDTO';
import type { ChampionshipStandingsOutputPort } from '../ports/output/ChampionshipStandingsOutputPort';
import type { ChampionshipStandingsRowOutputPort } from '../ports/output/ChampionshipStandingsRowOutputPort';
import type { AsyncUseCase } from '@core/shared/application/AsyncUseCase';
import { Result } from '@core/shared/application/Result';
@@ -31,7 +29,7 @@ type RecalculateChampionshipStandingsErrorCode =
| 'CHAMPIONSHIP_CONFIG_NOT_FOUND';
export class RecalculateChampionshipStandingsUseCase
implements AsyncUseCase<RecalculateChampionshipStandingsParams, ChampionshipStandingsDTO, RecalculateChampionshipStandingsErrorCode>
implements AsyncUseCase<RecalculateChampionshipStandingsParams, ChampionshipStandingsOutputPort, RecalculateChampionshipStandingsErrorCode>
{
constructor(
private readonly seasonRepository: ISeasonRepository,
@@ -44,7 +42,7 @@ export class RecalculateChampionshipStandingsUseCase
private readonly championshipAggregator: ChampionshipAggregator,
) {}
async execute(params: RecalculateChampionshipStandingsParams): Promise<Result<ChampionshipStandingsDTO, ApplicationErrorCode<RecalculateChampionshipStandingsErrorCode>>> {
async execute(params: RecalculateChampionshipStandingsParams): Promise<Result<ChampionshipStandingsOutputPort, ApplicationErrorCode<RecalculateChampionshipStandingsErrorCode>>> {
const { seasonId, championshipId } = params;
const season = await this.seasonRepository.findById(seasonId);
@@ -107,7 +105,7 @@ export class RecalculateChampionshipStandingsUseCase
resultsDropped: s.resultsDropped.toNumber(),
}));
const dto: ChampionshipStandingsDTO = {
const dto: ChampionshipStandingsOutputPort = {
seasonId,
championshipId: championship.id,
championshipName: championship.name,

View File

@@ -1,32 +1,26 @@
import { describe, it, expect, vi } from 'vitest';
import { UpdateDriverProfileUseCase } from './UpdateDriverProfileUseCase';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import { EntityMappers } from '../mappers/EntityMappers';
vi.mock('../mappers/EntityMappers', () => ({
EntityMappers: {
toDriverDTO: vi.fn(),
},
}));
import type { Driver } from '../../domain/entities/Driver';
describe('UpdateDriverProfileUseCase', () => {
it('updates driver profile successfully', async () => {
const mockDriver = {
id: 'driver-1',
update: vi.fn().mockReturnValue({}),
};
} as unknown as Driver;
const mockUpdatedDriver = {};
const mockDTO = { id: 'driver-1', bio: 'New bio' };
const mockUpdatedDriver = {
id: 'driver-1',
bio: 'New bio',
country: 'US',
} as Driver;
const mockDriverRepository = {
findById: vi.fn().mockResolvedValue(mockDriver),
update: vi.fn().mockResolvedValue(mockUpdatedDriver),
} as unknown as IDriverRepository;
(EntityMappers.toDriverDTO as any).mockReturnValue(mockDTO);
const useCase = new UpdateDriverProfileUseCase(mockDriverRepository);
const input = {
@@ -38,11 +32,10 @@ describe('UpdateDriverProfileUseCase', () => {
const result = await useCase.execute(input);
expect(result.isOk()).toBe(true);
expect(result.unwrap()).toEqual(mockDTO);
expect(result.unwrap()).toEqual(mockUpdatedDriver);
expect(mockDriverRepository.findById).toHaveBeenCalledWith('driver-1');
expect(mockDriver.update).toHaveBeenCalledWith({ bio: 'New bio', country: 'US' });
expect(mockDriverRepository.update).toHaveBeenCalledWith({});
expect(EntityMappers.toDriverDTO).toHaveBeenCalledWith(mockUpdatedDriver);
});
it('returns error when driver not found', async () => {
@@ -67,19 +60,18 @@ describe('UpdateDriverProfileUseCase', () => {
const mockDriver = {
id: 'driver-1',
update: vi.fn().mockReturnValue({}),
};
} as unknown as Driver;
const mockUpdatedDriver = {};
const mockDTO = { id: 'driver-1', country: 'US' };
const mockUpdatedDriver = {
id: 'driver-1',
country: 'US',
} as Driver;
const mockDriverRepository = {
findById: vi.fn().mockResolvedValue(mockDriver),
update: vi.fn().mockResolvedValue(mockUpdatedDriver),
} as unknown as IDriverRepository;
(EntityMappers.toDriverDTO as any).mockReturnValue(mockDTO);
const useCase = new UpdateDriverProfileUseCase(mockDriverRepository);
const input = {

View File

@@ -1,8 +1,7 @@
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { IDriverRepository } from '../../domain/repositories/IDriverRepository';
import type { DriverDTO } from '../dto/DriverDTO';
import { EntityMappers } from '../mappers/EntityMappers';
import type { Driver } from '../../domain/entities/Driver';
export interface UpdateDriverProfileInput {
driverId: string;
@@ -12,12 +11,13 @@ export interface UpdateDriverProfileInput {
/**
* Application use case responsible for updating basic driver profile details.
* Encapsulates domain entity mutation and mapping to a DriverDTO.
* Encapsulates domain entity mutation and returns the updated entity.
* Mapping to DTOs is handled by presenters in the presentation layer.
*/
export class UpdateDriverProfileUseCase {
constructor(private readonly driverRepository: IDriverRepository) {}
async execute(input: UpdateDriverProfileInput): Promise<Result<DriverDTO, ApplicationErrorCode<'DRIVER_NOT_FOUND'>>> {
async execute(input: UpdateDriverProfileInput): Promise<Result<Driver, ApplicationErrorCode<'DRIVER_NOT_FOUND'>>> {
const { driverId, bio, country } = input;
const existing = await this.driverRepository.findById(driverId);
@@ -31,7 +31,6 @@ export class UpdateDriverProfileUseCase {
});
const persisted = await this.driverRepository.update(updated);
const dto = EntityMappers.toDriverDTO(persisted);
return Result.ok(dto!);
return Result.ok(persisted);
}
}