refactor api modules

This commit is contained in:
2025-12-22 19:17:33 +01:00
parent c90b2166c1
commit 1333f5e907
100 changed files with 2226 additions and 1936 deletions

View File

@@ -13,11 +13,15 @@ import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
// Import use cases
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
// Import presenters
import { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter';
// Define injection tokens
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
export const LOGGER_TOKEN = 'Logger';
export const REVIEW_PROTEST_PRESENTER_TOKEN = 'ReviewProtestPresenter';
export const ProtestsProviders: Provider[] = [
ProtestsService, // Provide the service itself
@@ -40,6 +44,26 @@ export const ProtestsProviders: Provider[] = [
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
},
{
provide: REVIEW_PROTEST_PRESENTER_TOKEN,
useClass: ReviewProtestPresenter,
},
// Use cases
ReviewProtestUseCase,
{
provide: ReviewProtestUseCase,
useFactory: (
protestRepo: any,
raceRepo: any,
leagueMembershipRepo: any,
logger: Logger,
output: ReviewProtestPresenter,
) => new ReviewProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo, logger, output),
inject: [
PROTEST_REPOSITORY_TOKEN,
RACE_REPOSITORY_TOKEN,
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
LOGGER_TOKEN,
REVIEW_PROTEST_PRESENTER_TOKEN,
],
},
];

View File

@@ -3,10 +3,10 @@ import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application/Logger';
import type {
ReviewProtestUseCase,
ReviewProtestResult,
ReviewProtestApplicationError,
} from '@core/racing/application/use-cases/ReviewProtestUseCase';
import { ProtestsService } from './ProtestsService';
import type { ReviewProtestPresenter } from './presenters/ReviewProtestPresenter';
import type { ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter';
describe('ProtestsService', () => {
@@ -17,6 +17,21 @@ describe('ProtestsService', () => {
beforeEach(() => {
executeMock = vi.fn();
const reviewProtestUseCase = { execute: executeMock } as unknown as ReviewProtestUseCase;
const reviewProtestPresenter = {
reset: vi.fn(),
setCommand: vi.fn(),
present: vi.fn(),
presentError: vi.fn(),
getResponseModel: vi.fn(),
get responseModel() {
return {
success: true,
protestId: 'test',
stewardId: 'test',
decision: 'uphold' as const,
};
},
} as unknown as ReviewProtestPresenter;
logger = {
debug: vi.fn(),
info: vi.fn(),
@@ -24,7 +39,7 @@ describe('ProtestsService', () => {
error: vi.fn(),
} as unknown as Logger;
service = new ProtestsService(reviewProtestUseCase, logger);
service = new ProtestsService(reviewProtestUseCase, reviewProtestPresenter, logger);
});
const baseCommand = {
@@ -35,15 +50,7 @@ describe('ProtestsService', () => {
};
it('returns DTO with success model on success', async () => {
const coreResult: ReviewProtestResult = {
leagueId: 'league-1',
protestId: baseCommand.protestId,
status: 'upheld',
stewardId: baseCommand.stewardId,
decision: baseCommand.decision,
};
executeMock.mockResolvedValue(Result.ok<ReviewProtestResult, ReviewProtestApplicationError>(coreResult));
executeMock.mockResolvedValue(Result.ok(undefined));
const dto = await service.reviewProtest(baseCommand);
@@ -62,7 +69,7 @@ describe('ProtestsService', () => {
details: { message: 'Protest not found' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);
@@ -79,7 +86,7 @@ describe('ProtestsService', () => {
details: { message: 'Race not found for protest' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);
@@ -96,7 +103,7 @@ describe('ProtestsService', () => {
details: { message: 'Steward is not authorized to review this protest' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);
@@ -114,7 +121,7 @@ describe('ProtestsService', () => {
details: { message: 'Failed to review protest' },
};
executeMock.mockResolvedValue(Result.err<ReviewProtestResult, ReviewProtestApplicationError>(error));
executeMock.mockResolvedValue(Result.err<void, ReviewProtestApplicationError>(error));
const dto = await service.reviewProtest(baseCommand);

View File

@@ -8,17 +8,14 @@ import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewP
import { ReviewProtestPresenter, type ReviewProtestResponseDTO } from './presenters/ReviewProtestPresenter';
// Tokens
import { LOGGER_TOKEN } from './ProtestsProviders';
import { LOGGER_TOKEN, REVIEW_PROTEST_PRESENTER_TOKEN } from './ProtestsProviders';
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
import { PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN } from './ProtestsProviders';
@Injectable()
export class ProtestsService {
constructor(
private readonly reviewProtestUseCase: ReviewProtestUseCase,
@Inject(REVIEW_PROTEST_PRESENTER_TOKEN) private readonly reviewProtestPresenter: ReviewProtestPresenter,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
@@ -30,11 +27,14 @@ export class ProtestsService {
}): Promise<ReviewProtestResponseDTO> {
this.logger.debug('[ProtestsService] Reviewing protest:', command);
const result = await this.reviewProtestUseCase.execute(command);
const presenter = new ReviewProtestPresenter();
// Set the command on the presenter so it can include stewardId and decision in the response
this.reviewProtestPresenter.setCommand({
stewardId: command.stewardId,
decision: command.decision,
});
presenter.present(result);
await this.reviewProtestUseCase.execute(command);
return presenter.responseModel;
return this.reviewProtestPresenter.responseModel;
}
}
}

View File

@@ -12,16 +12,27 @@ export interface ReviewProtestResponseDTO {
export class ReviewProtestPresenter implements UseCaseOutputPort<ReviewProtestResult> {
private model: ReviewProtestResponseDTO | null = null;
private command: { stewardId: string; decision: 'uphold' | 'dismiss' } | null = null;
reset(): void {
this.model = null;
this.command = null;
}
setCommand(command: { stewardId: string; decision: 'uphold' | 'dismiss' }): void {
this.command = command;
}
present(result: ReviewProtestResult): void {
if (!this.command) {
throw new Error('Command must be set before presenting result');
}
this.model = {
success: true,
protestId: result.protestId,
decision: result.status === 'upheld' ? 'uphold' : 'dismiss',
stewardId: this.command.stewardId,
decision: this.command.decision,
};
}