/** * Tests for GetTeamRatingLedgerQuery */ import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; import { TeamRatingEvent } from '../../domain/entities/TeamRatingEvent'; import type { TeamRatingEventRepository } from '../../domain/repositories/TeamRatingEventRepository'; import { TeamRatingDelta } from '../../domain/value-objects/TeamRatingDelta'; import { TeamRatingDimensionKey } from '../../domain/value-objects/TeamRatingDimensionKey'; import { TeamRatingEventId } from '../../domain/value-objects/TeamRatingEventId'; import { GetTeamRatingLedgerQuery, GetTeamRatingLedgerQueryHandler } from './GetTeamRatingLedgerQuery'; describe('GetTeamRatingLedgerQuery', () => { let mockRatingEventRepo: { findEventsPaginated: Mock }; let handler: GetTeamRatingLedgerQueryHandler; beforeEach(() => { mockRatingEventRepo = { findEventsPaginated: vi.fn(), }; handler = new GetTeamRatingLedgerQueryHandler(mockRatingEventRepo as unknown as TeamRatingEventRepository); }); describe('execute', () => { it('should return paginated ledger entries', async () => { const teamId = 'team-123'; // Mock paginated result const event1 = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(10), occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'race', id: 'race-123' }, reason: { code: 'RACE_FINISH' }, visibility: { public: true }, version: 1, }); const event2 = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(-5), occurredAt: new Date('2024-01-02T10:00:00Z'), createdAt: new Date('2024-01-02T10:00:00Z'), source: { type: 'penalty', id: 'penalty-456' }, reason: { code: 'LATE_JOIN' }, visibility: { public: true }, version: 1, weight: 2, }); mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [event1, event2], total: 2, limit: 20, offset: 0, hasMore: false, nextOffset: undefined, }); const query: GetTeamRatingLedgerQuery = { teamId }; const result = await handler.execute(query); expect(result.entries.length).toBe(2); const entry1 = result.entries[0]; expect(entry1).toBeDefined(); if (entry1) { expect(entry1.teamId).toBe(teamId); expect(entry1.dimension).toBe('driving'); expect(entry1.delta).toBe(10); expect(entry1.source.type).toBe('race'); expect(entry1.source.id).toBe('race-123'); expect(entry1.reason.code).toBe('RACE_FINISH'); expect(entry1.visibility.public).toBe(true); } const entry2 = result.entries[1]; expect(entry2).toBeDefined(); if (entry2) { expect(entry2.dimension).toBe('adminTrust'); expect(entry2.delta).toBe(-5); expect(entry2.weight).toBe(2); expect(entry2.source.type).toBe('penalty'); expect(entry2.source.id).toBe('penalty-456'); } expect(result.pagination.total).toBe(2); expect(result.pagination.limit).toBe(20); expect(result.pagination.hasMore).toBe(false); }); it('should apply default pagination values', async () => { const teamId = 'team-123'; mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0, hasMore: false, }); const query: GetTeamRatingLedgerQuery = { teamId }; await handler.execute(query); expect(mockRatingEventRepo.findEventsPaginated).toHaveBeenCalledWith( teamId, expect.objectContaining({ limit: 20, offset: 0, }) ); }); it('should apply custom pagination values', async () => { const teamId = 'team-123'; mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [], total: 0, limit: 10, offset: 20, hasMore: true, nextOffset: 30, }); const query: GetTeamRatingLedgerQuery = { teamId, limit: 10, offset: 20 }; await handler.execute(query); expect(mockRatingEventRepo.findEventsPaginated).toHaveBeenCalledWith( teamId, expect.objectContaining({ limit: 10, offset: 20, }) ); }); it('should apply filters when provided', async () => { const teamId = 'team-123'; const filter = { dimensions: ['driving'], sourceTypes: ['race', 'penalty'] as ('race' | 'penalty' | 'vote' | 'adminAction' | 'manualAdjustment')[], from: '2024-01-01T00:00:00Z', to: '2024-01-31T23:59:59Z', reasonCodes: ['RACE_FINISH', 'LATE_JOIN'], }; mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0, hasMore: false, }); const query: GetTeamRatingLedgerQuery = { teamId, filter }; await handler.execute(query); expect(mockRatingEventRepo.findEventsPaginated).toHaveBeenCalledWith( teamId, expect.objectContaining({ filter: expect.objectContaining({ dimensions: ['driving'], sourceTypes: ['race', 'penalty'], from: new Date('2024-01-01T00:00:00Z'), to: new Date('2024-01-31T23:59:59Z'), reasonCodes: ['RACE_FINISH', 'LATE_JOIN'], }), }) ); }); it('should handle events with optional weight', async () => { const teamId = 'team-123'; const eventWithWeight = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(15), weight: 1.5, occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'race', id: 'race-789' }, reason: { code: 'PERFORMANCE_BONUS' }, visibility: { public: true }, version: 1, }); const eventWithoutWeight = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(5), occurredAt: new Date('2024-01-02T10:00:00Z'), createdAt: new Date('2024-01-02T10:00:00Z'), source: { type: 'vote', id: 'vote-123' }, reason: { code: 'POSITIVE_VOTE' }, visibility: { public: true }, version: 1, }); mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [eventWithWeight, eventWithoutWeight], total: 2, limit: 20, offset: 0, hasMore: false, }); const query: GetTeamRatingLedgerQuery = { teamId }; const result = await handler.execute(query); const entry1 = result.entries[0]; const entry2 = result.entries[1]; expect(entry1).toBeDefined(); expect(entry2).toBeDefined(); if (entry1) { expect(entry1.weight).toBe(1.5); } if (entry2) { expect(entry2.weight).toBeUndefined(); } }); it('should handle events with optional source.id', async () => { const teamId = 'team-123'; const eventWithId = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(10), occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'race', id: 'race-123' }, reason: { code: 'RACE_FINISH' }, visibility: { public: true }, version: 1, }); const eventWithoutId = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(5), occurredAt: new Date('2024-01-02T10:00:00Z'), createdAt: new Date('2024-01-02T10:00:00Z'), source: { type: 'manualAdjustment' }, reason: { code: 'ADMIN_ADJUSTMENT' }, visibility: { public: true }, version: 1, }); mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [eventWithId, eventWithoutId], total: 2, limit: 20, offset: 0, hasMore: false, }); const query: GetTeamRatingLedgerQuery = { teamId }; const result = await handler.execute(query); const entry1 = result.entries[0]; const entry2 = result.entries[1]; expect(entry1).toBeDefined(); expect(entry2).toBeDefined(); if (entry1) { expect(entry1.source.id).toBe('race-123'); } if (entry2) { expect(entry2.source.id).toBeUndefined(); } }); it('should handle events with optional reason.description', async () => { const teamId = 'team-123'; const eventWithDescription = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('driving'), delta: TeamRatingDelta.create(10), occurredAt: new Date('2024-01-01T10:00:00Z'), createdAt: new Date('2024-01-01T10:00:00Z'), source: { type: 'race', id: 'race-123' }, reason: { code: 'RACE_FINISH', description: 'Finished 1st in class' }, visibility: { public: true }, version: 1, }); const eventWithoutDescription = TeamRatingEvent.create({ id: TeamRatingEventId.generate(), teamId, dimension: TeamRatingDimensionKey.create('adminTrust'), delta: TeamRatingDelta.create(5), occurredAt: new Date('2024-01-02T10:00:00Z'), createdAt: new Date('2024-01-02T10:00:00Z'), source: { type: 'vote', id: 'vote-123' }, reason: { code: 'POSITIVE_VOTE' }, visibility: { public: true }, version: 1, }); mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [eventWithDescription, eventWithoutDescription], total: 2, limit: 20, offset: 0, hasMore: false, }); const query: GetTeamRatingLedgerQuery = { teamId }; const result = await handler.execute(query); const entry1 = result.entries[0]; const entry2 = result.entries[1]; expect(entry1).toBeDefined(); expect(entry2).toBeDefined(); if (entry1) { expect(entry1.reason.description).toBe('Finished 1st in class'); } if (entry2) { expect(entry2.reason.description).toBeUndefined(); } }); it('should return nextOffset when hasMore is true', async () => { const teamId = 'team-123'; mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [], total: 50, limit: 20, offset: 20, hasMore: true, nextOffset: 40, }); const query: GetTeamRatingLedgerQuery = { teamId }; const result = await handler.execute(query); expect(result.pagination.hasMore).toBe(true); expect(result.pagination.nextOffset).toBe(40); }); it('should return null nextOffset when hasMore is false', async () => { const teamId = 'team-123'; mockRatingEventRepo.findEventsPaginated.mockResolvedValue({ items: [], total: 15, limit: 20, offset: 0, hasMore: false, }); const query: GetTeamRatingLedgerQuery = { teamId }; const result = await handler.execute(query); expect(result.pagination.hasMore).toBe(false); expect(result.pagination.nextOffset).toBeNull(); }); }); });