rating
This commit is contained in:
@@ -10,6 +10,7 @@ import type { IResultRepository } from '../../domain/repositories/IResultReposit
|
||||
import type { IStandingRepository } from '../../domain/repositories/IStandingRepository';
|
||||
import { RatingUpdateService } from '@core/identity/domain/services/RatingUpdateService';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { IRaceResultsProvider } from '@core/identity/application/ports/IRaceResultsProvider';
|
||||
|
||||
describe('CompleteRaceUseCaseWithRatings', () => {
|
||||
let useCase: CompleteRaceUseCaseWithRatings;
|
||||
@@ -32,6 +33,11 @@ describe('CompleteRaceUseCaseWithRatings', () => {
|
||||
};
|
||||
let ratingUpdateService: {
|
||||
updateDriverRatingsAfterRace: Mock;
|
||||
recordRaceRatingEvents: Mock;
|
||||
};
|
||||
let raceResultsProvider: {
|
||||
getRaceResults: Mock;
|
||||
hasRaceResults: Mock;
|
||||
};
|
||||
let output: { present: Mock };
|
||||
|
||||
@@ -55,9 +61,15 @@ describe('CompleteRaceUseCaseWithRatings', () => {
|
||||
};
|
||||
ratingUpdateService = {
|
||||
updateDriverRatingsAfterRace: vi.fn(),
|
||||
recordRaceRatingEvents: vi.fn(),
|
||||
};
|
||||
raceResultsProvider = {
|
||||
getRaceResults: vi.fn(),
|
||||
hasRaceResults: vi.fn(),
|
||||
};
|
||||
output = { present: vi.fn() };
|
||||
|
||||
// Test without raceResultsProvider (backward compatible mode)
|
||||
useCase = new CompleteRaceUseCaseWithRatings(
|
||||
raceRepository as unknown as IRaceRepository,
|
||||
raceRegistrationRepository as unknown as IRaceRegistrationRepository,
|
||||
@@ -221,4 +233,185 @@ describe('CompleteRaceUseCaseWithRatings', () => {
|
||||
expect(error.details?.message).toBe('DB error');
|
||||
expect(output.present).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// SLICE 7: New tests for ledger-based approach
|
||||
describe('Ledger-based rating updates (Slice 7)', () => {
|
||||
let useCaseWithLedger: CompleteRaceUseCaseWithRatings;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create use case with raceResultsProvider for ledger mode
|
||||
useCaseWithLedger = new CompleteRaceUseCaseWithRatings(
|
||||
raceRepository as unknown as IRaceRepository,
|
||||
raceRegistrationRepository as unknown as IRaceRegistrationRepository,
|
||||
resultRepository as unknown as IResultRepository,
|
||||
standingRepository as unknown as IStandingRepository,
|
||||
driverRatingProvider,
|
||||
ratingUpdateService as unknown as RatingUpdateService,
|
||||
output as unknown as UseCaseOutputPort<CompleteRaceWithRatingsResult>,
|
||||
raceResultsProvider as unknown as IRaceResultsProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it('completes race with ledger-based rating updates when raceResultsProvider is available', async () => {
|
||||
const command: CompleteRaceWithRatingsInput = {
|
||||
raceId: 'race-1',
|
||||
};
|
||||
|
||||
const mockRace = {
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
status: 'scheduled',
|
||||
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
|
||||
};
|
||||
raceRepository.findById.mockResolvedValue(mockRace);
|
||||
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1', 'driver-2']);
|
||||
driverRatingProvider.getRatings.mockReturnValue(
|
||||
new Map([
|
||||
['driver-1', 1600],
|
||||
['driver-2', 1500],
|
||||
]),
|
||||
);
|
||||
resultRepository.create.mockResolvedValue(undefined);
|
||||
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
|
||||
standingRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
// Mock ledger-based rating update
|
||||
ratingUpdateService.recordRaceRatingEvents.mockResolvedValue({
|
||||
success: true,
|
||||
eventsCreated: 4,
|
||||
driversUpdated: ['driver-1', 'driver-2'],
|
||||
});
|
||||
|
||||
raceRepository.update.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCaseWithLedger.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toBeUndefined();
|
||||
|
||||
// Verify ledger-based approach was used
|
||||
expect(ratingUpdateService.recordRaceRatingEvents).toHaveBeenCalledWith(
|
||||
'race-1',
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
userId: 'driver-1',
|
||||
startPos: expect.any(Number),
|
||||
finishPos: expect.any(Number),
|
||||
incidents: expect.any(Number),
|
||||
status: 'finished',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// Verify legacy method was NOT called
|
||||
expect(ratingUpdateService.updateDriverRatingsAfterRace).not.toHaveBeenCalled();
|
||||
|
||||
expect(raceRepository.update).toHaveBeenCalledWith({ id: 'race-1', status: 'completed' });
|
||||
expect(output.present).toHaveBeenCalledWith({
|
||||
raceId: 'race-1',
|
||||
ratingsUpdatedForDriverIds: ['driver-1', 'driver-2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to legacy approach when ledger update fails', async () => {
|
||||
const command: CompleteRaceWithRatingsInput = {
|
||||
raceId: 'race-1',
|
||||
};
|
||||
|
||||
const mockRace = {
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
status: 'scheduled',
|
||||
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
|
||||
};
|
||||
raceRepository.findById.mockResolvedValue(mockRace);
|
||||
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']);
|
||||
driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]]));
|
||||
resultRepository.create.mockResolvedValue(undefined);
|
||||
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
|
||||
standingRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
// Mock ledger-based rating update failure
|
||||
ratingUpdateService.recordRaceRatingEvents.mockResolvedValue({
|
||||
success: false,
|
||||
eventsCreated: 0,
|
||||
driversUpdated: [],
|
||||
});
|
||||
|
||||
// Legacy method should be called as fallback
|
||||
ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined);
|
||||
raceRepository.update.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCaseWithLedger.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
// Verify both methods were called (ledger attempted, then fallback)
|
||||
expect(ratingUpdateService.recordRaceRatingEvents).toHaveBeenCalled();
|
||||
expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles ledger update errors gracefully', async () => {
|
||||
const command: CompleteRaceWithRatingsInput = {
|
||||
raceId: 'race-1',
|
||||
};
|
||||
|
||||
const mockRace = {
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
status: 'scheduled',
|
||||
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
|
||||
};
|
||||
raceRepository.findById.mockResolvedValue(mockRace);
|
||||
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']);
|
||||
driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]]));
|
||||
resultRepository.create.mockResolvedValue(undefined);
|
||||
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
|
||||
standingRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
// Mock ledger-based rating update throwing error
|
||||
ratingUpdateService.recordRaceRatingEvents.mockRejectedValue(new Error('Ledger error'));
|
||||
|
||||
// Legacy method should be called as fallback
|
||||
ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined);
|
||||
raceRepository.update.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCaseWithLedger.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
// Verify fallback was used
|
||||
expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still works without raceResultsProvider (backward compatibility)', async () => {
|
||||
const command: CompleteRaceWithRatingsInput = {
|
||||
raceId: 'race-1',
|
||||
};
|
||||
|
||||
const mockRace = {
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
status: 'scheduled',
|
||||
complete: vi.fn().mockReturnValue({ id: 'race-1', status: 'completed' }),
|
||||
};
|
||||
raceRepository.findById.mockResolvedValue(mockRace);
|
||||
raceRegistrationRepository.getRegisteredDrivers.mockResolvedValue(['driver-1']);
|
||||
driverRatingProvider.getRatings.mockReturnValue(new Map([['driver-1', 1600]]));
|
||||
resultRepository.create.mockResolvedValue(undefined);
|
||||
standingRepository.findByDriverIdAndLeagueId.mockResolvedValue(null);
|
||||
standingRepository.save.mockResolvedValue(undefined);
|
||||
ratingUpdateService.updateDriverRatingsAfterRace.mockResolvedValue(undefined);
|
||||
raceRepository.update.mockResolvedValue(undefined);
|
||||
|
||||
// Use original useCase without raceResultsProvider
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
|
||||
// Verify only legacy method was called
|
||||
expect(ratingUpdateService.updateDriverRatingsAfterRace).toHaveBeenCalled();
|
||||
expect(ratingUpdateService.recordRaceRatingEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user