module cleanup
This commit is contained in:
121
apps/api/src/domain/analytics/AnalyticsController.test.ts
Normal file
121
apps/api/src/domain/analytics/AnalyticsController.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
import type { Response } from 'express';
|
||||
import { EntityType, VisitorType } from '@core/analytics/domain/types/PageView';
|
||||
import { EngagementAction, EngagementEntityType } from '@core/analytics/domain/types/EngagementEvent';
|
||||
|
||||
describe('AnalyticsController', () => {
|
||||
let controller: AnalyticsController;
|
||||
let service: ReturnType<typeof vi.mocked<AnalyticsService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AnalyticsController],
|
||||
providers: [
|
||||
{
|
||||
provide: AnalyticsService,
|
||||
useValue: {
|
||||
recordPageView: vi.fn(),
|
||||
recordEngagement: vi.fn(),
|
||||
getDashboardData: vi.fn(),
|
||||
getAnalyticsMetrics: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AnalyticsController>(AnalyticsController);
|
||||
service = vi.mocked(module.get(AnalyticsService));
|
||||
});
|
||||
|
||||
describe('recordPageView', () => {
|
||||
it('should record a page view and return 201', async () => {
|
||||
const input = {
|
||||
entityType: EntityType.RACE,
|
||||
entityId: 'race-123',
|
||||
visitorType: VisitorType.ANONYMOUS,
|
||||
sessionId: 'session-456',
|
||||
visitorId: 'visitor-789',
|
||||
referrer: 'https://example.com',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
country: 'US',
|
||||
};
|
||||
const output = { pageViewId: 'pv-123' };
|
||||
service.recordPageView.mockResolvedValue(output);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.recordPageView(input, mockRes);
|
||||
|
||||
expect(service.recordPageView).toHaveBeenCalledWith(input);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordEngagement', () => {
|
||||
it('should record an engagement and return 201', async () => {
|
||||
const input = {
|
||||
action: EngagementAction.CLICK_SPONSOR_LOGO,
|
||||
entityType: EngagementEntityType.RACE,
|
||||
entityId: 'race-123',
|
||||
actorType: 'driver' as const,
|
||||
sessionId: 'session-456',
|
||||
actorId: 'actor-789',
|
||||
metadata: { key: 'value' },
|
||||
};
|
||||
const output = { eventId: 'event-123', engagementWeight: 10 };
|
||||
service.recordEngagement.mockResolvedValue(output);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.recordEngagement(input, mockRes);
|
||||
|
||||
expect(service.recordEngagement).toHaveBeenCalledWith(input);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDashboardData', () => {
|
||||
it('should return dashboard data', async () => {
|
||||
const output = {
|
||||
totalUsers: 100,
|
||||
activeUsers: 50,
|
||||
totalRaces: 20,
|
||||
totalLeagues: 5,
|
||||
};
|
||||
service.getDashboardData.mockResolvedValue(output);
|
||||
|
||||
const result = await controller.getDashboardData();
|
||||
|
||||
expect(service.getDashboardData).toHaveBeenCalled();
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAnalyticsMetrics', () => {
|
||||
it('should return analytics metrics', async () => {
|
||||
const output = {
|
||||
pageViews: 1000,
|
||||
uniqueVisitors: 500,
|
||||
averageSessionDuration: 300,
|
||||
bounceRate: 0.4,
|
||||
};
|
||||
service.getAnalyticsMetrics.mockResolvedValue(output);
|
||||
|
||||
const result = await controller.getAnalyticsMetrics();
|
||||
|
||||
expect(service.getAnalyticsMetrics).toHaveBeenCalled();
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Controller, Get, Post, Body, Res, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
import type { RecordPageViewInputDTO } from './dtos/RecordPageViewInputDTO';
|
||||
import type { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO';
|
||||
import type { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO';
|
||||
import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
|
||||
import type { GetDashboardDataOutputDTO } from './dtos/GetDashboardDataOutputDTO';
|
||||
import type { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDTO';
|
||||
import { RecordPageViewInputDTO } from './dtos/RecordPageViewInputDTO';
|
||||
import { RecordPageViewOutputDTO } from './dtos/RecordPageViewOutputDTO';
|
||||
import { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO';
|
||||
import { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
|
||||
import { GetDashboardDataOutputDTO } from './dtos/GetDashboardDataOutputDTO';
|
||||
import { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDTO';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
type RecordPageViewInput = RecordPageViewInputDTO;
|
||||
|
||||
30
apps/api/src/domain/analytics/AnalyticsModule.test.ts
Normal file
30
apps/api/src/domain/analytics/AnalyticsModule.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AnalyticsModule } from './AnalyticsModule';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
|
||||
describe('AnalyticsModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [AnalyticsModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide AnalyticsController', () => {
|
||||
const controller = module.get<AnalyticsController>(AnalyticsController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(AnalyticsController);
|
||||
});
|
||||
|
||||
it('should provide AnalyticsService', () => {
|
||||
const service = module.get<AnalyticsService>(AnalyticsService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(AnalyticsService);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,27 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { AnalyticsService } from './AnalyticsService';
|
||||
import { RecordPageViewUseCase } from './use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from './use-cases/RecordEngagementUseCase';
|
||||
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
|
||||
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
|
||||
import type { IPageViewRepository } from '@core/analytics/domain/repositories/IPageViewRepository';
|
||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
const Logger_TOKEN = 'Logger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
|
||||
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
|
||||
const GET_DASHBOARD_DATA_USE_CASE_TOKEN = 'GetDashboardDataUseCase_TOKEN';
|
||||
const GET_ANALYTICS_METRICS_USE_CASE_TOKEN = 'GetAnalyticsMetricsUseCase_TOKEN';
|
||||
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
|
||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
import { InMemoryPageViewRepository } from '@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
|
||||
import { InMemoryEngagementRepository } from '@adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
|
||||
import { InMemoryPageViewRepository } from '@adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
|
||||
export const AnalyticsProviders: Provider[] = [
|
||||
AnalyticsService,
|
||||
RecordPageViewUseCase,
|
||||
RecordEngagementUseCase,
|
||||
{
|
||||
provide: Logger_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
@@ -35,10 +36,22 @@ export const AnalyticsProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: RECORD_PAGE_VIEW_USE_CASE_TOKEN,
|
||||
useClass: RecordPageViewUseCase,
|
||||
useFactory: (repo: IPageViewRepository, logger: Logger) => new RecordPageViewUseCase(repo, logger),
|
||||
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RECORD_ENGAGEMENT_USE_CASE_TOKEN,
|
||||
useClass: RecordEngagementUseCase,
|
||||
useFactory: (repo: IEngagementRepository, logger: Logger) => new RecordEngagementUseCase(repo, logger),
|
||||
inject: [IENGAGEMENT_REPO_TOKEN, Logger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_DASHBOARD_DATA_USE_CASE_TOKEN,
|
||||
useFactory: (logger: Logger) => new GetDashboardDataUseCase(logger),
|
||||
inject: [Logger_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_ANALYTICS_METRICS_USE_CASE_TOKEN,
|
||||
useFactory: (repo: IPageViewRepository, logger: Logger) => new GetAnalyticsMetricsUseCase(repo, logger),
|
||||
inject: [IPAGE_VIEW_REPO_TOKEN, Logger_TOKEN],
|
||||
},
|
||||
];
|
||||
@@ -5,9 +5,10 @@ import type { RecordEngagementInputDTO } from './dtos/RecordEngagementInputDTO';
|
||||
import type { RecordEngagementOutputDTO } from './dtos/RecordEngagementOutputDTO';
|
||||
import type { GetDashboardDataOutputDTO } from './dtos/GetDashboardDataOutputDTO';
|
||||
import type { GetAnalyticsMetricsOutputDTO } from './dtos/GetAnalyticsMetricsOutputDTO';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { RecordPageViewUseCase } from './use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from './use-cases/RecordEngagementUseCase';
|
||||
import { RecordPageViewUseCase } from '@core/analytics/application/use-cases/RecordPageViewUseCase';
|
||||
import { RecordEngagementUseCase } from '@core/analytics/application/use-cases/RecordEngagementUseCase';
|
||||
import { GetDashboardDataUseCase } from '@core/analytics/application/use-cases/GetDashboardDataUseCase';
|
||||
import { GetAnalyticsMetricsUseCase } from '@core/analytics/application/use-cases/GetAnalyticsMetricsUseCase';
|
||||
|
||||
type RecordPageViewInput = RecordPageViewInputDTO;
|
||||
type RecordPageViewOutput = RecordPageViewOutputDTO;
|
||||
@@ -16,16 +17,18 @@ type RecordEngagementOutput = RecordEngagementOutputDTO;
|
||||
type GetDashboardDataOutput = GetDashboardDataOutputDTO;
|
||||
type GetAnalyticsMetricsOutput = GetAnalyticsMetricsOutputDTO;
|
||||
|
||||
const Logger_TOKEN = 'Logger_TOKEN';
|
||||
const RECORD_PAGE_VIEW_USE_CASE_TOKEN = 'RecordPageViewUseCase_TOKEN';
|
||||
const RECORD_ENGAGEMENT_USE_CASE_TOKEN = 'RecordEngagementUseCase_TOKEN';
|
||||
const GET_DASHBOARD_DATA_USE_CASE_TOKEN = 'GetDashboardDataUseCase_TOKEN';
|
||||
const GET_ANALYTICS_METRICS_USE_CASE_TOKEN = 'GetAnalyticsMetricsUseCase_TOKEN';
|
||||
|
||||
@Injectable()
|
||||
export class AnalyticsService {
|
||||
constructor(
|
||||
@Inject(RECORD_PAGE_VIEW_USE_CASE_TOKEN) private readonly recordPageViewUseCase: RecordPageViewUseCase,
|
||||
@Inject(RECORD_ENGAGEMENT_USE_CASE_TOKEN) private readonly recordEngagementUseCase: RecordEngagementUseCase,
|
||||
@Inject(Logger_TOKEN) private readonly logger: Logger,
|
||||
@Inject(GET_DASHBOARD_DATA_USE_CASE_TOKEN) private readonly getDashboardDataUseCase: GetDashboardDataUseCase,
|
||||
@Inject(GET_ANALYTICS_METRICS_USE_CASE_TOKEN) private readonly getAnalyticsMetricsUseCase: GetAnalyticsMetricsUseCase,
|
||||
) {}
|
||||
|
||||
async recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
@@ -37,22 +40,10 @@ export class AnalyticsService {
|
||||
}
|
||||
|
||||
async getDashboardData(): Promise<GetDashboardDataOutput> {
|
||||
// TODO: Implement actual dashboard data retrieval
|
||||
return {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
totalRaces: 0,
|
||||
totalLeagues: 0,
|
||||
};
|
||||
return await this.getDashboardDataUseCase.execute();
|
||||
}
|
||||
|
||||
async getAnalyticsMetrics(): Promise<GetAnalyticsMetricsOutput> {
|
||||
// TODO: Implement actual analytics metrics retrieval
|
||||
return {
|
||||
pageViews: 0,
|
||||
uniqueVisitors: 0,
|
||||
averageSessionDuration: 0,
|
||||
bounceRate: 0,
|
||||
};
|
||||
return await this.getAnalyticsMetricsUseCase.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementAction {
|
||||
CLICK_SPONSOR_LOGO = 'click_sponsor_logo',
|
||||
CLICK_SPONSOR_URL = 'click_sponsor_url',
|
||||
DOWNLOAD_LIVERY_PACK = 'download_livery_pack',
|
||||
JOIN_LEAGUE = 'join_league',
|
||||
REGISTER_RACE = 'register_race',
|
||||
VIEW_STANDINGS = 'view_standings',
|
||||
VIEW_SCHEDULE = 'view_schedule',
|
||||
SHARE_SOCIAL = 'share_social',
|
||||
CONTACT_SPONSOR = 'contact_sponsor',
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// From core/analytics/domain/types/EngagementEvent.ts
|
||||
export enum EngagementEntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum EntityType {
|
||||
LEAGUE = 'league',
|
||||
DRIVER = 'driver',
|
||||
TEAM = 'team',
|
||||
RACE = 'race',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsEnum, IsObject } from 'class-validator';
|
||||
import { EngagementAction } from './EngagementAction';
|
||||
import { EngagementEntityType } from './EngagementEntityType';
|
||||
import { EngagementAction, EngagementEntityType } from '@core/analytics/domain/types/EngagementEvent';
|
||||
|
||||
export class RecordEngagementInputDTO {
|
||||
@ApiProperty({ enum: EngagementAction })
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsEnum } from 'class-validator';
|
||||
import { EntityType } from './EntityType';
|
||||
import { VisitorType } from './VisitorType';
|
||||
import { EntityType, VisitorType } from '@core/analytics/domain/types/PageView';
|
||||
|
||||
export class RecordPageViewInputDTO {
|
||||
@ApiProperty({ enum: EntityType })
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// From core/analytics/domain/types/PageView.ts
|
||||
export enum VisitorType {
|
||||
ANONYMOUS = 'anonymous',
|
||||
DRIVER = 'driver',
|
||||
SPONSOR = 'sponsor',
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { RecordEngagementUseCase } from './RecordEngagementUseCase';
|
||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('RecordEngagementUseCase', () => {
|
||||
let useCase: RecordEngagementUseCase;
|
||||
let engagementRepository: jest.Mocked<IEngagementRepository>;
|
||||
let logger: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RecordEngagementUseCase,
|
||||
{
|
||||
provide: 'IEngagementRepository_TOKEN',
|
||||
useValue: {
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'Logger_TOKEN',
|
||||
useValue: {
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
useCase = module.get<RecordEngagementUseCase>(RecordEngagementUseCase);
|
||||
engagementRepository = module.get('IEngagementRepository_TOKEN');
|
||||
logger = module.get('Logger_TOKEN');
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should save the engagement event and return the eventId and engagementWeight', async () => {
|
||||
const input = {
|
||||
action: 'like' as any,
|
||||
entityType: 'race' as any,
|
||||
entityId: 'race-123',
|
||||
actorType: 'driver',
|
||||
sessionId: 'session-456',
|
||||
actorId: 'actor-789',
|
||||
metadata: { some: 'data' },
|
||||
};
|
||||
|
||||
const mockEvent = {
|
||||
getEngagementWeight: jest.fn().mockReturnValue(10),
|
||||
};
|
||||
|
||||
// Mock the create function to return the mock event
|
||||
const originalCreate = require('@gridpilot/analytics/domain/entities/EngagementEvent').EngagementEvent.create;
|
||||
require('@gridpilot/analytics/domain/entities/EngagementEvent').EngagementEvent.create = jest.fn().mockReturnValue(mockEvent);
|
||||
|
||||
engagementRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith('Executing RecordEngagementUseCase', { input });
|
||||
expect(engagementRepository.save).toHaveBeenCalledWith(mockEvent);
|
||||
expect(logger.info).toHaveBeenCalledWith('Engagement recorded successfully', expect.objectContaining({ eventId: expect.any(String), input }));
|
||||
expect(result).toHaveProperty('eventId');
|
||||
expect(result).toHaveProperty('engagementWeight', 10);
|
||||
expect(typeof result.eventId).toBe('string');
|
||||
|
||||
// Restore original
|
||||
require('@gridpilot/analytics/domain/entities/EngagementEvent').EngagementEvent.create = originalCreate;
|
||||
});
|
||||
|
||||
it('should handle errors and throw them', async () => {
|
||||
const input = {
|
||||
action: 'like' as any,
|
||||
entityType: 'race' as any,
|
||||
entityId: 'race-123',
|
||||
actorType: 'driver',
|
||||
sessionId: 'session-456',
|
||||
};
|
||||
|
||||
const error = new Error('Save failed');
|
||||
engagementRepository.save.mockRejectedValue(error);
|
||||
|
||||
await expect(useCase.execute(input)).rejects.toThrow('Save failed');
|
||||
expect(logger.error).toHaveBeenCalledWith('Error recording engagement', error, { input });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { RecordEngagementInputDTO } from '../dtos/RecordEngagementInputDTO';
|
||||
import type { RecordEngagementOutputDTO } from '../dtos/RecordEngagementOutputDTO';
|
||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { EngagementEvent } from '@core/analytics/domain/entities/EngagementEvent';
|
||||
|
||||
type RecordEngagementInput = RecordEngagementInputDTO;
|
||||
type RecordEngagementOutput = RecordEngagementOutputDTO;
|
||||
|
||||
const Logger_TOKEN = 'Logger_TOKEN';
|
||||
const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN';
|
||||
|
||||
@Injectable()
|
||||
export class RecordEngagementUseCase {
|
||||
constructor(
|
||||
@Inject(IENGAGEMENT_REPO_TOKEN) private readonly engagementRepository: IEngagementRepository,
|
||||
@Inject(Logger_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||
this.logger.debug('Executing RecordEngagementUseCase', { input });
|
||||
try {
|
||||
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
|
||||
id: eventId,
|
||||
action: input.action as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
actorType: input.actorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const event = EngagementEvent.create({
|
||||
...baseProps,
|
||||
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
||||
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
||||
});
|
||||
|
||||
await this.engagementRepository.save(event);
|
||||
this.logger.info('Engagement recorded successfully', { eventId, input });
|
||||
|
||||
return {
|
||||
eventId,
|
||||
engagementWeight: event.getEngagementWeight(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording engagement', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { RecordPageViewUseCase } from './RecordPageViewUseCase';
|
||||
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('RecordPageViewUseCase', () => {
|
||||
let useCase: RecordPageViewUseCase;
|
||||
let pageViewRepository: jest.Mocked<IPageViewRepository>;
|
||||
let logger: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
RecordPageViewUseCase,
|
||||
{
|
||||
provide: 'IPageViewRepository_TOKEN',
|
||||
useValue: {
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'Logger_TOKEN',
|
||||
useValue: {
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
useCase = module.get<RecordPageViewUseCase>(RecordPageViewUseCase);
|
||||
pageViewRepository = module.get('IPageViewRepository_TOKEN');
|
||||
logger = module.get('Logger_TOKEN');
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should save the page view and return the pageViewId', async () => {
|
||||
const input = {
|
||||
entityType: 'race' as any,
|
||||
entityId: 'race-123',
|
||||
visitorType: 'anonymous' as any,
|
||||
sessionId: 'session-456',
|
||||
visitorId: 'visitor-789',
|
||||
referrer: 'https://example.com',
|
||||
userAgent: 'Mozilla/5.0',
|
||||
country: 'US',
|
||||
};
|
||||
|
||||
pageViewRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith('Executing RecordPageViewUseCase', { input });
|
||||
expect(pageViewRepository.save).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenCalledWith('Page view recorded successfully', expect.objectContaining({ pageViewId: expect.any(String), input }));
|
||||
expect(result).toHaveProperty('pageViewId');
|
||||
expect(typeof result.pageViewId).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle errors and throw them', async () => {
|
||||
const input = {
|
||||
entityType: 'race' as any,
|
||||
entityId: 'race-123',
|
||||
visitorType: 'anonymous' as any,
|
||||
sessionId: 'session-456',
|
||||
};
|
||||
|
||||
const error = new Error('Save failed');
|
||||
pageViewRepository.save.mockRejectedValue(error);
|
||||
|
||||
await expect(useCase.execute(input)).rejects.toThrow('Save failed');
|
||||
expect(logger.error).toHaveBeenCalledWith('Error recording page view', error, { input });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { RecordPageViewInputDTO } from '../dtos/RecordPageViewInputDTO';
|
||||
import type { RecordPageViewOutputDTO } from '../dtos/RecordPageViewOutputDTO';
|
||||
import type { IPageViewRepository } from '@core/analytics/application/repositories/IPageViewRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { PageView } from '@core/analytics/domain/entities/PageView';
|
||||
|
||||
type RecordPageViewInput = RecordPageViewInputDTO;
|
||||
type RecordPageViewOutput = RecordPageViewOutputDTO;
|
||||
|
||||
const Logger_TOKEN = 'Logger_TOKEN';
|
||||
const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN';
|
||||
|
||||
@Injectable()
|
||||
export class RecordPageViewUseCase {
|
||||
constructor(
|
||||
@Inject(IPAGE_VIEW_REPO_TOKEN) private readonly pageViewRepository: IPageViewRepository,
|
||||
@Inject(Logger_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
this.logger.debug('Executing RecordPageViewUseCase', { input });
|
||||
try {
|
||||
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
|
||||
id: pageViewId,
|
||||
entityType: input.entityType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
entityId: input.entityId,
|
||||
visitorType: input.visitorType as any, // Cast to any to bypass strict type checking, will resolve with proper domain layer alignment
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const pageView = PageView.create({
|
||||
...baseProps,
|
||||
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
|
||||
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
|
||||
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
||||
...(input.country !== undefined ? { country: input.country } : {}),
|
||||
});
|
||||
|
||||
await this.pageViewRepository.save(pageView);
|
||||
this.logger.info('Page view recorded successfully', { pageViewId, input });
|
||||
return { pageViewId };
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording page view', error, { input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
108
apps/api/src/domain/auth/AuthController.test.ts
Normal file
108
apps/api/src/domain/auth/AuthController.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { vi } from 'vitest';
|
||||
import { AuthController } from './AuthController';
|
||||
import { AuthService } from './AuthService';
|
||||
import { SignupParams, LoginParams, AuthSessionDTO } from './dtos/AuthDto';
|
||||
|
||||
describe('AuthController', () => {
|
||||
let controller: AuthController;
|
||||
let service: ReturnType<typeof vi.mocked<AuthService>>;
|
||||
|
||||
beforeEach(() => {
|
||||
service = vi.mocked<AuthService>({
|
||||
signupWithEmail: vi.fn(),
|
||||
loginWithEmail: vi.fn(),
|
||||
getCurrentSession: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
});
|
||||
|
||||
controller = new AuthController(service);
|
||||
});
|
||||
|
||||
describe('signup', () => {
|
||||
it('should call service.signupWithEmail and return session', async () => {
|
||||
const params: SignupParams = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
displayName: 'Test User',
|
||||
iracingCustomerId: '12345',
|
||||
primaryDriverId: 'driver1',
|
||||
avatarUrl: 'http://example.com/avatar.jpg',
|
||||
};
|
||||
const session: AuthSessionDTO = {
|
||||
token: 'token123',
|
||||
user: {
|
||||
userId: 'user1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
},
|
||||
};
|
||||
service.signupWithEmail.mockResolvedValue(session);
|
||||
|
||||
const result = await controller.signup(params);
|
||||
|
||||
expect(service.signupWithEmail).toHaveBeenCalledWith(params);
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should call service.loginWithEmail and return session', async () => {
|
||||
const params: LoginParams = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
const session: AuthSessionDTO = {
|
||||
token: 'token123',
|
||||
user: {
|
||||
userId: 'user1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
},
|
||||
};
|
||||
service.loginWithEmail.mockResolvedValue(session);
|
||||
|
||||
const result = await controller.login(params);
|
||||
|
||||
expect(service.loginWithEmail).toHaveBeenCalledWith(params);
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
it('should call service.getCurrentSession and return session', async () => {
|
||||
const session: AuthSessionDTO = {
|
||||
token: 'token123',
|
||||
user: {
|
||||
userId: 'user1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
},
|
||||
};
|
||||
service.getCurrentSession.mockResolvedValue(session);
|
||||
|
||||
const result = await controller.getSession();
|
||||
|
||||
expect(service.getCurrentSession).toHaveBeenCalled();
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
|
||||
it('should return null if no session', async () => {
|
||||
service.getCurrentSession.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getSession();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
it('should call service.logout', async () => {
|
||||
service.logout.mockResolvedValue(undefined);
|
||||
|
||||
await controller.logout();
|
||||
|
||||
expect(service.logout).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Controller, Get, Post, Body, Query, Res, Redirect, HttpStatus } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { Controller, Get, Post, Body } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParams, SignupParams, LoginWithIracingCallbackParams, AuthSessionDTO, IracingAuthRedirectResult } from './dto/AuthDto';
|
||||
import { LoginParams, SignupParams, AuthSessionDTO } from './dtos/AuthDto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@@ -27,16 +26,4 @@ export class AuthController {
|
||||
return this.authService.logout();
|
||||
}
|
||||
|
||||
@Get('iracing/start')
|
||||
async startIracingAuthRedirect(@Query('returnTo') returnTo?: string, @Res() res?: Response): Promise<void> {
|
||||
const { redirectUrl, state } = await this.authService.startIracingAuthRedirect(returnTo);
|
||||
// In real application, you might want to store 'state' in a secure cookie or session.
|
||||
// For this example, we'll just redirect.
|
||||
res.redirect(HttpStatus.FOUND, redirectUrl);
|
||||
}
|
||||
|
||||
@Get('iracing/callback')
|
||||
async loginWithIracingCallback(@Query('code') code: string, @Query('state') state: string, @Query('returnTo') returnTo?: string): Promise<AuthSessionDTO> {
|
||||
return this.authService.loginWithIracingCallback({ code, state, returnTo });
|
||||
}
|
||||
}
|
||||
|
||||
30
apps/api/src/domain/auth/AuthModule.test.ts
Normal file
30
apps/api/src/domain/auth/AuthModule.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthModule } from './AuthModule';
|
||||
import { AuthController } from './AuthController';
|
||||
import { AuthService } from './AuthService';
|
||||
|
||||
describe('AuthModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [AuthModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide AuthController', () => {
|
||||
const controller = module.get<AuthController>(AuthController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(AuthController);
|
||||
});
|
||||
|
||||
it('should provide AuthService', () => {
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(AuthService);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { AuthProviders } from './AuthProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: AuthProviders,
|
||||
providers: [AuthService, ...AuthProviders],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { AuthService } from './AuthService';
|
||||
|
||||
// Import interfaces and concrete implementations
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository, StoredUser } from '@core/identity/domain/repositories/IUserRepository';
|
||||
import { StoredUser } from '@core/identity/domain/repositories/IUserRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
@@ -11,7 +9,6 @@ import { InMemoryAuthRepository } from '@adapters/identity/persistence/inmemory/
|
||||
import { InMemoryUserRepository } from '@adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
||||
import { InMemoryPasswordHashingService } from '@adapters/identity/services/InMemoryPasswordHashingService';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
|
||||
import { CookieIdentitySessionAdapter } from '@adapters/identity/session/CookieIdentitySessionAdapter';
|
||||
|
||||
// Define the tokens for dependency injection
|
||||
@@ -22,10 +19,9 @@ export const LOGGER_TOKEN = 'Logger';
|
||||
export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
|
||||
|
||||
export const AuthProviders: Provider[] = [
|
||||
AuthService, // Provide the service itself
|
||||
{
|
||||
provide: AUTH_REPOSITORY_TOKEN,
|
||||
useFactory: (userRepository: IUserRepository, passwordHashingService: IPasswordHashingService, logger: Logger) => {
|
||||
useFactory: (passwordHashingService: IPasswordHashingService, logger: Logger) => {
|
||||
// Seed initial users for InMemoryUserRepository
|
||||
const initialUsers: StoredUser[] = [
|
||||
// Example user (replace with actual test users as needed)
|
||||
@@ -41,7 +37,7 @@ export const AuthProviders: Provider[] = [
|
||||
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
|
||||
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
|
||||
},
|
||||
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
inject: [PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
import { Injectable, Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import type { AuthenticatedUserDTO, AuthSessionDTO, SignupParams, LoginParams, IracingAuthRedirectResult, LoginWithIracingCallbackParams } from './dto/AuthDto';
|
||||
import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
|
||||
// Core Use Cases
|
||||
import { LoginUseCase } from '@core/identity/application/use-cases/LoginUseCase';
|
||||
import { SignupUseCase } from '@core/identity/application/use-cases/SignupUseCase';
|
||||
import { GetCurrentSessionUseCase } from '@core/identity/application/use-cases/GetCurrentSessionUseCase';
|
||||
import { LogoutUseCase } from '@core/identity/application/use-cases/LogoutUseCase';
|
||||
import { StartIracingAuthRedirectUseCase } from '@core/identity/application/use-cases/StartIracingAuthRedirectUseCase';
|
||||
import { LoginWithIracingCallbackUseCase } from '@core/identity/application/use-cases/LoginWithIracingCallbackUseCase';
|
||||
import { SignupUseCase } from '@core/identity/application/use-cases/SignupUseCase';
|
||||
|
||||
// Core Interfaces and Tokens
|
||||
import { AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, IDENTITY_SESSION_PORT_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
||||
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '@core/identity/application/dto/AuthenticatedUserDTO';
|
||||
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
|
||||
import { User } from '@core/identity/domain/entities/User';
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { Logger } from "@core/shared/application";
|
||||
import { IdentitySessionPort } from '@core/identity/application/ports/IdentitySessionPort';
|
||||
import { UserId } from '@core/identity/domain/value-objects/UserId';
|
||||
import { User } from '@core/identity/domain/entities/User';
|
||||
import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
|
||||
import { AuthenticatedUserDTO as CoreAuthenticatedUserDTO } from '@core/identity/application/dto/AuthenticatedUserDTO';
|
||||
import { AUTH_REPOSITORY_TOKEN, IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
||||
import { AuthSessionDTO, LoginParams, SignupParams, AuthenticatedUserDTO } from './dtos/AuthDto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly loginUseCase: LoginUseCase;
|
||||
private readonly signupUseCase: SignupUseCase;
|
||||
private readonly getCurrentSessionUseCase: GetCurrentSessionUseCase;
|
||||
private readonly logoutUseCase: LogoutUseCase;
|
||||
private readonly startIracingAuthRedirectUseCase: StartIracingAuthRedirectUseCase;
|
||||
private readonly loginWithIracingCallbackUseCase: LoginWithIracingCallbackUseCase;
|
||||
|
||||
constructor(
|
||||
@Inject(AUTH_REPOSITORY_TOKEN) private authRepository: IAuthRepository,
|
||||
@@ -38,10 +31,7 @@ export class AuthService {
|
||||
) {
|
||||
this.loginUseCase = new LoginUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.signupUseCase = new SignupUseCase(this.authRepository, this.passwordHashingService);
|
||||
this.getCurrentSessionUseCase = new GetCurrentSessionUseCase(); // Doesn't have constructor parameters normally
|
||||
this.logoutUseCase = new LogoutUseCase(this.identitySessionPort);
|
||||
this.startIracingAuthRedirectUseCase = new StartIracingAuthRedirectUseCase();
|
||||
this.loginWithIracingCallbackUseCase = new LoginWithIracingCallbackUseCase();
|
||||
}
|
||||
|
||||
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
|
||||
@@ -49,10 +39,14 @@ export class AuthService {
|
||||
userId: user.getId().value,
|
||||
email: user.getEmail() ?? '',
|
||||
displayName: user.getDisplayName() ?? '',
|
||||
// Map other fields as necessary
|
||||
iracingCustomerId: user.getIracingCustomerId() ?? undefined,
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
avatarUrl: user.getAvatarUrl() ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private mapToCoreAuthenticatedUserDTO(apiDto: AuthenticatedUserDTO): CoreAuthenticatedUserDTO {
|
||||
return {
|
||||
id: apiDto.userId,
|
||||
displayName: apiDto.displayName,
|
||||
email: apiDto.email,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,7 +79,8 @@ export class AuthService {
|
||||
|
||||
// Create session after successful signup
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
const coreDto = this.mapToCoreAuthenticatedUserDTO(authenticatedUserDTO);
|
||||
const session = await this.identitySessionPort.createSession(coreDto);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
@@ -99,7 +94,8 @@ export class AuthService {
|
||||
const user = await this.loginUseCase.execute(params.email, params.password);
|
||||
// Create session after successful login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
const coreDto = this.mapToCoreAuthenticatedUserDTO(authenticatedUserDTO);
|
||||
const session = await this.identitySessionPort.createSession(coreDto);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
@@ -111,27 +107,6 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async startIracingAuthRedirect(returnTo?: string): Promise<IracingAuthRedirectResult> {
|
||||
this.logger.debug('[AuthService] Starting iRacing auth redirect.');
|
||||
// Note: The StartIracingAuthRedirectUseCase takes optional returnTo, but the DTO doesnt
|
||||
const result = await this.startIracingAuthRedirectUseCase.execute(returnTo);
|
||||
// Map core IracingAuthRedirectResult to AuthDto's IracingAuthRedirectResult
|
||||
return { redirectUrl: result.redirectUrl, state: result.state };
|
||||
}
|
||||
|
||||
async loginWithIracingCallback(params: LoginWithIracingCallbackParams): Promise<AuthSessionDTO> {
|
||||
this.logger.debug(`[AuthService] Handling iRacing callback for code: ${params.code}`);
|
||||
const user = await this.loginWithIracingCallbackUseCase.execute(params); // Pass params as is
|
||||
|
||||
// Create session after successful iRacing login
|
||||
const authenticatedUserDTO = this.mapUserToAuthenticatedUserDTO(user);
|
||||
const session = await this.identitySessionPort.createSession(authenticatedUserDTO as CoreAuthenticatedUserDTO);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user: authenticatedUserDTO,
|
||||
};
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
this.logger.debug('[AuthService] Attempting logout.');
|
||||
|
||||
45
apps/api/src/domain/dashboard/DashboardController.test.ts
Normal file
45
apps/api/src/domain/dashboard/DashboardController.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { DashboardController } from './DashboardController';
|
||||
import { DashboardService } from './DashboardService';
|
||||
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
|
||||
|
||||
describe('DashboardController', () => {
|
||||
let controller: DashboardController;
|
||||
let mockService: { getDashboardOverview: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockService = {
|
||||
getDashboardOverview: vi.fn(),
|
||||
};
|
||||
|
||||
controller = new DashboardController(mockService as any);
|
||||
});
|
||||
|
||||
describe('getDashboardOverview', () => {
|
||||
it('should call service.getDashboardOverview and return overview', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const overview: DashboardOverviewDTO = {
|
||||
currentDriver: null,
|
||||
myUpcomingRaces: [],
|
||||
otherUpcomingRaces: [],
|
||||
upcomingRaces: [],
|
||||
activeLeaguesCount: 5,
|
||||
nextRace: null,
|
||||
recentResults: [],
|
||||
leagueStandingsSummaries: [],
|
||||
feedSummary: {
|
||||
notificationCount: 0,
|
||||
items: [],
|
||||
},
|
||||
friends: [],
|
||||
};
|
||||
mockService.getDashboardOverview.mockResolvedValue(overview);
|
||||
|
||||
const result = await controller.getDashboardOverview(driverId);
|
||||
|
||||
expect(mockService.getDashboardOverview).toHaveBeenCalledWith(driverId);
|
||||
expect(result).toEqual(overview);
|
||||
});
|
||||
});
|
||||
});
|
||||
30
apps/api/src/domain/dashboard/DashboardModule.test.ts
Normal file
30
apps/api/src/domain/dashboard/DashboardModule.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DashboardModule } from './DashboardModule';
|
||||
import { DashboardController } from './DashboardController';
|
||||
import { DashboardService } from './DashboardService';
|
||||
|
||||
describe('DashboardModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [DashboardModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide DashboardController', () => {
|
||||
const controller = module.get<DashboardController>(DashboardController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(DashboardController);
|
||||
});
|
||||
|
||||
it('should provide DashboardService', () => {
|
||||
const service = module.get<DashboardService>(DashboardService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(DashboardService);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { DashboardProviders } from './DashboardProviders';
|
||||
|
||||
@Module({
|
||||
controllers: [DashboardController],
|
||||
providers: DashboardProviders,
|
||||
providers: [DashboardService, ...DashboardProviders],
|
||||
exports: [DashboardService],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
@@ -3,21 +3,114 @@ import { DashboardService } from './DashboardService';
|
||||
|
||||
// Import core interfaces
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
|
||||
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
|
||||
// Import concrete implementations
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/InMemoryResultRepository';
|
||||
import { InMemoryLeagueRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
|
||||
// Import use cases
|
||||
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
|
||||
// Simple mock implementations for missing adapters
|
||||
class MockFeedRepository implements IFeedRepository {
|
||||
async getFeedForDriver(driverId: string, limit?: number) {
|
||||
return [];
|
||||
}
|
||||
async getGlobalFeed(limit?: number) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class MockSocialGraphRepository implements ISocialGraphRepository {
|
||||
async getFriends(driverId: string) {
|
||||
return [];
|
||||
}
|
||||
async getFriendIds(driverId: string) {
|
||||
return [];
|
||||
}
|
||||
async getSuggestedFriends(driverId: string, limit?: number) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Define injection tokens
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
||||
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
|
||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
||||
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
|
||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
||||
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const FEED_REPOSITORY_TOKEN = 'IFeedRepository';
|
||||
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
|
||||
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
|
||||
|
||||
export const DashboardProviders: Provider[] = [
|
||||
DashboardService,
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
DashboardOverviewUseCase,
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RESULT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryResultRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: STANDING_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger, {}),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueMembershipRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryRaceRegistrationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: FEED_REPOSITORY_TOKEN,
|
||||
useFactory: () => new MockFeedRepository(),
|
||||
},
|
||||
{
|
||||
provide: SOCIAL_GRAPH_REPOSITORY_TOKEN,
|
||||
useFactory: () => new MockSocialGraphRepository(),
|
||||
},
|
||||
{
|
||||
provide: IMAGE_SERVICE_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
@@ -1,28 +1,76 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
|
||||
import type { DashboardOverviewViewModel } from '@core/racing/application/presenters/IDashboardOverviewPresenter';
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './DashboardProviders';
|
||||
import {
|
||||
LOGGER_TOKEN,
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
FEED_REPOSITORY_TOKEN,
|
||||
SOCIAL_GRAPH_REPOSITORY_TOKEN,
|
||||
IMAGE_SERVICE_TOKEN,
|
||||
} from './DashboardProviders';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
constructor(
|
||||
private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
private readonly dashboardOverviewUseCase: DashboardOverviewUseCase;
|
||||
|
||||
async getDashboardOverview(driverId: string): Promise<any> {
|
||||
constructor(
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository?: IDriverRepository,
|
||||
@Inject(RACE_REPOSITORY_TOKEN) private readonly raceRepository?: IRaceRepository,
|
||||
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository?: IResultRepository,
|
||||
@Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository?: ILeagueRepository,
|
||||
@Inject(STANDING_REPOSITORY_TOKEN) private readonly standingRepository?: IStandingRepository,
|
||||
@Inject(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN) private readonly leagueMembershipRepository?: ILeagueMembershipRepository,
|
||||
@Inject(RACE_REGISTRATION_REPOSITORY_TOKEN) private readonly raceRegistrationRepository?: IRaceRegistrationRepository,
|
||||
@Inject(FEED_REPOSITORY_TOKEN) private readonly feedRepository?: IFeedRepository,
|
||||
@Inject(SOCIAL_GRAPH_REPOSITORY_TOKEN) private readonly socialRepository?: ISocialGraphRepository,
|
||||
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService?: IImageServicePort,
|
||||
) {
|
||||
this.dashboardOverviewUseCase = new DashboardOverviewUseCase(
|
||||
driverRepository,
|
||||
raceRepository,
|
||||
resultRepository,
|
||||
leagueRepository,
|
||||
standingRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRegistrationRepository,
|
||||
feedRepository,
|
||||
socialRepository,
|
||||
imageService,
|
||||
() => null, // getDriverStats
|
||||
);
|
||||
}
|
||||
|
||||
async getDashboardOverview(driverId: string): Promise<DashboardOverviewViewModel> {
|
||||
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
|
||||
|
||||
const result = await this.dashboardOverviewUseCase.execute({ driverId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to get dashboard overview');
|
||||
throw new Error(result.error?.message || 'Failed to get dashboard overview');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
return result.value!;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
import { DashboardDriverSummaryDTO } from '../../../race/dtos/DashboardDriverSummaryDTO';
|
||||
import { DashboardRaceSummaryDTO } from '../../../race/dtos/DashboardRaceSummaryDTO';
|
||||
import { DashboardRecentResultDTO } from '../../../race/dtos/DashboardRecentResultDTO';
|
||||
import { DashboardLeagueStandingSummaryDTO } from '../../../race/dtos/DashboardLeagueStandingSummaryDTO';
|
||||
import { DashboardFeedSummaryDTO } from '../../../race/dtos/DashboardFeedSummaryDTO';
|
||||
import { DashboardFriendSummaryDTO } from '../../../race/dtos/DashboardFriendSummaryDTO';
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { DashboardDriverSummaryDTO } from './DashboardDriverSummaryDTO';
|
||||
import { DashboardRaceSummaryDTO } from './DashboardRaceSummaryDTO';
|
||||
import { DashboardRecentResultDTO } from './DashboardRecentResultDTO';
|
||||
import { DashboardLeagueStandingSummaryDTO } from './DashboardLeagueStandingSummaryDTO';
|
||||
import { DashboardFeedSummaryDTO } from './DashboardFeedSummaryDTO';
|
||||
import { DashboardFriendSummaryDTO } from './DashboardFriendSummaryDTO';
|
||||
|
||||
export class DashboardOverviewDTO {
|
||||
@ApiProperty({ nullable: true })
|
||||
|
||||
163
apps/api/src/domain/driver/DriverController.test.ts
Normal file
163
apps/api/src/domain/driver/DriverController.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { DriverController } from './DriverController';
|
||||
import { DriverService } from './DriverService';
|
||||
import type { Request } from 'express';
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: { userId: string };
|
||||
}
|
||||
import { CompleteOnboardingInputDTO } from './dtos/CompleteOnboardingInputDTO';
|
||||
import { CompleteOnboardingOutputDTO } from './dtos/CompleteOnboardingOutputDTO';
|
||||
import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
|
||||
import { DriverStatsDTO } from './dtos/DriverStatsDTO';
|
||||
import { GetDriverOutputDTO } from './dtos/GetDriverOutputDTO';
|
||||
import { GetDriverProfileOutputDTO } from './dtos/GetDriverProfileOutputDTO';
|
||||
import { DriverRegistrationStatusDTO } from './dtos/DriverRegistrationStatusDTO';
|
||||
|
||||
describe('DriverController', () => {
|
||||
let controller: DriverController;
|
||||
let service: ReturnType<typeof vi.mocked<DriverService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [DriverController],
|
||||
providers: [
|
||||
{
|
||||
provide: DriverService,
|
||||
useValue: {
|
||||
getDriversLeaderboard: vi.fn(),
|
||||
getTotalDrivers: vi.fn(),
|
||||
getCurrentDriver: vi.fn(),
|
||||
completeOnboarding: vi.fn(),
|
||||
getDriverRegistrationStatus: vi.fn(),
|
||||
getDriver: vi.fn(),
|
||||
getDriverProfile: vi.fn(),
|
||||
updateDriverProfile: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<DriverController>(DriverController);
|
||||
service = vi.mocked(module.get(DriverService));
|
||||
});
|
||||
|
||||
describe('getDriversLeaderboard', () => {
|
||||
it('should return drivers leaderboard', async () => {
|
||||
const leaderboard: DriversLeaderboardDTO = { items: [] };
|
||||
service.getDriversLeaderboard.mockResolvedValue(leaderboard);
|
||||
|
||||
const result = await controller.getDriversLeaderboard();
|
||||
|
||||
expect(service.getDriversLeaderboard).toHaveBeenCalled();
|
||||
expect(result).toEqual(leaderboard);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalDrivers', () => {
|
||||
it('should return total drivers stats', async () => {
|
||||
const stats: DriverStatsDTO = { totalDrivers: 100 };
|
||||
service.getTotalDrivers.mockResolvedValue(stats);
|
||||
|
||||
const result = await controller.getTotalDrivers();
|
||||
|
||||
expect(service.getTotalDrivers).toHaveBeenCalled();
|
||||
expect(result).toEqual(stats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentDriver', () => {
|
||||
it('should return current driver if userId exists', async () => {
|
||||
const userId = 'user-123';
|
||||
const driver: GetDriverOutputDTO = { id: 'driver-123', name: 'Driver' };
|
||||
service.getCurrentDriver.mockResolvedValue(driver);
|
||||
|
||||
const mockReq: Partial<AuthenticatedRequest> = { user: { userId } };
|
||||
|
||||
const result = await controller.getCurrentDriver(mockReq as AuthenticatedRequest);
|
||||
|
||||
expect(service.getCurrentDriver).toHaveBeenCalledWith(userId);
|
||||
expect(result).toEqual(driver);
|
||||
});
|
||||
|
||||
it('should return null if no userId', async () => {
|
||||
const mockReq: Partial<AuthenticatedRequest> = {};
|
||||
|
||||
const result = await controller.getCurrentDriver(mockReq as AuthenticatedRequest);
|
||||
|
||||
expect(service.getCurrentDriver).not.toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeOnboarding', () => {
|
||||
it('should complete onboarding', async () => {
|
||||
const userId = 'user-123';
|
||||
const input: CompleteOnboardingInputDTO = { someField: 'value' };
|
||||
const output: CompleteOnboardingOutputDTO = { success: true };
|
||||
service.completeOnboarding.mockResolvedValue(output);
|
||||
|
||||
const mockReq: Partial<AuthenticatedRequest> = { user: { userId } };
|
||||
|
||||
const result = await controller.completeOnboarding(input, mockReq as AuthenticatedRequest);
|
||||
|
||||
expect(service.completeOnboarding).toHaveBeenCalledWith(userId, input);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriverRegistrationStatus', () => {
|
||||
it('should return registration status', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const raceId = 'race-456';
|
||||
const status: DriverRegistrationStatusDTO = { registered: true };
|
||||
service.getDriverRegistrationStatus.mockResolvedValue(status);
|
||||
|
||||
const result = await controller.getDriverRegistrationStatus(driverId, raceId);
|
||||
|
||||
expect(service.getDriverRegistrationStatus).toHaveBeenCalledWith({ driverId, raceId });
|
||||
expect(result).toEqual(status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriver', () => {
|
||||
it('should return driver by id', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const driver: GetDriverOutputDTO = { id: driverId, name: 'Driver' };
|
||||
service.getDriver.mockResolvedValue(driver);
|
||||
|
||||
const result = await controller.getDriver(driverId);
|
||||
|
||||
expect(service.getDriver).toHaveBeenCalledWith(driverId);
|
||||
expect(result).toEqual(driver);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriverProfile', () => {
|
||||
it('should return driver profile', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const profile: GetDriverProfileOutputDTO = { id: driverId, bio: 'Bio' };
|
||||
service.getDriverProfile.mockResolvedValue(profile);
|
||||
|
||||
const result = await controller.getDriverProfile(driverId);
|
||||
|
||||
expect(service.getDriverProfile).toHaveBeenCalledWith(driverId);
|
||||
expect(result).toEqual(profile);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDriverProfile', () => {
|
||||
it('should update driver profile', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const body = { bio: 'New bio', country: 'US' };
|
||||
const updated: GetDriverOutputDTO = { id: driverId, name: 'Driver' };
|
||||
service.updateDriverProfile.mockResolvedValue(updated);
|
||||
|
||||
const result = await controller.updateDriverProfile(driverId, body);
|
||||
|
||||
expect(service.updateDriverProfile).toHaveBeenCalledWith(driverId, body.bio, body.country);
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Controller, Get, Post, Body, Req, Param } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: { userId: string };
|
||||
}
|
||||
import { DriverService } from './DriverService';
|
||||
import { DriversLeaderboardDTO } from './dtos/DriversLeaderboardDTO';
|
||||
import { DriverStatsDTO } from './dtos/DriverStatsDTO';
|
||||
@@ -35,9 +39,9 @@ export class DriverController {
|
||||
@ApiOperation({ summary: 'Get current authenticated driver' })
|
||||
@ApiResponse({ status: 200, description: 'Current driver data', type: GetDriverOutputDTO })
|
||||
@ApiResponse({ status: 404, description: 'Driver not found' })
|
||||
async getCurrentDriver(@Req() req: Request): Promise<GetDriverOutputDTO | null> {
|
||||
async getCurrentDriver(@Req() req: AuthenticatedRequest): Promise<GetDriverOutputDTO | null> {
|
||||
// Assuming userId is available from the request (e.g., via auth middleware)
|
||||
const userId = req['user']?.userId;
|
||||
const userId = req.user?.userId;
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
@@ -49,10 +53,10 @@ export class DriverController {
|
||||
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutputDTO })
|
||||
async completeOnboarding(
|
||||
@Body() input: CompleteOnboardingInputDTO,
|
||||
@Req() req: Request,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
): Promise<CompleteOnboardingOutputDTO> {
|
||||
// Assuming userId is available from the request (e.g., via auth middleware)
|
||||
const userId = req['user'].userId; // Placeholder for actual user extraction
|
||||
const userId = req.user!.userId; // Placeholder for actual user extraction
|
||||
return this.driverService.completeOnboarding(userId, input);
|
||||
}
|
||||
|
||||
|
||||
30
apps/api/src/domain/driver/DriverModule.test.ts
Normal file
30
apps/api/src/domain/driver/DriverModule.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DriverModule } from './DriverModule';
|
||||
import { DriverController } from './DriverController';
|
||||
import { DriverService } from './DriverService';
|
||||
|
||||
describe('DriverModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [DriverModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide DriverController', () => {
|
||||
const controller = module.get<DriverController>(DriverController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(DriverController);
|
||||
});
|
||||
|
||||
it('should provide DriverService', () => {
|
||||
const service = module.get<DriverService>(DriverService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(DriverService);
|
||||
});
|
||||
});
|
||||
@@ -9,25 +9,24 @@ import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatin
|
||||
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository';
|
||||
import type { Logger } from "@gridpilot/core/shared/application";
|
||||
import type { Logger } from "@core/shared/application";
|
||||
|
||||
// Import use cases
|
||||
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { GetProfileOverviewUseCase } from '@core/racing/application/use-cases/GetProfileOverviewUseCase';
|
||||
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
|
||||
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryDriverRepository } from '../../..//racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '../../..//racing/services/InMemoryRankingService';
|
||||
import { InMemoryDriverStatsService } from '../../..//racing/services/InMemoryDriverStatsService';
|
||||
import { InMemoryDriverRatingProvider } from '../../..//racing/ports/InMemoryDriverRatingProvider';
|
||||
import { InMemoryImageServiceAdapter } from '../../..//media/ports/InMemoryImageServiceAdapter';
|
||||
import { InMemoryRaceRegistrationRepository } from '../../..//racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryNotificationPreferenceRepository } from '../../..//notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
||||
import { ConsoleLogger } from '../../..//logging/ConsoleLogger';
|
||||
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankingService';
|
||||
import { InMemoryDriverStatsService } from '@adapters/racing/services/InMemoryDriverStatsService';
|
||||
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
|
||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
@@ -44,8 +43,6 @@ export const GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN = 'GetDriversLeaderboardUseC
|
||||
export const GET_TOTAL_DRIVERS_USE_CASE_TOKEN = 'GetTotalDriversUseCase';
|
||||
export const COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN = 'CompleteDriverOnboardingUseCase';
|
||||
export const IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN = 'IsDriverRegisteredForRaceUseCase';
|
||||
export const GET_PROFILE_OVERVIEW_USE_CASE_TOKEN = 'GetProfileOverviewUseCase';
|
||||
export const GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
|
||||
export const UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN = 'UpdateDriverProfileUseCase';
|
||||
|
||||
export const DriverProviders: Provider[] = [
|
||||
@@ -92,14 +89,14 @@ export const DriverProviders: Provider[] = [
|
||||
// Use cases
|
||||
{
|
||||
provide: GET_DRIVERS_LEADERBOARD_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort) =>
|
||||
new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN],
|
||||
useFactory: (driverRepo: IDriverRepository, rankingService: IRankingService, driverStatsService: IDriverStatsService, imageService: IImageServicePort, logger: Logger) =>
|
||||
new GetDriversLeaderboardUseCase(driverRepo, rankingService, driverStatsService, imageService, logger),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN, RANKING_SERVICE_TOKEN, DRIVER_STATS_SERVICE_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_TOTAL_DRIVERS_USE_CASE_TOKEN,
|
||||
useFactory: (driverRepo: IDriverRepository) => new GetTotalDriversUseCase(driverRepo),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN],
|
||||
useFactory: (driverRepo: IDriverRepository, logger: Logger) => new GetTotalDriversUseCase(driverRepo, logger),
|
||||
inject: [DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: COMPLETE_DRIVER_ONBOARDING_USE_CASE_TOKEN,
|
||||
@@ -108,8 +105,8 @@ export const DriverProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: IS_DRIVER_REGISTERED_FOR_RACE_USE_CASE_TOKEN,
|
||||
useFactory: (registrationRepo: IRaceRegistrationRepository) => new IsDriverRegisteredForRaceUseCase(registrationRepo),
|
||||
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN],
|
||||
useFactory: (registrationRepo: IRaceRegistrationRepository, logger: Logger) => new IsDriverRegisteredForRaceUseCase(registrationRepo, logger),
|
||||
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: UPDATE_DRIVER_PROFILE_USE_CASE_TOKEN,
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { DriverService } from './DriverService';
|
||||
import { GetDriversLeaderboardUseCase } from '@core/racing/application/use-cases/GetDriversLeaderboardUseCase';
|
||||
import { GetTotalDriversUseCase } from '@core/racing/application/use-cases/GetTotalDriversUseCase';
|
||||
import { CompleteDriverOnboardingUseCase } from '@core/racing/application/use-cases/CompleteDriverOnboardingUseCase';
|
||||
import { IsDriverRegisteredForRaceUseCase } from '@core/racing/application/use-cases/IsDriverRegisteredForRaceUseCase';
|
||||
import { UpdateDriverProfileUseCase } from '@core/racing/application/use-cases/UpdateDriverProfileUseCase';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
|
||||
describe('DriverService', () => {
|
||||
let service: DriverService;
|
||||
let getDriversLeaderboardUseCase: jest.Mocked<GetDriversLeaderboardUseCase>;
|
||||
let getTotalDriversUseCase: jest.Mocked<GetTotalDriversUseCase>;
|
||||
let completeDriverOnboardingUseCase: jest.Mocked<CompleteDriverOnboardingUseCase>;
|
||||
let isDriverRegisteredForRaceUseCase: jest.Mocked<IsDriverRegisteredForRaceUseCase>;
|
||||
let logger: jest.Mocked<Logger>;
|
||||
let getDriversLeaderboardUseCase: ReturnType<typeof vi.mocked<GetDriversLeaderboardUseCase>>;
|
||||
let getTotalDriversUseCase: ReturnType<typeof vi.mocked<GetTotalDriversUseCase>>;
|
||||
let completeDriverOnboardingUseCase: ReturnType<typeof vi.mocked<CompleteDriverOnboardingUseCase>>;
|
||||
let isDriverRegisteredForRaceUseCase: ReturnType<typeof vi.mocked<IsDriverRegisteredForRaceUseCase>>;
|
||||
let updateDriverProfileUseCase: ReturnType<typeof vi.mocked<UpdateDriverProfileUseCase>>;
|
||||
let driverRepository: ReturnType<typeof vi.mocked<IDriverRepository>>;
|
||||
let logger: ReturnType<typeof vi.mocked<Logger>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -21,42 +26,57 @@ describe('DriverService', () => {
|
||||
{
|
||||
provide: 'GetDriversLeaderboardUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
execute: vi.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'GetTotalDriversUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
execute: vi.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'CompleteDriverOnboardingUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
execute: vi.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'IsDriverRegisteredForRaceUseCase',
|
||||
useValue: {
|
||||
execute: jest.fn(),
|
||||
execute: vi.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'UpdateDriverProfileUseCase',
|
||||
useValue: {
|
||||
execute: vi.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'IDriverRepository',
|
||||
useValue: {
|
||||
findById: vi.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'Logger',
|
||||
useValue: {
|
||||
debug: jest.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DriverService>(DriverService);
|
||||
getDriversLeaderboardUseCase = module.get('GetDriversLeaderboardUseCase');
|
||||
getTotalDriversUseCase = module.get('GetTotalDriversUseCase');
|
||||
completeDriverOnboardingUseCase = module.get('CompleteDriverOnboardingUseCase');
|
||||
isDriverRegisteredForRaceUseCase = module.get('IsDriverRegisteredForRaceUseCase');
|
||||
logger = module.get('Logger');
|
||||
getDriversLeaderboardUseCase = vi.mocked(module.get('GetDriversLeaderboardUseCase'));
|
||||
getTotalDriversUseCase = vi.mocked(module.get('GetTotalDriversUseCase'));
|
||||
completeDriverOnboardingUseCase = vi.mocked(module.get('CompleteDriverOnboardingUseCase'));
|
||||
isDriverRegisteredForRaceUseCase = vi.mocked(module.get('IsDriverRegisteredForRaceUseCase'));
|
||||
updateDriverProfileUseCase = vi.mocked(module.get('UpdateDriverProfileUseCase'));
|
||||
driverRepository = vi.mocked(module.get('IDriverRepository'));
|
||||
logger = vi.mocked(module.get('Logger'));
|
||||
});
|
||||
|
||||
describe('getDriversLeaderboard', () => {
|
||||
|
||||
@@ -143,7 +143,12 @@ export class DriverProfileSocialSummaryDTO {
|
||||
|
||||
export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
|
||||
|
||||
export type DriverProfileAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
|
||||
export enum DriverProfileAchievementRarity {
|
||||
COMMON = 'common',
|
||||
RARE = 'rare',
|
||||
EPIC = 'epic',
|
||||
LEGENDARY = 'legendary',
|
||||
}
|
||||
|
||||
export class DriverProfileAchievementDTO {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,37 +1,34 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
|
||||
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
|
||||
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
|
||||
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
|
||||
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
|
||||
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
||||
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
|
||||
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
|
||||
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
|
||||
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
|
||||
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
|
||||
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
|
||||
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
|
||||
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
|
||||
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
|
||||
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
|
||||
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
|
||||
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
|
||||
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
|
||||
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
|
||||
import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
|
||||
import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
|
||||
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
|
||||
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
|
||||
import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
|
||||
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
|
||||
import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
|
||||
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
|
||||
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
|
||||
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
|
||||
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
|
||||
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
|
||||
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
|
||||
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
|
||||
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
|
||||
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
|
||||
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
||||
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
|
||||
import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
|
||||
import { GetLeagueJoinRequestsQueryDTO } from './dtos/GetLeagueJoinRequestsQueryDTO';
|
||||
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
|
||||
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
|
||||
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
|
||||
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
|
||||
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
|
||||
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
|
||||
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
|
||||
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
|
||||
|
||||
@ApiTags('leagues')
|
||||
@Controller('leagues')
|
||||
@@ -104,8 +101,7 @@ export class LeagueController {
|
||||
async removeLeagueMember(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('performerDriverId') performerDriverId: string,
|
||||
@Param('targetDriverId') targetDriverId: string,
|
||||
@Body() input: RemoveLeagueMemberInputDTO, // Body content for a patch often includes IDs
|
||||
@Param('targetDriverId') targetDriverId: string, // Body content for a patch often includes IDs
|
||||
): Promise<RemoveLeagueMemberOutputDTO> {
|
||||
return this.leagueService.removeLeagueMember({ leagueId, performerDriverId, targetDriverId });
|
||||
}
|
||||
@@ -133,27 +129,27 @@ export class LeagueController {
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('ownerId') ownerId: string,
|
||||
): Promise<LeagueOwnerSummaryDTO | null> {
|
||||
const query: GetLeagueOwnerSummaryQuery = { ownerId, leagueId };
|
||||
const query: GetLeagueOwnerSummaryQueryDTO = { ownerId, leagueId };
|
||||
return this.leagueService.getLeagueOwnerSummary(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/config')
|
||||
@ApiOperation({ summary: 'Get league full configuration' })
|
||||
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDTO })
|
||||
async getLeagueFullConfig(
|
||||
@Param('leagueId') leagueId: string,
|
||||
): Promise<LeagueConfigFormModelDTO | null> {
|
||||
const query: GetLeagueAdminConfigQuery = { leagueId };
|
||||
return this.leagueService.getLeagueFullConfig(query);
|
||||
}
|
||||
@ApiOperation({ summary: 'Get league full configuration' })
|
||||
@ApiResponse({ status: 200, description: 'League configuration form model', type: LeagueConfigFormModelDTO })
|
||||
async getLeagueFullConfig(
|
||||
@Param('leagueId') leagueId: string,
|
||||
): Promise<LeagueConfigFormModelDTO | null> {
|
||||
const query: GetLeagueAdminConfigQueryDTO = { leagueId };
|
||||
return this.leagueService.getLeagueFullConfig(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/protests')
|
||||
@ApiOperation({ summary: 'Get protests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsDTO })
|
||||
async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsDTO> {
|
||||
const query: GetLeagueProtestsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueProtests(query);
|
||||
}
|
||||
@ApiOperation({ summary: 'Get protests for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of protests for the league', type: LeagueAdminProtestsDTO })
|
||||
async getLeagueProtests(@Param('leagueId') leagueId: string): Promise<LeagueAdminProtestsDTO> {
|
||||
const query: GetLeagueProtestsQueryDTO = { leagueId };
|
||||
return this.leagueService.getLeagueProtests(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/protests/:protestId')
|
||||
@ApiOperation({ summary: 'Get a specific protest for a league' })
|
||||
@@ -162,7 +158,7 @@ export class LeagueController {
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Param('protestId') protestId: string,
|
||||
): Promise<LeagueAdminProtestsDTO> {
|
||||
const query: GetLeagueProtestsQuery = { leagueId };
|
||||
const query: GetLeagueProtestsQueryDTO = { leagueId };
|
||||
const allProtests = await this.leagueService.getLeagueProtests(query);
|
||||
|
||||
// Filter to only include the specific protest
|
||||
@@ -187,12 +183,12 @@ export class LeagueController {
|
||||
}
|
||||
|
||||
@Get(':leagueId/seasons')
|
||||
@ApiOperation({ summary: 'Get seasons for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryDTO] })
|
||||
async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
const query: GetLeagueSeasonsQuery = { leagueId };
|
||||
return this.leagueService.getLeagueSeasons(query);
|
||||
}
|
||||
@ApiOperation({ summary: 'Get seasons for a league' })
|
||||
@ApiResponse({ status: 200, description: 'List of seasons for the league', type: [LeagueSeasonSummaryDTO] })
|
||||
async getLeagueSeasons(@Param('leagueId') leagueId: string): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
const query: GetLeagueSeasonsQueryDTO = { leagueId };
|
||||
return this.leagueService.getLeagueSeasons(query);
|
||||
}
|
||||
|
||||
@Get(':leagueId/memberships')
|
||||
@ApiOperation({ summary: 'Get league memberships' })
|
||||
|
||||
@@ -19,7 +19,8 @@ import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
|
||||
// Import use cases
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { GetLeagueStandingsUseCase } from '@core/league/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { GetLeagueStandingsUseCaseImpl } from '@core/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||
@@ -49,6 +50,7 @@ export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
|
||||
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
|
||||
|
||||
export const LeagueProviders: Provider[] = [
|
||||
LeagueService, // Provide the service itself
|
||||
@@ -108,7 +110,10 @@ export const LeagueProviders: Provider[] = [
|
||||
},
|
||||
// Use cases
|
||||
GetAllLeaguesWithCapacityUseCase,
|
||||
GetLeagueStandingsUseCase,
|
||||
{
|
||||
provide: GET_LEAGUE_STANDINGS_USE_CASE,
|
||||
useClass: GetLeagueStandingsUseCaseImpl,
|
||||
},
|
||||
GetLeagueStatsUseCase,
|
||||
GetLeagueFullConfigUseCase,
|
||||
CreateLeagueWithSeasonAndScoringUseCase,
|
||||
|
||||
@@ -1,81 +1,111 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
|
||||
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
|
||||
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
|
||||
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
|
||||
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
|
||||
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
||||
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
|
||||
import { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
|
||||
import { TransferLeagueOwnershipOutputDTO } from './dtos/TransferLeagueOwnershipOutputDTO';
|
||||
import { LeagueJoinRequestWithDriverDTO } from './dtos/LeagueJoinRequestWithDriverDTO';
|
||||
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
|
||||
import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
|
||||
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
|
||||
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
|
||||
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
|
||||
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
|
||||
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
|
||||
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
|
||||
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
|
||||
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
|
||||
import { LeagueJoinRequestDTO } from './dtos/LeagueJoinRequestDTO';
|
||||
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
|
||||
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
|
||||
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
|
||||
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
|
||||
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
|
||||
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
|
||||
import { RejectJoinRequestInputDTO } from './dtos/RejectJoinRequestInputDTO';
|
||||
import { RejectJoinRequestOutputDTO } from './dtos/RejectJoinRequestOutputDTO';
|
||||
import { LeagueAdminPermissionsDTO } from './dtos/LeagueAdminPermissionsDTO';
|
||||
import { RemoveLeagueMemberInputDTO } from './dtos/RemoveLeagueMemberInputDTO';
|
||||
import { RemoveLeagueMemberOutputDTO } from './dtos/RemoveLeagueMemberOutputDTO';
|
||||
import { UpdateLeagueMemberRoleInputDTO } from './dtos/UpdateLeagueMemberRoleInputDTO';
|
||||
import { UpdateLeagueMemberRoleOutputDTO } from './dtos/UpdateLeagueMemberRoleOutputDTO';
|
||||
import { LeagueOwnerSummaryDTO } from './dtos/LeagueOwnerSummaryDTO';
|
||||
import { LeagueConfigFormModelDTO } from './dtos/LeagueConfigFormModelDTO';
|
||||
import { LeagueAdminProtestsDTO } from './dtos/LeagueAdminProtestsDTO';
|
||||
import { LeagueSeasonSummaryDTO } from './dtos/LeagueSeasonSummaryDTO';
|
||||
import { GetLeagueAdminPermissionsInputDTO } from './dtos/GetLeagueAdminPermissionsInputDTO';
|
||||
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
|
||||
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
|
||||
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
|
||||
import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQueryDTO';
|
||||
import { LeagueMembershipsDTO } from './dtos/LeagueMembershipsDTO';
|
||||
import { LeagueStandingsDTO } from './dtos/LeagueStandingsDTO';
|
||||
import { LeagueScheduleDTO } from './dtos/LeagueScheduleDTO';
|
||||
import { LeagueStatsDTO } from './dtos/LeagueStatsDTO';
|
||||
import { LeagueAdminDTO } from './dtos/LeagueAdminDTO';
|
||||
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
||||
import { CreateLeagueOutputDTO } from './dtos/CreateLeagueOutputDTO';
|
||||
import { GetSeasonSponsorshipsOutputDTO } from './dtos/GetSeasonSponsorshipsOutputDTO';
|
||||
import { GetLeagueRacesOutputDTO } from './dtos/GetLeagueRacesOutputDTO';
|
||||
|
||||
// Core imports for entities
|
||||
import type { League } from '@core/racing/domain/entities/League';
|
||||
|
||||
// Core imports for view models
|
||||
import type { LeagueScoringConfigViewModel } from '@core/racing/application/presenters/ILeagueScoringConfigPresenter';
|
||||
import type { LeagueScoringPresetsViewModel } from '@core/racing/application/presenters/ILeagueScoringPresetsPresenter';
|
||||
import type { AllLeaguesWithCapacityViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityPresenter';
|
||||
import type { GetTotalLeaguesViewModel } from '@core/racing/application/presenters/IGetTotalLeaguesPresenter';
|
||||
import type { GetLeagueJoinRequestsViewModel } from '@core/racing/application/presenters/IGetLeagueJoinRequestsPresenter';
|
||||
import type { ApproveLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IApproveLeagueJoinRequestPresenter';
|
||||
import type { RejectLeagueJoinRequestViewModel } from '@core/racing/application/presenters/IRejectLeagueJoinRequestPresenter';
|
||||
import type { GetLeagueAdminPermissionsViewModel } from '@core/racing/application/presenters/IGetLeagueAdminPermissionsPresenter';
|
||||
import type { RemoveLeagueMemberViewModel } from '@core/racing/application/presenters/IRemoveLeagueMemberPresenter';
|
||||
import type { UpdateLeagueMemberRoleViewModel } from '@core/racing/application/presenters/IUpdateLeagueMemberRolePresenter';
|
||||
import type { GetLeagueOwnerSummaryViewModel } from '@core/racing/application/presenters/IGetLeagueOwnerSummaryPresenter';
|
||||
import type { GetLeagueProtestsViewModel } from '@core/racing/application/presenters/IGetLeagueProtestsPresenter';
|
||||
import type { GetLeagueSeasonsViewModel } from '@core/racing/application/presenters/IGetLeagueSeasonsPresenter';
|
||||
import type { GetLeagueMembershipsViewModel } from '@core/racing/application/presenters/IGetLeagueMembershipsPresenter';
|
||||
import type { LeagueStandingsViewModel } from '@core/racing/application/presenters/ILeagueStandingsPresenter';
|
||||
import type { LeagueScheduleViewModel } from '@core/racing/application/presenters/ILeagueSchedulePresenter';
|
||||
import type { LeagueStatsViewModel } from '@core/racing/application/presenters/ILeagueStatsPresenter';
|
||||
import type { LeagueConfigFormViewModel } from '@core/racing/application/presenters/ILeagueFullConfigPresenter';
|
||||
import type { CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter';
|
||||
import type { JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter';
|
||||
import type { TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter';
|
||||
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
// Use cases
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import { GetLeagueStandingsUseCase } from '@core/racing/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import { GetLeagueScoringConfigUseCase } from '@core/racing/application/use-cases/GetLeagueScoringConfigUseCase';
|
||||
import { ListLeagueScoringPresetsUseCase } from '@core/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
|
||||
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
|
||||
import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
|
||||
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
||||
import { GetLeagueStandingsUseCase } from '@core/league/application/use-cases/GetLeagueStandingsUseCase';
|
||||
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
|
||||
import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
|
||||
import { RemoveLeagueMemberUseCase } from '@core/racing/application/use-cases/RemoveLeagueMemberUseCase';
|
||||
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
||||
import { GetLeagueMembershipsUseCase } from '@core/racing/application/use-cases/GetLeagueMembershipsUseCase';
|
||||
import { GetLeagueOwnerSummaryUseCase } from '@core/racing/application/use-cases/GetLeagueOwnerSummaryUseCase';
|
||||
import { GetLeagueProtestsUseCase } from '@core/racing/application/use-cases/GetLeagueProtestsUseCase';
|
||||
import { GetLeagueSeasonsUseCase } from '@core/racing/application/use-cases/GetLeagueSeasonsUseCase';
|
||||
import { GetLeagueMembershipsUseCase } from '@core/racing/application/use-cases/GetLeagueMembershipsUseCase';
|
||||
import { GetLeagueScheduleUseCase } from '@core/racing/application/use-cases/GetLeagueScheduleUseCase';
|
||||
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
||||
import { GetLeagueScoringConfigUseCase } from '@core/racing/application/use-cases/GetLeagueScoringConfigUseCase';
|
||||
import { GetLeagueSeasonsUseCase } from '@core/racing/application/use-cases/GetLeagueSeasonsUseCase';
|
||||
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
|
||||
import { GetRaceProtestsUseCase } from '@core/racing/application/use-cases/GetRaceProtestsUseCase';
|
||||
import { GetTotalLeaguesUseCase } from '@core/racing/application/use-cases/GetTotalLeaguesUseCase';
|
||||
import { JoinLeagueUseCase } from '@core/racing/application/use-cases/JoinLeagueUseCase';
|
||||
import { ListLeagueScoringPresetsUseCase } from '@core/racing/application/use-cases/ListLeagueScoringPresetsUseCase';
|
||||
import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
|
||||
import { RemoveLeagueMemberUseCase } from '@core/racing/application/use-cases/RemoveLeagueMemberUseCase';
|
||||
import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
|
||||
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
|
||||
|
||||
// API Presenters
|
||||
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
|
||||
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
||||
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter';
|
||||
import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter';
|
||||
import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter';
|
||||
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
|
||||
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter';
|
||||
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter';
|
||||
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter';
|
||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter';
|
||||
import { GetLeagueOwnerSummaryPresenter } from './presenters/GetLeagueOwnerSummaryPresenter';
|
||||
import { GetLeagueProtestsPresenter } from './presenters/GetLeagueProtestsPresenter';
|
||||
import { GetLeagueSeasonsPresenter } from './presenters/GetLeagueSeasonsPresenter';
|
||||
import { GetLeagueMembershipsPresenter } from './presenters/GetLeagueMembershipsPresenter';
|
||||
import { LeagueJoinRequestsPresenter } from './presenters/LeagueJoinRequestsPresenter';
|
||||
import { LeagueSchedulePresenter } from './presenters/LeagueSchedulePresenter';
|
||||
import { TotalLeaguesPresenter } from './presenters/TotalLeaguesPresenter';
|
||||
import { LeagueConfigPresenter } from './presenters/LeagueConfigPresenter';
|
||||
import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter';
|
||||
import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter';
|
||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||
import { RejectLeagueJoinRequestPresenter } from './presenters/RejectLeagueJoinRequestPresenter';
|
||||
import { RemoveLeagueMemberPresenter } from './presenters/RemoveLeagueMemberPresenter';
|
||||
import { UpdateLeagueMemberRolePresenter } from './presenters/UpdateLeagueMemberRolePresenter';
|
||||
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
||||
import { JoinLeaguePresenter } from './presenters/JoinLeaguePresenter';
|
||||
import { TransferLeagueOwnershipPresenter } from './presenters/TransferLeagueOwnershipPresenter';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './LeagueProviders';
|
||||
@@ -111,127 +141,173 @@ export class LeagueService {
|
||||
async getAllLeaguesWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||
this.logger.debug('[LeagueService] Fetching all leagues with capacity.');
|
||||
|
||||
const result = await this.getAllLeaguesWithCapacityUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new AllLeaguesWithCapacityPresenter();
|
||||
await this.getAllLeaguesWithCapacityUseCase.execute(undefined, presenter);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getTotalLeagues(): Promise<LeagueStatsDto> {
|
||||
async getTotalLeagues(): Promise<GetTotalLeaguesViewModel> {
|
||||
this.logger.debug('[LeagueService] Fetching total leagues count.');
|
||||
const result = await this.getTotalLeaguesUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new TotalLeaguesPresenter();
|
||||
await this.getTotalLeaguesUseCase.execute({}, presenter);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||
async getLeagueJoinRequests(leagueId: string): Promise<GetLeagueJoinRequestsViewModel> {
|
||||
this.logger.debug(`[LeagueService] Fetching join requests for league: ${leagueId}.`);
|
||||
const result = await this.getLeagueJoinRequestsUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new LeagueJoinRequestsPresenter();
|
||||
await this.getLeagueJoinRequestsUseCase.execute({ leagueId }, presenter);
|
||||
return presenter.getViewModel()!.joinRequests;
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInput): Promise<ApproveJoinRequestOutput> {
|
||||
async approveLeagueJoinRequest(input: ApproveJoinRequestInputDTO): Promise<ApproveLeagueJoinRequestViewModel> {
|
||||
this.logger.debug('Approving join request:', input);
|
||||
const result = await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new ApproveLeagueJoinRequestPresenter();
|
||||
await this.approveLeagueJoinRequestUseCase.execute({ leagueId: input.leagueId, requestId: input.requestId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInput): Promise<RejectJoinRequestOutput> {
|
||||
async rejectLeagueJoinRequest(input: RejectJoinRequestInputDTO): Promise<RejectLeagueJoinRequestViewModel> {
|
||||
this.logger.debug('Rejecting join request:', input);
|
||||
const result = await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new RejectLeagueJoinRequestPresenter();
|
||||
await this.rejectLeagueJoinRequestUseCase.execute({ requestId: input.requestId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInput): Promise<LeagueAdminPermissionsViewModel> {
|
||||
async getLeagueAdminPermissions(query: GetLeagueAdminPermissionsInputDTO): Promise<GetLeagueAdminPermissionsViewModel> {
|
||||
this.logger.debug('Getting league admin permissions', { query });
|
||||
const result = await this.getLeagueAdminPermissionsUseCase.execute({ leagueId: query.leagueId, performerDriverId: query.performerDriverId });
|
||||
// This use case never errors
|
||||
const presenter = new GetLeagueAdminPermissionsPresenter();
|
||||
await this.getLeagueAdminPermissionsUseCase.execute(
|
||||
{ leagueId: query.leagueId, performerDriverId: query.performerDriverId },
|
||||
presenter
|
||||
);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async removeLeagueMember(input: RemoveLeagueMemberInput): Promise<RemoveLeagueMemberOutput> {
|
||||
async removeLeagueMember(input: RemoveLeagueMemberInputDTO): Promise<RemoveLeagueMemberViewModel> {
|
||||
this.logger.debug('Removing league member', { leagueId: input.leagueId, targetDriverId: input.targetDriverId });
|
||||
const result = await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new RemoveLeagueMemberPresenter();
|
||||
await this.removeLeagueMemberUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInput): Promise<UpdateLeagueMemberRoleOutput> {
|
||||
async updateLeagueMemberRole(input: UpdateLeagueMemberRoleInputDTO): Promise<UpdateLeagueMemberRoleViewModel> {
|
||||
this.logger.debug('Updating league member role', { leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
|
||||
const result = await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new UpdateLeagueMemberRolePresenter();
|
||||
await this.updateLeagueMemberRoleUseCase.execute({ leagueId: input.leagueId, targetDriverId: input.targetDriverId, newRole: input.newRole }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQuery): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||
async getLeagueOwnerSummary(query: GetLeagueOwnerSummaryQueryDTO): Promise<GetLeagueOwnerSummaryViewModel> {
|
||||
this.logger.debug('Getting league owner summary:', query);
|
||||
const result = await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new GetLeagueOwnerSummaryPresenter();
|
||||
await this.getLeagueOwnerSummaryUseCase.execute({ ownerId: query.ownerId }, presenter);
|
||||
return presenter.getViewModel()!.summary;
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQuery): Promise<LeagueConfigFormModelDto | null> {
|
||||
async getLeagueFullConfig(query: GetLeagueAdminConfigQueryDTO): Promise<LeagueConfigFormViewModel | null> {
|
||||
this.logger.debug('Getting league full config', { query });
|
||||
|
||||
const presenter = new LeagueConfigPresenter();
|
||||
try {
|
||||
await this.getLeagueFullConfigUseCase.execute({ leagueId: query.leagueId }, presenter);
|
||||
return presenter.viewModel;
|
||||
const result = await this.getLeagueFullConfigUseCase.execute({ leagueId: query.leagueId });
|
||||
if (result.isErr()) {
|
||||
this.logger.error('Error getting league full config', new Error(result.unwrapErr().code));
|
||||
return null;
|
||||
}
|
||||
return result.unwrap();
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error)));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getLeagueProtests(query: GetLeagueProtestsQuery): Promise<LeagueAdminProtestsViewModel> {
|
||||
async getLeagueProtests(query: GetLeagueProtestsQueryDTO): Promise<LeagueAdminProtestsDTO> {
|
||||
this.logger.debug('Getting league protests:', query);
|
||||
const presenter = new GetLeagueProtestsPresenter();
|
||||
await this.getLeagueProtestsUseCase.execute({ leagueId: query.leagueId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
const result = await this.getLeagueProtestsUseCase.execute({ leagueId: query.leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQuery): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||
async getLeagueSeasons(query: GetLeagueSeasonsQueryDTO): Promise<LeagueSeasonSummaryDTO[]> {
|
||||
this.logger.debug('Getting league seasons:', query);
|
||||
const presenter = new GetLeagueSeasonsPresenter();
|
||||
await this.getLeagueSeasonsUseCase.execute({ leagueId: query.leagueId }, presenter);
|
||||
return presenter.getViewModel()!.seasons;
|
||||
const result = await this.getLeagueSeasonsUseCase.execute({ leagueId: query.leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
return result.unwrap().seasons;
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsViewModel> {
|
||||
async getLeagueMemberships(leagueId: string): Promise<LeagueMembershipsDTO> {
|
||||
this.logger.debug('Getting league memberships', { leagueId });
|
||||
const presenter = new GetLeagueMembershipsPresenter();
|
||||
await this.getLeagueMembershipsUseCase.execute({ leagueId }, presenter);
|
||||
return presenter.apiViewModel!;
|
||||
const result = await this.getLeagueMembershipsUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
async getLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> {
|
||||
this.logger.debug('Getting league standings', { leagueId });
|
||||
|
||||
const presenter = new LeagueStandingsPresenter();
|
||||
await this.getLeagueStandingsUseCase.execute({ leagueId }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
return await this.getLeagueStandingsUseCase.execute(leagueId);
|
||||
}
|
||||
|
||||
async getLeagueSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
|
||||
this.logger.debug('Getting league schedule', { leagueId });
|
||||
const result = await this.getLeagueScheduleUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new LeagueSchedulePresenter();
|
||||
await this.getLeagueScheduleUseCase.execute({ leagueId }, presenter);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getLeagueStats(leagueId: string): Promise<LeagueStatsViewModel> {
|
||||
this.logger.debug('Getting league stats', { leagueId });
|
||||
const result = await this.getLeagueStatsUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new LeagueStatsPresenter();
|
||||
await this.getLeagueStatsUseCase.execute({ leagueId }, presenter);
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminViewModel> {
|
||||
async getLeagueAdmin(leagueId: string): Promise<LeagueAdminDTO> {
|
||||
this.logger.debug('Getting league admin data', { leagueId });
|
||||
// For now, we'll keep the orchestration in the service since it combines multiple use cases
|
||||
// TODO: Create a composite use case that handles all the admin data fetching
|
||||
@@ -253,7 +329,7 @@ export class LeagueService {
|
||||
};
|
||||
}
|
||||
|
||||
async createLeague(input: CreateLeagueInput): Promise<CreateLeagueOutput> {
|
||||
async createLeague(input: CreateLeagueInputDTO): Promise<CreateLeagueViewModel> {
|
||||
this.logger.debug('Creating league', { input });
|
||||
const command = {
|
||||
name: input.name,
|
||||
@@ -268,10 +344,12 @@ export class LeagueService {
|
||||
enableTrophyChampionship: false,
|
||||
};
|
||||
const result = await this.createLeagueWithSeasonAndScoringUseCase.execute(command);
|
||||
return {
|
||||
leagueId: result.leagueId,
|
||||
success: true,
|
||||
};
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
const presenter = new CreateLeaguePresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async getLeagueScoringConfig(leagueId: string): Promise<LeagueScoringConfigViewModel | null> {
|
||||
@@ -281,10 +359,10 @@ export class LeagueService {
|
||||
try {
|
||||
const result = await this.getLeagueScoringConfigUseCase.execute({ leagueId });
|
||||
if (result.isErr()) {
|
||||
this.logger.error('Error getting league scoring config', result.error);
|
||||
this.logger.error('Error getting league scoring config', new Error(result.unwrapErr().code));
|
||||
return null;
|
||||
}
|
||||
await presenter.present(result.value);
|
||||
await presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting league scoring config', error instanceof Error ? error : new Error(String(error)));
|
||||
@@ -295,40 +373,46 @@ export class LeagueService {
|
||||
async listLeagueScoringPresets(): Promise<LeagueScoringPresetsViewModel> {
|
||||
this.logger.debug('Listing league scoring presets');
|
||||
|
||||
const result = await this.listLeagueScoringPresetsUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
const presenter = new LeagueScoringPresetsPresenter();
|
||||
await this.listLeagueScoringPresetsUseCase.execute(undefined, presenter);
|
||||
await presenter.present(result.unwrap());
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async joinLeague(leagueId: string, driverId: string): Promise<JoinLeagueOutput> {
|
||||
async joinLeague(leagueId: string, driverId: string): Promise<JoinLeagueViewModel> {
|
||||
this.logger.debug('Joining league', { leagueId, driverId });
|
||||
|
||||
const result = await this.joinLeagueUseCase.execute({ leagueId, driverId });
|
||||
if (result.isErr()) {
|
||||
const error = result.unwrapErr();
|
||||
return {
|
||||
success: false,
|
||||
error: result.error.code,
|
||||
error: error.code,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
membershipId: result.value.id,
|
||||
};
|
||||
const presenter = new JoinLeaguePresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<TransferLeagueOwnershipOutput> {
|
||||
async transferLeagueOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<TransferLeagueOwnershipViewModel> {
|
||||
this.logger.debug('Transferring league ownership', { leagueId, currentOwnerId, newOwnerId });
|
||||
|
||||
const result = await this.transferLeagueOwnershipUseCase.execute({ leagueId, currentOwnerId, newOwnerId });
|
||||
if (result.isErr()) {
|
||||
const error = result.unwrapErr();
|
||||
return {
|
||||
success: false,
|
||||
error: result.error.code,
|
||||
error: error.code,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
const presenter = new TransferLeagueOwnershipPresenter();
|
||||
presenter.present(result.unwrap());
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async getSeasonSponsorships(seasonId: string): Promise<GetSeasonSponsorshipsOutputDTO> {
|
||||
|
||||
5
apps/api/src/domain/league/dtos/JoinLeagueOutputDTO.ts
Normal file
5
apps/api/src/domain/league/dtos/JoinLeagueOutputDTO.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface JoinLeagueOutputDTO {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
membershipId?: string;
|
||||
}
|
||||
@@ -23,9 +23,9 @@ export class LeagueConfigFormModelDTO {
|
||||
@Type(() => LeagueConfigFormModelStructureDTO)
|
||||
structure: LeagueConfigFormModelStructureDTO;
|
||||
|
||||
@ApiProperty({ type: [Object] })
|
||||
@ApiProperty({ type: [Object] })
|
||||
@IsArray()
|
||||
championships: any[];
|
||||
championships: Object[];
|
||||
|
||||
@ApiProperty({ type: LeagueConfigFormModelScoringDTO })
|
||||
@ValidateNested()
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface LeagueJoinRequestWithDriverDTO {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
driverId: string;
|
||||
requestedAt: Date;
|
||||
message?: string;
|
||||
driver: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface TransferLeagueOwnershipOutputDTO {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ICreateLeaguePresenter, CreateLeagueResultDTO, CreateLeagueViewModel } from '@core/racing/application/presenters/ICreateLeaguePresenter';
|
||||
|
||||
export class CreateLeaguePresenter implements ICreateLeaguePresenter {
|
||||
private result: CreateLeagueViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CreateLeagueResultDTO): void {
|
||||
this.result = {
|
||||
leagueId: dto.leagueId,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): CreateLeagueViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
21
apps/api/src/domain/league/presenters/JoinLeaguePresenter.ts
Normal file
21
apps/api/src/domain/league/presenters/JoinLeaguePresenter.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IJoinLeaguePresenter, JoinLeagueResultDTO, JoinLeagueViewModel } from '@core/racing/application/presenters/IJoinLeaguePresenter';
|
||||
|
||||
export class JoinLeaguePresenter implements IJoinLeaguePresenter {
|
||||
private result: JoinLeagueViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: JoinLeagueResultDTO): void {
|
||||
this.result = {
|
||||
success: true,
|
||||
membershipId: dto.id,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): JoinLeagueViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ export class LeagueAdminPresenter {
|
||||
}
|
||||
|
||||
present(data: {
|
||||
joinRequests: any[];
|
||||
ownerSummary: any;
|
||||
config: any;
|
||||
protests: any;
|
||||
seasons: any[];
|
||||
joinRequests: unknown[];
|
||||
ownerSummary: unknown;
|
||||
config: unknown;
|
||||
protests: unknown;
|
||||
seasons: unknown[];
|
||||
}) {
|
||||
this.result = {
|
||||
joinRequests: data.joinRequests,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ITransferLeagueOwnershipPresenter, TransferLeagueOwnershipResultDTO, TransferLeagueOwnershipViewModel } from '@core/racing/application/presenters/ITransferLeagueOwnershipPresenter';
|
||||
|
||||
export class TransferLeagueOwnershipPresenter implements ITransferLeagueOwnershipPresenter {
|
||||
private result: TransferLeagueOwnershipViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TransferLeagueOwnershipResultDTO): void {
|
||||
this.result = {
|
||||
success: dto.success,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TransferLeagueOwnershipViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
181
apps/api/src/domain/media/MediaController.test.ts
Normal file
181
apps/api/src/domain/media/MediaController.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { MediaController } from './MediaController';
|
||||
import { MediaService } from './MediaService';
|
||||
import type { Response } from 'express';
|
||||
import { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
|
||||
import { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO';
|
||||
|
||||
describe('MediaController', () => {
|
||||
let controller: MediaController;
|
||||
let service: ReturnType<typeof vi.mocked<MediaService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [MediaController],
|
||||
providers: [
|
||||
{
|
||||
provide: MediaService,
|
||||
useValue: {
|
||||
requestAvatarGeneration: vi.fn(),
|
||||
uploadMedia: vi.fn(),
|
||||
getMedia: vi.fn(),
|
||||
deleteMedia: vi.fn(),
|
||||
getAvatar: vi.fn(),
|
||||
updateAvatar: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<MediaController>(MediaController);
|
||||
service = vi.mocked(module.get(MediaService));
|
||||
});
|
||||
|
||||
describe('requestAvatarGeneration', () => {
|
||||
it('should request avatar generation and return 201 on success', async () => {
|
||||
const input: RequestAvatarGenerationInputDTO = { driverId: 'driver-123' };
|
||||
const result = { success: true, jobId: 'job-123' };
|
||||
service.requestAvatarGeneration.mockResolvedValue(result);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.requestAvatarGeneration(input, mockRes);
|
||||
|
||||
expect(service.requestAvatarGeneration).toHaveBeenCalledWith(input);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(result);
|
||||
});
|
||||
|
||||
it('should return 400 on failure', async () => {
|
||||
const input: RequestAvatarGenerationInputDTO = { driverId: 'driver-123' };
|
||||
const result = { success: false, error: 'Error' };
|
||||
service.requestAvatarGeneration.mockResolvedValue(result);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.requestAvatarGeneration(input, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadMedia', () => {
|
||||
it('should upload media and return 201 on success', async () => {
|
||||
const file: Express.Multer.File = { filename: 'file.jpg' } as Express.Multer.File;
|
||||
const input: UploadMediaInputDTO = { type: 'image' };
|
||||
const result = { success: true, mediaId: 'media-123' };
|
||||
service.uploadMedia.mockResolvedValue(result);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.uploadMedia(file, input, mockRes);
|
||||
|
||||
expect(service.uploadMedia).toHaveBeenCalledWith({ ...input, file });
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMedia', () => {
|
||||
it('should return media if found', async () => {
|
||||
const mediaId = 'media-123';
|
||||
const result = { id: mediaId, url: 'url' };
|
||||
service.getMedia.mockResolvedValue(result);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.getMedia(mediaId, mockRes);
|
||||
|
||||
expect(service.getMedia).toHaveBeenCalledWith(mediaId);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(result);
|
||||
});
|
||||
|
||||
it('should return 404 if not found', async () => {
|
||||
const mediaId = 'media-123';
|
||||
service.getMedia.mockResolvedValue(null);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.getMedia(mediaId, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(404);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Media not found' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteMedia', () => {
|
||||
it('should delete media', async () => {
|
||||
const mediaId = 'media-123';
|
||||
const result = { success: true };
|
||||
service.deleteMedia.mockResolvedValue(result);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.deleteMedia(mediaId, mockRes);
|
||||
|
||||
expect(service.deleteMedia).toHaveBeenCalledWith(mediaId);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvatar', () => {
|
||||
it('should return avatar if found', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const result = { url: 'avatar.jpg' };
|
||||
service.getAvatar.mockResolvedValue(result);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.getAvatar(driverId, mockRes);
|
||||
|
||||
expect(service.getAvatar).toHaveBeenCalledWith(driverId);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAvatar', () => {
|
||||
it('should update avatar', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const input = { url: 'new-avatar.jpg' };
|
||||
const result = { success: true };
|
||||
service.updateAvatar.mockResolvedValue(result);
|
||||
|
||||
const mockRes: ReturnType<typeof vi.mocked<Response>> = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn(),
|
||||
} as unknown as ReturnType<typeof vi.mocked<Response>>;
|
||||
|
||||
await controller.updateAvatar(driverId, input, mockRes);
|
||||
|
||||
expect(service.updateAvatar).toHaveBeenCalledWith(driverId, input);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,25 +3,19 @@ import { ApiTags, ApiResponse, ApiOperation, ApiParam, ApiConsumes } from '@nest
|
||||
import { Response } from 'express';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { MediaService } from './MediaService';
|
||||
import type { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
|
||||
import type { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO';
|
||||
import type { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO';
|
||||
import type { UploadMediaOutputDTO } from './dtos/UploadMediaOutputDTO';
|
||||
import type { GetMediaOutputDTO } from './dtos/GetMediaOutputDTO';
|
||||
import type { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO';
|
||||
import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO';
|
||||
import type { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO';
|
||||
import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO';
|
||||
import { RequestAvatarGenerationInputDTO } from './dtos/RequestAvatarGenerationInputDTO';
|
||||
import { RequestAvatarGenerationOutputDTO } from './dtos/RequestAvatarGenerationOutputDTO';
|
||||
import { UploadMediaInputDTO } from './dtos/UploadMediaInputDTO';
|
||||
import { UploadMediaOutputDTO } from './dtos/UploadMediaOutputDTO';
|
||||
import { GetMediaOutputDTO } from './dtos/GetMediaOutputDTO';
|
||||
import { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO';
|
||||
import { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO';
|
||||
import { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO';
|
||||
import { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO';
|
||||
|
||||
type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
|
||||
type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
|
||||
type UploadMediaInput = UploadMediaInputDTO;
|
||||
type UploadMediaOutput = UploadMediaOutputDTO;
|
||||
type GetMediaOutput = GetMediaOutputDTO;
|
||||
type DeleteMediaOutput = DeleteMediaOutputDTO;
|
||||
type GetAvatarOutput = GetAvatarOutputDTO;
|
||||
type UpdateAvatarInput = UpdateAvatarInputDTO;
|
||||
type UpdateAvatarOutput = UpdateAvatarOutputDTO;
|
||||
|
||||
@ApiTags('media')
|
||||
@Controller('media')
|
||||
@@ -30,7 +24,7 @@ export class MediaController {
|
||||
|
||||
@Post('avatar/generate')
|
||||
@ApiOperation({ summary: 'Request avatar generation' })
|
||||
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutput })
|
||||
@ApiResponse({ status: 201, description: 'Avatar generation request submitted', type: RequestAvatarGenerationOutputDTO })
|
||||
async requestAvatarGeneration(
|
||||
@Body() input: RequestAvatarGenerationInput,
|
||||
@Res() res: Response,
|
||||
@@ -47,7 +41,7 @@ export class MediaController {
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiOperation({ summary: 'Upload media file' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiResponse({ status: 201, description: 'Media uploaded successfully', type: UploadMediaOutput })
|
||||
@ApiResponse({ status: 201, description: 'Media uploaded successfully', type: UploadMediaOutputDTO })
|
||||
async uploadMedia(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body() input: UploadMediaInput,
|
||||
@@ -64,7 +58,7 @@ export class MediaController {
|
||||
@Get(':mediaId')
|
||||
@ApiOperation({ summary: 'Get media by ID' })
|
||||
@ApiParam({ name: 'mediaId', description: 'Media ID' })
|
||||
@ApiResponse({ status: 200, description: 'Media details', type: GetMediaOutput })
|
||||
@ApiResponse({ status: 200, description: 'Media details', type: GetMediaOutputDTO })
|
||||
async getMedia(
|
||||
@Param('mediaId') mediaId: string,
|
||||
@Res() res: Response,
|
||||
@@ -80,7 +74,7 @@ export class MediaController {
|
||||
@Delete(':mediaId')
|
||||
@ApiOperation({ summary: 'Delete media by ID' })
|
||||
@ApiParam({ name: 'mediaId', description: 'Media ID' })
|
||||
@ApiResponse({ status: 200, description: 'Media deleted', type: DeleteMediaOutput })
|
||||
@ApiResponse({ status: 200, description: 'Media deleted', type: DeleteMediaOutputDTO })
|
||||
async deleteMedia(
|
||||
@Param('mediaId') mediaId: string,
|
||||
@Res() res: Response,
|
||||
@@ -92,7 +86,7 @@ export class MediaController {
|
||||
@Get('avatar/:driverId')
|
||||
@ApiOperation({ summary: 'Get avatar for driver' })
|
||||
@ApiParam({ name: 'driverId', description: 'Driver ID' })
|
||||
@ApiResponse({ status: 200, description: 'Avatar details', type: GetAvatarOutput })
|
||||
@ApiResponse({ status: 200, description: 'Avatar details', type: GetAvatarOutputDTO })
|
||||
async getAvatar(
|
||||
@Param('driverId') driverId: string,
|
||||
@Res() res: Response,
|
||||
@@ -108,7 +102,7 @@ export class MediaController {
|
||||
@Put('avatar/:driverId')
|
||||
@ApiOperation({ summary: 'Update avatar for driver' })
|
||||
@ApiParam({ name: 'driverId', description: 'Driver ID' })
|
||||
@ApiResponse({ status: 200, description: 'Avatar updated', type: UpdateAvatarOutput })
|
||||
@ApiResponse({ status: 200, description: 'Avatar updated', type: UpdateAvatarOutputDTO })
|
||||
async updateAvatar(
|
||||
@Param('driverId') driverId: string,
|
||||
@Body() input: UpdateAvatarInput,
|
||||
|
||||
30
apps/api/src/domain/media/MediaModule.test.ts
Normal file
30
apps/api/src/domain/media/MediaModule.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MediaModule } from './MediaModule';
|
||||
import { MediaController } from './MediaController';
|
||||
import { MediaService } from './MediaService';
|
||||
|
||||
describe('MediaModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [MediaModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide MediaController', () => {
|
||||
const controller = module.get<MediaController>(MediaController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(MediaController);
|
||||
});
|
||||
|
||||
it('should provide MediaService', () => {
|
||||
const service = module.get<MediaService>(MediaService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(MediaService);
|
||||
});
|
||||
});
|
||||
@@ -3,39 +3,82 @@ import { MediaService } from './MediaService';
|
||||
|
||||
// Import core interfaces
|
||||
import { IAvatarGenerationRepository } from '@core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { IMediaRepository } from '@core/media/domain/repositories/IMediaRepository';
|
||||
import { IAvatarRepository } from '@core/media/domain/repositories/IAvatarRepository';
|
||||
import { FaceValidationPort } from '@core/media/application/ports/FaceValidationPort';
|
||||
import { AvatarGenerationPort } from '@core/media/application/ports/AvatarGenerationPort';
|
||||
import { MediaStoragePort } from '@core/media/application/ports/MediaStoragePort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
// Import use cases
|
||||
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
|
||||
import { UploadMediaUseCase } from '@core/media/application/use-cases/UploadMediaUseCase';
|
||||
import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCase';
|
||||
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
|
||||
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
|
||||
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
|
||||
|
||||
// Define injection tokens
|
||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
||||
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
|
||||
export const AVATAR_REPOSITORY_TOKEN = 'IAvatarRepository';
|
||||
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
|
||||
export const AVATAR_GENERATION_PORT_TOKEN = 'AvatarGenerationPort';
|
||||
export const MEDIA_STORAGE_PORT_TOKEN = 'MediaStoragePort';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
|
||||
// Use case tokens
|
||||
export const REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN = 'RequestAvatarGenerationUseCase';
|
||||
export const UPLOAD_MEDIA_USE_CASE_TOKEN = 'UploadMediaUseCase';
|
||||
export const GET_MEDIA_USE_CASE_TOKEN = 'GetMediaUseCase';
|
||||
export const DELETE_MEDIA_USE_CASE_TOKEN = 'DeleteMediaUseCase';
|
||||
export const GET_AVATAR_USE_CASE_TOKEN = 'GetAvatarUseCase';
|
||||
export const UPDATE_AVATAR_USE_CASE_TOKEN = 'UpdateAvatarUseCase';
|
||||
|
||||
import type { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
|
||||
import type { Media } from '@core/media/domain/entities/Media';
|
||||
import type { Avatar } from '@core/media/domain/entities/Avatar';
|
||||
import type { FaceValidationResult } from '@core/media/application/ports/FaceValidationPort';
|
||||
import type { AvatarGenerationResult } from '@core/media/application/ports/AvatarGenerationPort';
|
||||
import type { UploadResult } from '@core/media/application/ports/MediaStoragePort';
|
||||
|
||||
// Mock implementations
|
||||
class MockAvatarGenerationRepository implements IAvatarGenerationRepository {
|
||||
async save(_request: any): Promise<void> {}
|
||||
async findById(_id: string): Promise<any | null> { return null; }
|
||||
async findByUserId(_userId: string): Promise<any[]> { return []; }
|
||||
async findLatestByUserId(_userId: string): Promise<any | null> { return null; }
|
||||
async delete(_id: string): Promise<void> {}
|
||||
async save(): Promise<void> {}
|
||||
async findById(): Promise<AvatarGenerationRequest | null> { return null; }
|
||||
async findByUserId(): Promise<AvatarGenerationRequest[]> { return []; }
|
||||
async findLatestByUserId(): Promise<AvatarGenerationRequest | null> { return null; }
|
||||
async delete(): Promise<void> {}
|
||||
}
|
||||
|
||||
class MockMediaRepository implements IMediaRepository {
|
||||
async save(): Promise<void> {}
|
||||
async findById(): Promise<Media | null> { return null; }
|
||||
async findByUploadedBy(): Promise<Media[]> { return []; }
|
||||
async delete(): Promise<void> {}
|
||||
}
|
||||
|
||||
class MockAvatarRepository implements IAvatarRepository {
|
||||
async save(): Promise<void> {}
|
||||
async findById(): Promise<Avatar | null> { return null; }
|
||||
async findActiveByDriverId(): Promise<Avatar | null> { return null; }
|
||||
async findByDriverId(): Promise<Avatar[]> { return []; }
|
||||
async delete(): Promise<void> {}
|
||||
}
|
||||
|
||||
class MockFaceValidationAdapter implements FaceValidationPort {
|
||||
async validateFacePhoto(data: string): Promise<any> {
|
||||
return { isValid: true, hasFace: true, faceCount: 1 };
|
||||
async validateFacePhoto(): Promise<FaceValidationResult> {
|
||||
return {
|
||||
isValid: true,
|
||||
hasFace: true,
|
||||
faceCount: 1,
|
||||
confidence: 0.95,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MockAvatarGenerationAdapter implements AvatarGenerationPort {
|
||||
async generateAvatars(options: any): Promise<any> {
|
||||
async generateAvatars(): Promise<AvatarGenerationResult> {
|
||||
return {
|
||||
success: true,
|
||||
avatars: [
|
||||
@@ -47,11 +90,22 @@ class MockAvatarGenerationAdapter implements AvatarGenerationPort {
|
||||
}
|
||||
}
|
||||
|
||||
class MockMediaStorageAdapter implements MediaStoragePort {
|
||||
async uploadMedia(): Promise<UploadResult> {
|
||||
return {
|
||||
success: true,
|
||||
url: 'https://cdn.example.com/media/mock-file.png',
|
||||
filename: 'mock-file.png',
|
||||
};
|
||||
}
|
||||
async deleteMedia(): Promise<void> {}
|
||||
}
|
||||
|
||||
class MockLogger implements Logger {
|
||||
debug(message: string, meta?: any): void {}
|
||||
info(message: string, meta?: any): void {}
|
||||
warn(message: string, meta?: any): void {}
|
||||
error(message: string, error?: Error): void {}
|
||||
debug(): void {}
|
||||
info(): void {}
|
||||
warn(): void {}
|
||||
error(): void {}
|
||||
}
|
||||
|
||||
export const MediaProviders: Provider[] = [
|
||||
@@ -60,6 +114,14 @@ export const MediaProviders: Provider[] = [
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useClass: MockAvatarGenerationRepository,
|
||||
},
|
||||
{
|
||||
provide: MEDIA_REPOSITORY_TOKEN,
|
||||
useClass: MockMediaRepository,
|
||||
},
|
||||
{
|
||||
provide: AVATAR_REPOSITORY_TOKEN,
|
||||
useClass: MockAvatarRepository,
|
||||
},
|
||||
{
|
||||
provide: FACE_VALIDATION_PORT_TOKEN,
|
||||
useClass: MockFaceValidationAdapter,
|
||||
@@ -68,6 +130,10 @@ export const MediaProviders: Provider[] = [
|
||||
provide: AVATAR_GENERATION_PORT_TOKEN,
|
||||
useClass: MockAvatarGenerationAdapter,
|
||||
},
|
||||
{
|
||||
provide: MEDIA_STORAGE_PORT_TOKEN,
|
||||
useClass: MockMediaStorageAdapter,
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: MockLogger,
|
||||
@@ -79,4 +145,34 @@ export const MediaProviders: Provider[] = [
|
||||
new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger),
|
||||
inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: UPLOAD_MEDIA_USE_CASE_TOKEN,
|
||||
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
|
||||
new UploadMediaUseCase(mediaRepo, mediaStorage, logger),
|
||||
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_MEDIA_USE_CASE_TOKEN,
|
||||
useFactory: (mediaRepo: IMediaRepository, logger: Logger) =>
|
||||
new GetMediaUseCase(mediaRepo, logger),
|
||||
inject: [MEDIA_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DELETE_MEDIA_USE_CASE_TOKEN,
|
||||
useFactory: (mediaRepo: IMediaRepository, mediaStorage: MediaStoragePort, logger: Logger) =>
|
||||
new DeleteMediaUseCase(mediaRepo, mediaStorage, logger),
|
||||
inject: [MEDIA_REPOSITORY_TOKEN, MEDIA_STORAGE_PORT_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_AVATAR_USE_CASE_TOKEN,
|
||||
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
|
||||
new GetAvatarUseCase(avatarRepo, logger),
|
||||
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: UPDATE_AVATAR_USE_CASE_TOKEN,
|
||||
useFactory: (avatarRepo: IAvatarRepository, logger: Logger) =>
|
||||
new UpdateAvatarUseCase(avatarRepo, logger),
|
||||
inject: [AVATAR_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { DeleteMediaOutputDTO } from './dtos/DeleteMediaOutputDTO';
|
||||
import type { GetAvatarOutputDTO } from './dtos/GetAvatarOutputDTO';
|
||||
import type { UpdateAvatarInputDTO } from './dtos/UpdateAvatarInputDTO';
|
||||
import type { UpdateAvatarOutputDTO } from './dtos/UpdateAvatarOutputDTO';
|
||||
import type { RacingSuitColor } from '@core/media/domain/types/AvatarGenerationRequest';
|
||||
|
||||
type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
|
||||
type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
|
||||
@@ -21,18 +22,41 @@ type UpdateAvatarOutput = UpdateAvatarOutputDTO;
|
||||
|
||||
// Use cases
|
||||
import { RequestAvatarGenerationUseCase } from '@core/media/application/use-cases/RequestAvatarGenerationUseCase';
|
||||
import { UploadMediaUseCase } from '@core/media/application/use-cases/UploadMediaUseCase';
|
||||
import { GetMediaUseCase } from '@core/media/application/use-cases/GetMediaUseCase';
|
||||
import { DeleteMediaUseCase } from '@core/media/application/use-cases/DeleteMediaUseCase';
|
||||
import { GetAvatarUseCase } from '@core/media/application/use-cases/GetAvatarUseCase';
|
||||
import { UpdateAvatarUseCase } from '@core/media/application/use-cases/UpdateAvatarUseCase';
|
||||
|
||||
// Presenters
|
||||
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
|
||||
import { UploadMediaPresenter } from './presenters/UploadMediaPresenter';
|
||||
import { GetMediaPresenter } from './presenters/GetMediaPresenter';
|
||||
import { DeleteMediaPresenter } from './presenters/DeleteMediaPresenter';
|
||||
import { GetAvatarPresenter } from './presenters/GetAvatarPresenter';
|
||||
import { UpdateAvatarPresenter } from './presenters/UpdateAvatarPresenter';
|
||||
|
||||
// Tokens
|
||||
import { REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, LOGGER_TOKEN } from './MediaProviders';
|
||||
import {
|
||||
REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN,
|
||||
UPLOAD_MEDIA_USE_CASE_TOKEN,
|
||||
GET_MEDIA_USE_CASE_TOKEN,
|
||||
DELETE_MEDIA_USE_CASE_TOKEN,
|
||||
GET_AVATAR_USE_CASE_TOKEN,
|
||||
UPDATE_AVATAR_USE_CASE_TOKEN,
|
||||
LOGGER_TOKEN
|
||||
} from './MediaProviders';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
constructor(
|
||||
@Inject(REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN) private readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase,
|
||||
@Inject(UPLOAD_MEDIA_USE_CASE_TOKEN) private readonly uploadMediaUseCase: UploadMediaUseCase,
|
||||
@Inject(GET_MEDIA_USE_CASE_TOKEN) private readonly getMediaUseCase: GetMediaUseCase,
|
||||
@Inject(DELETE_MEDIA_USE_CASE_TOKEN) private readonly deleteMediaUseCase: DeleteMediaUseCase,
|
||||
@Inject(GET_AVATAR_USE_CASE_TOKEN) private readonly getAvatarUseCase: GetAvatarUseCase,
|
||||
@Inject(UPDATE_AVATAR_USE_CASE_TOKEN) private readonly updateAvatarUseCase: UpdateAvatarUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -43,46 +67,118 @@ export class MediaService {
|
||||
await this.requestAvatarGenerationUseCase.execute({
|
||||
userId: input.userId,
|
||||
facePhotoData: input.facePhotoData,
|
||||
suitColor: input.suitColor as any,
|
||||
suitColor: input.suitColor as RacingSuitColor,
|
||||
}, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async uploadMedia(input: UploadMediaInput & { file: Express.Multer.File }): Promise<UploadMediaOutput> {
|
||||
this.logger.debug('[MediaService] Uploading media.');
|
||||
// TODO: Implement media upload logic
|
||||
return {
|
||||
success: true,
|
||||
mediaId: 'placeholder-media-id',
|
||||
url: 'placeholder-url',
|
||||
};
|
||||
|
||||
const presenter = new UploadMediaPresenter();
|
||||
|
||||
await this.uploadMediaUseCase.execute({
|
||||
file: input.file,
|
||||
uploadedBy: input.userId, // Assuming userId is the uploader
|
||||
metadata: input.metadata,
|
||||
}, presenter);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
mediaId: result.mediaId!,
|
||||
url: result.url!,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: result.errorMessage || 'Upload failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getMedia(mediaId: string): Promise<GetMediaOutput | null> {
|
||||
this.logger.debug(`[MediaService] Getting media: ${mediaId}`);
|
||||
// TODO: Implement get media logic
|
||||
|
||||
const presenter = new GetMediaPresenter();
|
||||
|
||||
await this.getMediaUseCase.execute({ mediaId }, presenter);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
if (result.success && result.media) {
|
||||
return {
|
||||
success: true,
|
||||
mediaId: result.media.id,
|
||||
filename: result.media.filename,
|
||||
originalName: result.media.originalName,
|
||||
mimeType: result.media.mimeType,
|
||||
size: result.media.size,
|
||||
url: result.media.url,
|
||||
type: result.media.type,
|
||||
uploadedBy: result.media.uploadedBy,
|
||||
uploadedAt: result.media.uploadedAt,
|
||||
metadata: result.media.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteMedia(mediaId: string): Promise<DeleteMediaOutput> {
|
||||
this.logger.debug(`[MediaService] Deleting media: ${mediaId}`);
|
||||
// TODO: Implement delete media logic
|
||||
|
||||
const presenter = new DeleteMediaPresenter();
|
||||
|
||||
await this.deleteMediaUseCase.execute({ mediaId }, presenter);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
success: result.success,
|
||||
errorMessage: result.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
async getAvatar(driverId: string): Promise<GetAvatarOutput | null> {
|
||||
this.logger.debug(`[MediaService] Getting avatar for driver: ${driverId}`);
|
||||
// TODO: Implement get avatar logic
|
||||
|
||||
const presenter = new GetAvatarPresenter();
|
||||
|
||||
await this.getAvatarUseCase.execute({ driverId }, presenter);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
if (result.success && result.avatar) {
|
||||
return {
|
||||
success: true,
|
||||
avatarId: result.avatar.id,
|
||||
driverId: result.avatar.driverId,
|
||||
mediaUrl: result.avatar.mediaUrl,
|
||||
selectedAt: result.avatar.selectedAt,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async updateAvatar(driverId: string, input: UpdateAvatarInput): Promise<UpdateAvatarOutput> {
|
||||
this.logger.debug(`[MediaService] Updating avatar for driver: ${driverId}`);
|
||||
// TODO: Implement update avatar logic
|
||||
|
||||
const presenter = new UpdateAvatarPresenter();
|
||||
|
||||
await this.updateAvatarUseCase.execute({
|
||||
driverId,
|
||||
mediaUrl: input.mediaUrl,
|
||||
}, presenter);
|
||||
|
||||
const result = presenter.viewModel;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
success: result.success,
|
||||
errorMessage: result.errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IsString, IsOptional } from 'class-validator';
|
||||
|
||||
export class UploadMediaInputDTO {
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
file: any; // File upload handled by multer
|
||||
file: Express.Multer.File;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
|
||||
14
apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts
Normal file
14
apps/api/src/domain/media/presenters/DeleteMediaPresenter.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IDeleteMediaPresenter, DeleteMediaResult } from '@core/media/application/presenters/IDeleteMediaPresenter';
|
||||
|
||||
export class DeleteMediaPresenter implements IDeleteMediaPresenter {
|
||||
private result: DeleteMediaResult | null = null;
|
||||
|
||||
present(result: DeleteMediaResult) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
get viewModel(): DeleteMediaResult {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
14
apps/api/src/domain/media/presenters/GetAvatarPresenter.ts
Normal file
14
apps/api/src/domain/media/presenters/GetAvatarPresenter.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IGetAvatarPresenter, GetAvatarResult } from '@core/media/application/presenters/IGetAvatarPresenter';
|
||||
|
||||
export class GetAvatarPresenter implements IGetAvatarPresenter {
|
||||
private result: GetAvatarResult | null = null;
|
||||
|
||||
present(result: GetAvatarResult) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
get viewModel(): GetAvatarResult {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
14
apps/api/src/domain/media/presenters/GetMediaPresenter.ts
Normal file
14
apps/api/src/domain/media/presenters/GetMediaPresenter.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IGetMediaPresenter, GetMediaResult } from '@core/media/application/presenters/IGetMediaPresenter';
|
||||
|
||||
export class GetMediaPresenter implements IGetMediaPresenter {
|
||||
private result: GetMediaResult | null = null;
|
||||
|
||||
present(result: GetMediaResult) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
get viewModel(): GetMediaResult {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { RequestAvatarGenerationOutput } from '../dto/MediaDto';
|
||||
import type { RequestAvatarGenerationOutputDTO } from '../dtos/RequestAvatarGenerationOutputDTO';
|
||||
import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '@core/media/application/presenters/IRequestAvatarGenerationPresenter';
|
||||
|
||||
type RequestAvatarGenerationOutput = RequestAvatarGenerationOutputDTO;
|
||||
|
||||
export class RequestAvatarGenerationPresenter implements IRequestAvatarGenerationPresenter {
|
||||
private result: RequestAvatarGenerationOutput | null = null;
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { IUpdateAvatarPresenter, UpdateAvatarResult } from '@core/media/application/presenters/IUpdateAvatarPresenter';
|
||||
|
||||
export class UpdateAvatarPresenter implements IUpdateAvatarPresenter {
|
||||
private result: UpdateAvatarResult | null = null;
|
||||
|
||||
present(result: UpdateAvatarResult) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
get viewModel(): UpdateAvatarResult {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
14
apps/api/src/domain/media/presenters/UploadMediaPresenter.ts
Normal file
14
apps/api/src/domain/media/presenters/UploadMediaPresenter.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { IUploadMediaPresenter, UploadMediaResult } from '@core/media/application/presenters/IUploadMediaPresenter';
|
||||
|
||||
export class UploadMediaPresenter implements IUploadMediaPresenter {
|
||||
private result: UploadMediaResult | null = null;
|
||||
|
||||
present(result: UploadMediaResult) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
get viewModel(): UploadMediaResult {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
194
apps/api/src/domain/payments/PaymentsController.test.ts
Normal file
194
apps/api/src/domain/payments/PaymentsController.test.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { PaymentsController } from './PaymentsController';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
import { GetPaymentsQuery, CreatePaymentInput, UpdatePaymentStatusInput, GetMembershipFeesQuery, UpsertMembershipFeeInput, UpdateMemberPaymentInput, GetPrizesQuery, CreatePrizeInput, AwardPrizeInput, DeletePrizeInput, GetWalletQuery, ProcessWalletTransactionInput } from './dtos/PaymentsDto';
|
||||
|
||||
describe('PaymentsController', () => {
|
||||
let controller: PaymentsController;
|
||||
let service: ReturnType<typeof vi.mocked<PaymentsService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PaymentsController],
|
||||
providers: [
|
||||
{
|
||||
provide: PaymentsService,
|
||||
useValue: {
|
||||
getPayments: vi.fn(),
|
||||
createPayment: vi.fn(),
|
||||
updatePaymentStatus: vi.fn(),
|
||||
getMembershipFees: vi.fn(),
|
||||
upsertMembershipFee: vi.fn(),
|
||||
updateMemberPayment: vi.fn(),
|
||||
getPrizes: vi.fn(),
|
||||
createPrize: vi.fn(),
|
||||
awardPrize: vi.fn(),
|
||||
deletePrize: vi.fn(),
|
||||
getWallet: vi.fn(),
|
||||
processWalletTransaction: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<PaymentsController>(PaymentsController);
|
||||
service = vi.mocked(module.get(PaymentsService));
|
||||
});
|
||||
|
||||
describe('getPayments', () => {
|
||||
it('should return payments', async () => {
|
||||
const query: GetPaymentsQuery = { status: 'pending' };
|
||||
const result = { payments: [] };
|
||||
service.getPayments.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getPayments(query);
|
||||
|
||||
expect(service.getPayments).toHaveBeenCalledWith(query);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPayment', () => {
|
||||
it('should create payment', async () => {
|
||||
const input: CreatePaymentInput = { amount: 100, type: 'membership_fee', payerId: 'payer-123', payerType: 'driver', leagueId: 'league-123' };
|
||||
const result = { payment: { id: 'pay-123' } };
|
||||
service.createPayment.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.createPayment(input);
|
||||
|
||||
expect(service.createPayment).toHaveBeenCalledWith(input);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePaymentStatus', () => {
|
||||
it('should update payment status', async () => {
|
||||
const input: UpdatePaymentStatusInput = { paymentId: 'pay-123', status: 'completed' };
|
||||
const result = { payment: { id: 'pay-123', status: 'completed' } };
|
||||
service.updatePaymentStatus.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.updatePaymentStatus(input);
|
||||
|
||||
expect(service.updatePaymentStatus).toHaveBeenCalledWith(input);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMembershipFees', () => {
|
||||
it('should return membership fees', async () => {
|
||||
const query: GetMembershipFeesQuery = { leagueId: 'league-123' };
|
||||
const result = { fees: [] };
|
||||
service.getMembershipFees.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getMembershipFees(query);
|
||||
|
||||
expect(service.getMembershipFees).toHaveBeenCalledWith(query);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertMembershipFee', () => {
|
||||
it('should upsert membership fee', async () => {
|
||||
const input: UpsertMembershipFeeInput = { leagueId: 'league-123', amount: 50 };
|
||||
const result = { feeId: 'fee-123' };
|
||||
service.upsertMembershipFee.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.upsertMembershipFee(input);
|
||||
|
||||
expect(service.upsertMembershipFee).toHaveBeenCalledWith(input);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMemberPayment', () => {
|
||||
it('should update member payment', async () => {
|
||||
const input: UpdateMemberPaymentInput = { memberId: 'member-123', paymentId: 'pay-123' };
|
||||
const result = { success: true };
|
||||
service.updateMemberPayment.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.updateMemberPayment(input);
|
||||
|
||||
expect(service.updateMemberPayment).toHaveBeenCalledWith(input);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrizes', () => {
|
||||
it('should return prizes', async () => {
|
||||
const query: GetPrizesQuery = { leagueId: 'league-123' };
|
||||
const result = { prizes: [] };
|
||||
service.getPrizes.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getPrizes(query);
|
||||
|
||||
expect(service.getPrizes).toHaveBeenCalledWith(query);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPrize', () => {
|
||||
it('should create prize', async () => {
|
||||
const input: CreatePrizeInput = { name: 'Prize', amount: 100 };
|
||||
const result = { prizeId: 'prize-123' };
|
||||
service.createPrize.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.createPrize(input);
|
||||
|
||||
expect(service.createPrize).toHaveBeenCalledWith(input);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awardPrize', () => {
|
||||
it('should award prize', async () => {
|
||||
const input: AwardPrizeInput = { prizeId: 'prize-123', driverId: 'driver-123' };
|
||||
const result = { success: true };
|
||||
service.awardPrize.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.awardPrize(input);
|
||||
|
||||
expect(service.awardPrize).toHaveBeenCalledWith(input);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deletePrize', () => {
|
||||
it('should delete prize', async () => {
|
||||
const query: DeletePrizeInput = { prizeId: 'prize-123' };
|
||||
const result = { success: true };
|
||||
service.deletePrize.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.deletePrize(query);
|
||||
|
||||
expect(service.deletePrize).toHaveBeenCalledWith(query);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWallet', () => {
|
||||
it('should return wallet', async () => {
|
||||
const query: GetWalletQuery = { userId: 'user-123' };
|
||||
const result = { balance: 100 };
|
||||
service.getWallet.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getWallet(query);
|
||||
|
||||
expect(service.getWallet).toHaveBeenCalledWith(query);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processWalletTransaction', () => {
|
||||
it('should process wallet transaction', async () => {
|
||||
const input: ProcessWalletTransactionInput = { userId: 'user-123', amount: 50, type: 'deposit' };
|
||||
const result = { transactionId: 'tx-123' };
|
||||
service.processWalletTransaction.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.processWalletTransaction(input);
|
||||
|
||||
expect(service.processWalletTransaction).toHaveBeenCalledWith(input);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
30
apps/api/src/domain/payments/PaymentsModule.test.ts
Normal file
30
apps/api/src/domain/payments/PaymentsModule.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PaymentsModule } from './PaymentsModule';
|
||||
import { PaymentsController } from './PaymentsController';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
|
||||
describe('PaymentsModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [PaymentsModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide PaymentsController', () => {
|
||||
const controller = module.get<PaymentsController>(PaymentsController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(PaymentsController);
|
||||
});
|
||||
|
||||
it('should provide PaymentsService', () => {
|
||||
const service = module.get<PaymentsService>(PaymentsService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(PaymentsService);
|
||||
});
|
||||
});
|
||||
@@ -23,10 +23,10 @@ import { GetWalletUseCase } from '@core/payments/application/use-cases/GetWallet
|
||||
import { ProcessWalletTransactionUseCase } from '@core/payments/application/use-cases/ProcessWalletTransactionUseCase';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryPaymentRepository } from '/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
import { InMemoryMembershipFeeRepository, InMemoryMemberPaymentRepository } from '/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
|
||||
import { InMemoryPrizeRepository } from '/payments/persistence/inmemory/InMemoryPrizeRepository';
|
||||
import { InMemoryWalletRepository, InMemoryTransactionRepository } from '/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||
import { InMemoryPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
import { InMemoryMembershipFeeRepository, InMemoryMemberPaymentRepository } from '@adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
|
||||
import { InMemoryPrizeRepository } from '@adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
|
||||
import { InMemoryWalletRepository, InMemoryTransactionRepository } from '@adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
|
||||
// Repository injection tokens
|
||||
|
||||
39
apps/api/src/domain/protests/ProtestsController.test.ts
Normal file
39
apps/api/src/domain/protests/ProtestsController.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { ProtestsController } from './ProtestsController';
|
||||
import { RaceService } from '../race/RaceService';
|
||||
import { ReviewProtestCommandDTO } from '../race/dtos/ReviewProtestCommandDTO';
|
||||
|
||||
describe('ProtestsController', () => {
|
||||
let controller: ProtestsController;
|
||||
let raceService: ReturnType<typeof vi.mocked<RaceService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ProtestsController],
|
||||
providers: [
|
||||
{
|
||||
provide: RaceService,
|
||||
useValue: {
|
||||
reviewProtest: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<ProtestsController>(ProtestsController);
|
||||
raceService = vi.mocked(module.get(RaceService));
|
||||
});
|
||||
|
||||
describe('reviewProtest', () => {
|
||||
it('should review protest', async () => {
|
||||
const protestId = 'protest-123';
|
||||
const body: Omit<ReviewProtestCommandDTO, 'protestId'> = { decision: 'upheld', reason: 'Reason' };
|
||||
raceService.reviewProtest.mockResolvedValue(undefined);
|
||||
|
||||
await controller.reviewProtest(protestId, body);
|
||||
|
||||
expect(raceService.reviewProtest).toHaveBeenCalledWith({ protestId, ...body });
|
||||
});
|
||||
});
|
||||
});
|
||||
23
apps/api/src/domain/protests/ProtestsModule.test.ts
Normal file
23
apps/api/src/domain/protests/ProtestsModule.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ProtestsModule } from './ProtestsModule';
|
||||
import { ProtestsController } from './ProtestsController';
|
||||
|
||||
describe('ProtestsModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [ProtestsModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide ProtestsController', () => {
|
||||
const controller = module.get<ProtestsController>(ProtestsController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(ProtestsController);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProtestsController } from './ProtestsController';
|
||||
import { RaceModule } from '../race/RaceModule';
|
||||
import { ProtestsService } from './ProtestsService';
|
||||
import { ProtestsProviders } from './ProtestsProviders';
|
||||
|
||||
@Module({
|
||||
imports: [RaceModule],
|
||||
providers: [ProtestsService, ...ProtestsProviders],
|
||||
controllers: [ProtestsController],
|
||||
})
|
||||
export class ProtestsModule {}
|
||||
45
apps/api/src/domain/protests/ProtestsProviders.ts
Normal file
45
apps/api/src/domain/protests/ProtestsProviders.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { ProtestsService } from './ProtestsService';
|
||||
|
||||
// Import core interfaces
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
|
||||
// Import use cases
|
||||
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
|
||||
|
||||
// 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 ProtestsProviders: Provider[] = [
|
||||
ProtestsService, // Provide the service itself
|
||||
{
|
||||
provide: PROTEST_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryProtestRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueMembershipRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Use cases
|
||||
ReviewProtestUseCase,
|
||||
];
|
||||
31
apps/api/src/domain/protests/ProtestsService.ts
Normal file
31
apps/api/src/domain/protests/ProtestsService.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
// Use cases
|
||||
import { ReviewProtestUseCase } from '@core/racing/application/use-cases/ReviewProtestUseCase';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './ProtestsProviders';
|
||||
|
||||
@Injectable()
|
||||
export class ProtestsService {
|
||||
constructor(
|
||||
private readonly reviewProtestUseCase: ReviewProtestUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async reviewProtest(command: {
|
||||
protestId: string;
|
||||
stewardId: string;
|
||||
decision: 'uphold' | 'dismiss';
|
||||
decisionNotes: string;
|
||||
}): Promise<void> {
|
||||
this.logger.debug('[ProtestsService] Reviewing protest:', command);
|
||||
|
||||
const result = await this.reviewProtestUseCase.execute(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to review protest');
|
||||
}
|
||||
}
|
||||
}
|
||||
74
apps/api/src/domain/race/RaceController.test.ts
Normal file
74
apps/api/src/domain/race/RaceController.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { RaceController } from './RaceController';
|
||||
import { RaceService } from './RaceService';
|
||||
|
||||
describe('RaceController', () => {
|
||||
let controller: RaceController;
|
||||
let service: jest.Mocked<RaceService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockService = {
|
||||
getAllRaces: jest.fn(),
|
||||
getTotalRaces: jest.fn(),
|
||||
getRacesPageData: jest.fn(),
|
||||
getAllRacesPageData: jest.fn(),
|
||||
getRaceDetail: jest.fn(),
|
||||
getRaceResultsDetail: jest.fn(),
|
||||
getRaceWithSOF: jest.fn(),
|
||||
getRaceProtests: jest.fn(),
|
||||
getRacePenalties: jest.fn(),
|
||||
registerForRace: jest.fn(),
|
||||
withdrawFromRace: jest.fn(),
|
||||
cancelRace: jest.fn(),
|
||||
completeRace: jest.fn(),
|
||||
importRaceResults: jest.fn(),
|
||||
fileProtest: jest.fn(),
|
||||
applyQuickPenalty: jest.fn(),
|
||||
applyPenalty: jest.fn(),
|
||||
requestProtestDefense: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [RaceController],
|
||||
providers: [
|
||||
{
|
||||
provide: RaceService,
|
||||
useValue: mockService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<RaceController>(RaceController);
|
||||
service = module.get(RaceService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAllRaces', () => {
|
||||
it('should return all races', async () => {
|
||||
const mockResult = { races: [], totalCount: 0 };
|
||||
service.getAllRaces.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getAllRaces();
|
||||
|
||||
expect(service.getAllRaces).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalRaces', () => {
|
||||
it('should return total races count', async () => {
|
||||
const mockResult = { totalRaces: 5 };
|
||||
service.getTotalRaces.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getTotalRaces();
|
||||
|
||||
expect(service.getTotalRaces).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
// Add more tests as needed
|
||||
});
|
||||
@@ -9,10 +9,8 @@ import { RaceResultsDetailDTO } from './dtos/RaceResultsDetailDTO';
|
||||
import { RaceWithSOFDTO } from './dtos/RaceWithSOFDTO';
|
||||
import { RaceProtestsDTO } from './dtos/RaceProtestsDTO';
|
||||
import { RacePenaltiesDTO } from './dtos/RacePenaltiesDTO';
|
||||
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
|
||||
import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
|
||||
import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
|
||||
import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
|
||||
import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO';
|
||||
import { ImportRaceResultsSummaryDTO } from './dtos/ImportRaceResultsSummaryDTO';
|
||||
import { FileProtestCommandDTO } from './dtos/FileProtestCommandDTO';
|
||||
@@ -156,7 +154,7 @@ export class RaceController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'File a protest' })
|
||||
@ApiResponse({ status: 200, description: 'Protest filed successfully' })
|
||||
async fileProtest(@Body() body: FileProtestCommandDTO): Promise<any> {
|
||||
async fileProtest(@Body() body: FileProtestCommandDTO): Promise<void> {
|
||||
return this.raceService.fileProtest(body);
|
||||
}
|
||||
|
||||
@@ -164,7 +162,7 @@ export class RaceController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Apply a quick penalty' })
|
||||
@ApiResponse({ status: 200, description: 'Penalty applied successfully' })
|
||||
async applyQuickPenalty(@Body() body: QuickPenaltyCommandDTO): Promise<any> {
|
||||
async applyQuickPenalty(@Body() body: QuickPenaltyCommandDTO): Promise<void> {
|
||||
return this.raceService.applyQuickPenalty(body);
|
||||
}
|
||||
|
||||
@@ -172,7 +170,7 @@ export class RaceController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Apply a penalty' })
|
||||
@ApiResponse({ status: 200, description: 'Penalty applied successfully' })
|
||||
async applyPenalty(@Body() body: ApplyPenaltyCommandDTO): Promise<any> {
|
||||
async applyPenalty(@Body() body: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
return this.raceService.applyPenalty(body);
|
||||
}
|
||||
|
||||
@@ -180,7 +178,7 @@ export class RaceController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Request protest defense' })
|
||||
@ApiResponse({ status: 200, description: 'Defense requested successfully' })
|
||||
async requestProtestDefense(@Body() body: RequestProtestDefenseCommandDTO): Promise<any> {
|
||||
async requestProtestDefense(@Body() body: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
return this.raceService.requestProtestDefense(body);
|
||||
}
|
||||
}
|
||||
|
||||
28
apps/api/src/domain/race/RaceModule.test.ts
Normal file
28
apps/api/src/domain/race/RaceModule.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { RaceModule } from './RaceModule';
|
||||
import { RaceController } from './RaceController';
|
||||
import { RaceService } from './RaceService';
|
||||
|
||||
describe('RaceModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [RaceModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', async () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have RaceController', () => {
|
||||
const controller = module.get<RaceController>(RaceController);
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have RaceService', () => {
|
||||
const service = module.get<RaceService>(RaceService);
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import type { Logger } from '@core/shared/application/Logger';
|
||||
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
|
||||
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
@@ -23,6 +24,7 @@ import { InMemoryResultRepository } from '@adapters/racing/persistence/inmemory/
|
||||
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory/InMemoryPenaltyRepository';
|
||||
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
|
||||
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
|
||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
@@ -58,6 +60,7 @@ export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
|
||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
||||
export const PENALTY_REPOSITORY_TOKEN = 'IPenaltyRepository';
|
||||
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||
export const STANDING_REPOSITORY_TOKEN = 'IStandingRepository';
|
||||
export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
|
||||
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
@@ -104,6 +107,11 @@ export const RaceProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryProtestRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: STANDING_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryStandingRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_RATING_PROVIDER_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryDriverRatingProvider(logger),
|
||||
@@ -121,13 +129,13 @@ export const RaceProviders: Provider[] = [
|
||||
// Use cases
|
||||
{
|
||||
provide: GetAllRacesUseCase,
|
||||
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => new GetAllRacesUseCase(raceRepo, leagueRepo),
|
||||
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
|
||||
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository, logger: Logger) => new GetAllRacesUseCase(raceRepo, leagueRepo, logger),
|
||||
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GetTotalRacesUseCase,
|
||||
useFactory: (raceRepo: IRaceRepository) => new GetTotalRacesUseCase(raceRepo),
|
||||
inject: [RACE_REPOSITORY_TOKEN],
|
||||
useFactory: (raceRepo: IRaceRepository, logger: Logger) => new GetTotalRacesUseCase(raceRepo, logger),
|
||||
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GetRaceDetailUseCase,
|
||||
@@ -173,14 +181,35 @@ export const RaceProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: GetRaceResultsDetailUseCase,
|
||||
useFactory: (resultRepo: IResultRepository, driverRepo: IDriverRepository, imageService: IImageServicePort) =>
|
||||
new GetRaceResultsDetailUseCase(resultRepo, driverRepo, imageService),
|
||||
inject: [RESULT_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN],
|
||||
useFactory: (
|
||||
raceRepo: IRaceRepository,
|
||||
leagueRepo: ILeagueRepository,
|
||||
resultRepo: IResultRepository,
|
||||
driverRepo: IDriverRepository,
|
||||
penaltyRepo: IPenaltyRepository,
|
||||
) => new GetRaceResultsDetailUseCase(raceRepo, leagueRepo, resultRepo, driverRepo, penaltyRepo),
|
||||
inject: [
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
PENALTY_REPOSITORY_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: GetRaceWithSOFUseCase,
|
||||
useFactory: (raceRepo: IRaceRepository) => new GetRaceWithSOFUseCase(raceRepo),
|
||||
inject: [RACE_REPOSITORY_TOKEN],
|
||||
useFactory: (
|
||||
raceRepo: IRaceRepository,
|
||||
raceRegRepo: IRaceRegistrationRepository,
|
||||
resultRepo: IResultRepository,
|
||||
driverRatingProvider: DriverRatingProvider,
|
||||
) => new GetRaceWithSOFUseCase(raceRepo, raceRegRepo, resultRepo, driverRatingProvider),
|
||||
inject: [
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
DRIVER_RATING_PROVIDER_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: GetRaceProtestsUseCase,
|
||||
@@ -200,8 +229,8 @@ export const RaceProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: WithdrawFromRaceUseCase,
|
||||
useFactory: (raceRegRepo: IRaceRegistrationRepository, logger: Logger) => new WithdrawFromRaceUseCase(raceRegRepo, logger),
|
||||
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
useFactory: (raceRegRepo: IRaceRegistrationRepository) => new WithdrawFromRaceUseCase(raceRegRepo),
|
||||
inject: [RACE_REGISTRATION_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: CancelRaceUseCase,
|
||||
@@ -210,16 +239,78 @@ export const RaceProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: CompleteRaceUseCase,
|
||||
useFactory: (raceRepo: IRaceRepository, logger: Logger) => new CompleteRaceUseCase(raceRepo, logger),
|
||||
inject: [RACE_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
useFactory: (
|
||||
raceRepo: IRaceRepository,
|
||||
raceRegRepo: IRaceRegistrationRepository,
|
||||
resultRepo: IResultRepository,
|
||||
standingRepo: IStandingRepository,
|
||||
driverRatingProvider: DriverRatingProvider,
|
||||
) => new CompleteRaceUseCase(raceRepo, raceRegRepo, resultRepo, standingRepo, driverRatingProvider),
|
||||
inject: [
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
DRIVER_RATING_PROVIDER_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: ImportRaceResultsApiUseCase,
|
||||
useFactory: (
|
||||
raceRepo: IRaceRepository,
|
||||
leagueRepo: ILeagueRepository,
|
||||
resultRepo: IResultRepository,
|
||||
driverRepo: IDriverRepository,
|
||||
standingRepo: IStandingRepository,
|
||||
logger: Logger,
|
||||
) => new ImportRaceResultsApiUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
resultRepo,
|
||||
driverRepo,
|
||||
standingRepo,
|
||||
logger,
|
||||
),
|
||||
inject: [
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: ImportRaceResultsUseCase,
|
||||
useFactory: (
|
||||
raceRepo: IRaceRepository,
|
||||
leagueRepo: ILeagueRepository,
|
||||
resultRepo: IResultRepository,
|
||||
driverRepo: IDriverRepository,
|
||||
standingRepo: IStandingRepository,
|
||||
logger: Logger,
|
||||
) => new ImportRaceResultsUseCase(
|
||||
raceRepo,
|
||||
leagueRepo,
|
||||
resultRepo,
|
||||
driverRepo,
|
||||
standingRepo,
|
||||
logger,
|
||||
),
|
||||
inject: [
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
],
|
||||
},
|
||||
ImportRaceResultsApiUseCase,
|
||||
ImportRaceResultsUseCase,
|
||||
{
|
||||
provide: FileProtestUseCase,
|
||||
useFactory: (protestRepo: IProtestRepository, raceRepo: IRaceRepository, driverRepo: IDriverRepository, logger: Logger) =>
|
||||
new FileProtestUseCase(protestRepo, raceRepo, driverRepo, logger),
|
||||
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
useFactory: (protestRepo: IProtestRepository, raceRepo: IRaceRepository, leagueMembershipRepo: ILeagueMembershipRepository) =>
|
||||
new FileProtestUseCase(protestRepo, raceRepo, leagueMembershipRepo),
|
||||
inject: [PROTEST_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: QuickPenaltyUseCase,
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import {
|
||||
AllRacesPageViewModel,
|
||||
RaceStatsDto,
|
||||
ImportRaceResultsInput,
|
||||
ImportRaceResultsSummaryViewModel,
|
||||
RaceDetailViewModelDto,
|
||||
RacesPageDataViewModelDto,
|
||||
RaceResultsDetailViewModelDto,
|
||||
RaceWithSOFViewModelDto,
|
||||
RaceProtestsViewModelDto,
|
||||
RacePenaltiesViewModelDto,
|
||||
GetRaceDetailParamsDto,
|
||||
RegisterForRaceParamsDto,
|
||||
WithdrawFromRaceParamsDto,
|
||||
RaceActionParamsDto,
|
||||
} from './dtos/RaceDTO';
|
||||
import type { AllRacesPageViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
|
||||
import type { GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter';
|
||||
import type { RaceDetailViewModel } from '@core/racing/application/presenters/IRaceDetailPresenter';
|
||||
import type { RacesPageViewModel } from '@core/racing/application/presenters/IRacesPagePresenter';
|
||||
import type { RaceResultsDetailViewModel } from '@core/racing/application/presenters/IRaceResultsDetailPresenter';
|
||||
import type { RaceWithSOFViewModel } from '@core/racing/application/presenters/IRaceWithSOFPresenter';
|
||||
import type { RaceProtestsViewModel } from '@core/racing/application/presenters/IRaceProtestsPresenter';
|
||||
import type { RacePenaltiesViewModel } from '@core/racing/application/presenters/IRacePenaltiesPresenter';
|
||||
|
||||
// DTOs
|
||||
import { GetRaceDetailParamsDTO } from './dtos/GetRaceDetailParamsDTO';
|
||||
import { RegisterForRaceParamsDTO } from './dtos/RegisterForRaceParamsDTO';
|
||||
import { WithdrawFromRaceParamsDTO } from './dtos/WithdrawFromRaceParamsDTO';
|
||||
import { RaceActionParamsDTO } from './dtos/RaceActionParamsDTO';
|
||||
import { ImportRaceResultsDTO } from './dtos/ImportRaceResultsDTO';
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
@@ -46,6 +45,13 @@ import { GetAllRacesPresenter } from './presenters/GetAllRacesPresenter';
|
||||
import { GetTotalRacesPresenter } from './presenters/GetTotalRacesPresenter';
|
||||
import { ImportRaceResultsApiPresenter } from './presenters/ImportRaceResultsApiPresenter';
|
||||
|
||||
// Command DTOs
|
||||
import { FileProtestCommandDTO } from './dtos/FileProtestCommandDTO';
|
||||
import { QuickPenaltyCommandDTO } from './dtos/QuickPenaltyCommandDTO';
|
||||
import { ApplyPenaltyCommandDTO } from './dtos/ApplyPenaltyCommandDTO';
|
||||
import { RequestProtestDefenseCommandDTO } from './dtos/RequestProtestDefenseCommandDTO';
|
||||
import { ReviewProtestCommandDTO } from './dtos/ReviewProtestCommandDTO';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './RaceProviders';
|
||||
|
||||
@@ -66,7 +72,6 @@ export class RaceService {
|
||||
private readonly withdrawFromRaceUseCase: WithdrawFromRaceUseCase,
|
||||
private readonly cancelRaceUseCase: CancelRaceUseCase,
|
||||
private readonly completeRaceUseCase: CompleteRaceUseCase,
|
||||
private readonly importRaceResultsUseCase: ImportRaceResultsUseCase,
|
||||
private readonly fileProtestUseCase: FileProtestUseCase,
|
||||
private readonly quickPenaltyUseCase: QuickPenaltyUseCase,
|
||||
private readonly applyPenaltyUseCase: ApplyPenaltyUseCase,
|
||||
@@ -83,34 +88,33 @@ export class RaceService {
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getTotalRaces(): Promise<RaceStatsDto> {
|
||||
async getTotalRaces(): Promise<GetTotalRacesViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching total races count.');
|
||||
const presenter = new GetTotalRacesPresenter();
|
||||
await this.getTotalRacesUseCase.execute({}, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
|
||||
async importRaceResults(input: ImportRaceResultsDTO): Promise<ImportRaceResultsSummaryDTO> {
|
||||
this.logger.debug('Importing race results:', input);
|
||||
const presenter = new ImportRaceResultsApiPresenter();
|
||||
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getRaceDetail(params: GetRaceDetailParamsDto): Promise<RaceDetailViewModelDto> {
|
||||
async getRaceDetail(params: GetRaceDetailParamsDTO): Promise<RaceDetailViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching race detail:', params);
|
||||
|
||||
const presenter = new RaceDetailPresenter();
|
||||
const result = await this.getRaceDetailUseCase.execute(params);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to get race detail');
|
||||
throw new Error('Failed to get race detail');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async getRacesPageData(): Promise<RacesPageDataViewModelDto> {
|
||||
async getRacesPageData(): Promise<RacesPageViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching races page data.');
|
||||
|
||||
const result = await this.getRacesPageDataUseCase.execute();
|
||||
@@ -122,7 +126,7 @@ export class RaceService {
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async getAllRacesPageData(): Promise<RacesPageDataViewModelDto> {
|
||||
async getAllRacesPageData(): Promise<RacesPageViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching all races page data.');
|
||||
|
||||
const result = await this.getAllRacesPageDataUseCase.execute();
|
||||
@@ -134,165 +138,143 @@ export class RaceService {
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailViewModelDto> {
|
||||
async getRaceResultsDetail(raceId: string): Promise<RaceResultsDetailViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching race results detail:', { raceId });
|
||||
|
||||
const result = await this.getRaceResultsDetailUseCase.execute({ raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to get race results detail');
|
||||
throw new Error('Failed to get race results detail');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFViewModelDto> {
|
||||
async getRaceWithSOF(raceId: string): Promise<RaceWithSOFViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching race with SOF:', { raceId });
|
||||
|
||||
const result = await this.getRaceWithSOFUseCase.execute({ raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to get race with SOF');
|
||||
throw new Error('Failed to get race with SOF');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async getRaceProtests(raceId: string): Promise<RaceProtestsViewModelDto> {
|
||||
async getRaceProtests(raceId: string): Promise<RaceProtestsViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching race protests:', { raceId });
|
||||
|
||||
const result = await this.getRaceProtestsUseCase.execute({ raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to get race protests');
|
||||
throw new Error('Failed to get race protests');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async getRacePenalties(raceId: string): Promise<RacePenaltiesViewModelDto> {
|
||||
async getRacePenalties(raceId: string): Promise<RacePenaltiesViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching race penalties:', { raceId });
|
||||
|
||||
const result = await this.getRacePenaltiesUseCase.execute({ raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to get race penalties');
|
||||
throw new Error('Failed to get race penalties');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async registerForRace(params: RegisterForRaceParamsDto): Promise<void> {
|
||||
async registerForRace(params: RegisterForRaceParamsDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Registering for race:', params);
|
||||
|
||||
const result = await this.registerForRaceUseCase.execute(params);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to register for race');
|
||||
throw new Error('Failed to register for race');
|
||||
}
|
||||
}
|
||||
|
||||
async withdrawFromRace(params: WithdrawFromRaceParamsDto): Promise<void> {
|
||||
async withdrawFromRace(params: WithdrawFromRaceParamsDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Withdrawing from race:', params);
|
||||
|
||||
const result = await this.withdrawFromRaceUseCase.execute(params);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to withdraw from race');
|
||||
throw new Error('Failed to withdraw from race');
|
||||
}
|
||||
}
|
||||
|
||||
async cancelRace(params: RaceActionParamsDto): Promise<void> {
|
||||
async cancelRace(params: RaceActionParamsDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Cancelling race:', params);
|
||||
|
||||
const result = await this.cancelRaceUseCase.execute({ raceId: params.raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to cancel race');
|
||||
throw new Error('Failed to cancel race');
|
||||
}
|
||||
}
|
||||
|
||||
async completeRace(params: RaceActionParamsDto): Promise<void> {
|
||||
async completeRace(params: RaceActionParamsDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Completing race:', params);
|
||||
|
||||
const result = await this.completeRaceUseCase.execute({ raceId: params.raceId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to complete race');
|
||||
}
|
||||
}
|
||||
|
||||
async importRaceResultsAlt(params: { raceId: string; resultsFileContent: string }): Promise<void> {
|
||||
this.logger.debug('[RaceService] Importing race results (alt):', params);
|
||||
|
||||
const result = await this.importRaceResultsUseCase.execute({
|
||||
raceId: params.raceId,
|
||||
resultsFileContent: params.resultsFileContent,
|
||||
});
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to import race results');
|
||||
throw new Error('Failed to complete race');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fileProtest(command: any): Promise<any> {
|
||||
|
||||
async fileProtest(command: FileProtestCommandDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Filing protest:', command);
|
||||
|
||||
const result = await this.fileProtestUseCase.execute(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to file protest');
|
||||
throw new Error('Failed to file protest');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async applyQuickPenalty(command: any): Promise<any> {
|
||||
async applyQuickPenalty(command: QuickPenaltyCommandDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Applying quick penalty:', command);
|
||||
|
||||
const result = await this.quickPenaltyUseCase.execute(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to apply quick penalty');
|
||||
throw new Error('Failed to apply quick penalty');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async applyPenalty(command: any): Promise<any> {
|
||||
async applyPenalty(command: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Applying penalty:', command);
|
||||
|
||||
const result = await this.applyPenaltyUseCase.execute(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to apply penalty');
|
||||
throw new Error('Failed to apply penalty');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async requestProtestDefense(command: any): Promise<any> {
|
||||
async requestProtestDefense(command: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Requesting protest defense:', command);
|
||||
|
||||
const result = await this.requestProtestDefenseUseCase.execute(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to request protest defense');
|
||||
throw new Error('Failed to request protest defense');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async reviewProtest(command: any): Promise<any> {
|
||||
async reviewProtest(command: ReviewProtestCommandDTO): Promise<void> {
|
||||
this.logger.debug('[RaceService] Reviewing protest:', command);
|
||||
|
||||
const result = await this.reviewProtestUseCase.execute(command);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error.details.message || 'Failed to review protest');
|
||||
throw new Error('Failed to review protest');
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { RaceViewModel } from './RaceViewModel';
|
||||
import { RaceViewModel } from '@core/racing/application/presenters/IGetAllRacesPresenter';
|
||||
|
||||
export class AllRacesPageDTO {
|
||||
@ApiProperty({ type: [RaceViewModel] })
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class GetRaceDetailParamsDTODTO {
|
||||
export class GetRaceDetailParamsDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsString, IsNumber } from 'class-validator';
|
||||
|
||||
export class ImportRaceResultsSummaryDTOViewModel {
|
||||
export class ImportRaceResultsSummaryDTO {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success!: boolean;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { RaceProtestDto } from './RaceProtestDto';
|
||||
import { RaceProtestDTO } from './RaceProtestDTO';
|
||||
|
||||
export class RaceProtestsDTO {
|
||||
@ApiProperty({ type: [RaceProtestDto] })
|
||||
protests!: RaceProtestDto[];
|
||||
@ApiProperty({ type: [RaceProtestDTO] })
|
||||
protests!: RaceProtestDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
driverMap!: Record<string, string>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString } from 'class-validator';
|
||||
import { RaceResultDto } from './RaceResultDto';
|
||||
import { RaceResultDTO } from './RaceResultDTO';
|
||||
|
||||
export class RaceResultsDetailDTO {
|
||||
@ApiProperty()
|
||||
@@ -11,6 +11,6 @@ export class RaceResultsDetailDTO {
|
||||
@IsString()
|
||||
track!: string;
|
||||
|
||||
@ApiProperty({ type: [RaceResultDto] })
|
||||
results!: RaceResultDto[];
|
||||
@ApiProperty({ type: [RaceResultDTO] })
|
||||
results!: RaceResultDTO[];
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { RacesPageDataRaceDto } from './RacesPageDataRaceDto';
|
||||
import { RacesPageDataRaceDTO } from './RacesPageDataRaceDTO';
|
||||
|
||||
export class RacesPageDataDTO {
|
||||
@ApiProperty({ type: [RacesPageDataRaceDto] })
|
||||
races!: RacesPageDataRaceDto[];
|
||||
@ApiProperty({ type: [RacesPageDataRaceDTO] })
|
||||
races!: RacesPageDataRaceDTO[];
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { IGetTotalRacesPresenter, GetTotalRacesResultDTO } from '@core/racing/application/presenters/IGetTotalRacesPresenter';
|
||||
import { RaceStatsDto } from '../dto/RaceDto';
|
||||
import { IGetTotalRacesPresenter, GetTotalRacesResultDTO, GetTotalRacesViewModel } from '@core/racing/application/presenters/IGetTotalRacesPresenter';
|
||||
|
||||
export class GetTotalRacesPresenter implements IGetTotalRacesPresenter {
|
||||
private result: RaceStatsDto | null = null;
|
||||
private result: GetTotalRacesViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
@@ -14,7 +13,7 @@ export class GetTotalRacesPresenter implements IGetTotalRacesPresenter {
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RaceStatsDto | null {
|
||||
getViewModel(): GetTotalRacesViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
210
apps/api/src/domain/sponsor/SponsorController.test.ts
Normal file
210
apps/api/src/domain/sponsor/SponsorController.test.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { SponsorController } from './SponsorController';
|
||||
import { SponsorService } from './SponsorService';
|
||||
|
||||
describe('SponsorController', () => {
|
||||
let controller: SponsorController;
|
||||
let sponsorService: ReturnType<typeof vi.mocked<SponsorService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SponsorController],
|
||||
providers: [
|
||||
{
|
||||
provide: SponsorService,
|
||||
useValue: {
|
||||
getEntitySponsorshipPricing: vi.fn(),
|
||||
getSponsors: vi.fn(),
|
||||
createSponsor: vi.fn(),
|
||||
getSponsorDashboard: vi.fn(),
|
||||
getSponsorSponsorships: vi.fn(),
|
||||
getSponsor: vi.fn(),
|
||||
getPendingSponsorshipRequests: vi.fn(),
|
||||
acceptSponsorshipRequest: vi.fn(),
|
||||
rejectSponsorshipRequest: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SponsorController>(SponsorController);
|
||||
sponsorService = vi.mocked(module.get(SponsorService));
|
||||
});
|
||||
|
||||
describe('getEntitySponsorshipPricing', () => {
|
||||
it('should return sponsorship pricing', async () => {
|
||||
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
|
||||
sponsorService.getEntitySponsorshipPricing.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getEntitySponsorshipPricing();
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.getEntitySponsorshipPricing).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsors', () => {
|
||||
it('should return sponsors list', async () => {
|
||||
const mockResult = { sponsors: [] };
|
||||
sponsorService.getSponsors.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getSponsors();
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.getSponsors).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSponsor', () => {
|
||||
it('should create sponsor', async () => {
|
||||
const input = { name: 'Test Sponsor', contactEmail: 'test@example.com' };
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
|
||||
sponsorService.createSponsor.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.createSponsor(input);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.createSponsor).toHaveBeenCalledWith(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsorDashboard', () => {
|
||||
it('should return sponsor dashboard', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
const mockResult = { sponsorId, metrics: {}, sponsoredLeagues: [] };
|
||||
sponsorService.getSponsorDashboard.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getSponsorDashboard(sponsorId);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.getSponsorDashboard).toHaveBeenCalledWith({ sponsorId });
|
||||
});
|
||||
|
||||
it('should return null when sponsor not found', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
sponsorService.getSponsorDashboard.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getSponsorDashboard(sponsorId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsorSponsorships', () => {
|
||||
it('should return sponsor sponsorships', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
const mockResult = { sponsorId, sponsorships: [] };
|
||||
sponsorService.getSponsorSponsorships.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getSponsorSponsorships(sponsorId);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.getSponsorSponsorships).toHaveBeenCalledWith({ sponsorId });
|
||||
});
|
||||
|
||||
it('should return null when sponsor not found', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
sponsorService.getSponsorSponsorships.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getSponsorSponsorships(sponsorId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsor', () => {
|
||||
it('should return sponsor', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
const mockResult = { id: sponsorId, name: 'Test Sponsor' };
|
||||
sponsorService.getSponsor.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getSponsor(sponsorId);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.getSponsor).toHaveBeenCalledWith(sponsorId);
|
||||
});
|
||||
|
||||
it('should return null when sponsor not found', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
sponsorService.getSponsor.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getSponsor(sponsorId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPendingSponsorshipRequests', () => {
|
||||
it('should return pending sponsorship requests', async () => {
|
||||
const query = { entityType: 'season' as const, entityId: 'season-1' };
|
||||
const mockResult = { entityType: 'season', entityId: 'season-1', requests: [], totalCount: 0 };
|
||||
sponsorService.getPendingSponsorshipRequests.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.getPendingSponsorshipRequests(query);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.getPendingSponsorshipRequests).toHaveBeenCalledWith(query);
|
||||
});
|
||||
});
|
||||
|
||||
describe('acceptSponsorshipRequest', () => {
|
||||
it('should accept sponsorship request', async () => {
|
||||
const requestId = 'request-1';
|
||||
const input = { respondedBy: 'user-1' };
|
||||
const mockResult = {
|
||||
requestId,
|
||||
sponsorshipId: 'sponsorship-1',
|
||||
status: 'accepted' as const,
|
||||
acceptedAt: new Date(),
|
||||
platformFee: 10,
|
||||
netAmount: 90,
|
||||
};
|
||||
sponsorService.acceptSponsorshipRequest.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.acceptSponsorshipRequest(requestId, input);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.acceptSponsorshipRequest).toHaveBeenCalledWith(requestId, input.respondedBy);
|
||||
});
|
||||
|
||||
it('should return null on error', async () => {
|
||||
const requestId = 'request-1';
|
||||
const input = { respondedBy: 'user-1' };
|
||||
sponsorService.acceptSponsorshipRequest.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.acceptSponsorshipRequest(requestId, input);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejectSponsorshipRequest', () => {
|
||||
it('should reject sponsorship request', async () => {
|
||||
const requestId = 'request-1';
|
||||
const input = { respondedBy: 'user-1', reason: 'Not interested' };
|
||||
const mockResult = {
|
||||
requestId,
|
||||
status: 'rejected' as const,
|
||||
rejectedAt: new Date(),
|
||||
reason: 'Not interested',
|
||||
};
|
||||
sponsorService.rejectSponsorshipRequest.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await controller.rejectSponsorshipRequest(requestId, input);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(sponsorService.rejectSponsorshipRequest).toHaveBeenCalledWith(requestId, input.respondedBy, input.reason);
|
||||
});
|
||||
|
||||
it('should return null on error', async () => {
|
||||
const requestId = 'request-1';
|
||||
const input = { respondedBy: 'user-1' };
|
||||
sponsorService.rejectSponsorshipRequest.mockResolvedValue(null);
|
||||
|
||||
const result = await controller.rejectSponsorshipRequest(requestId, input);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,8 @@ import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO';
|
||||
import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO';
|
||||
import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO';
|
||||
import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO';
|
||||
import type { AcceptSponsorshipRequestResultDTO } from '@core/racing/application/dtos/AcceptSponsorshipRequestResultDTO';
|
||||
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
|
||||
|
||||
@ApiTags('sponsors')
|
||||
@Controller('sponsors')
|
||||
@@ -69,26 +71,26 @@ export class SponsorController {
|
||||
@ApiOperation({ summary: 'Get pending sponsorship requests' })
|
||||
@ApiResponse({ status: 200, description: 'List of pending sponsorship requests', type: GetPendingSponsorshipRequestsOutputDTO })
|
||||
async getPendingSponsorshipRequests(@Query() query: { entityType: string; entityId: string }): Promise<GetPendingSponsorshipRequestsOutputDTO> {
|
||||
return this.sponsorService.getPendingSponsorshipRequests(query);
|
||||
return this.sponsorService.getPendingSponsorshipRequests(query as { entityType: import('@core/racing/domain/entities/SponsorshipRequest').SponsorableEntityType; entityId: string });
|
||||
}
|
||||
|
||||
@Post('requests/:requestId/accept')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Accept a sponsorship request' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsorship request accepted' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid request' })
|
||||
@ApiResponse({ status: 404, description: 'Request not found' })
|
||||
async acceptSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: AcceptSponsorshipRequestInputDTO): Promise<any> {
|
||||
return this.sponsorService.acceptSponsorshipRequest(requestId, input.respondedBy);
|
||||
}
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Accept a sponsorship request' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsorship request accepted' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid request' })
|
||||
@ApiResponse({ status: 404, description: 'Request not found' })
|
||||
async acceptSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: AcceptSponsorshipRequestInputDTO): Promise<AcceptSponsorshipRequestResultDTO | null> {
|
||||
return this.sponsorService.acceptSponsorshipRequest(requestId, input.respondedBy);
|
||||
}
|
||||
|
||||
@Post('requests/:requestId/reject')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Reject a sponsorship request' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsorship request rejected' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid request' })
|
||||
@ApiResponse({ status: 404, description: 'Request not found' })
|
||||
async rejectSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: RejectSponsorshipRequestInputDTO): Promise<any> {
|
||||
return this.sponsorService.rejectSponsorshipRequest(requestId, input.respondedBy, input.reason);
|
||||
}
|
||||
@Post('requests/:requestId/reject')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: 'Reject a sponsorship request' })
|
||||
@ApiResponse({ status: 200, description: 'Sponsorship request rejected' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid request' })
|
||||
@ApiResponse({ status: 404, description: 'Request not found' })
|
||||
async rejectSponsorshipRequest(@Param('requestId') requestId: string, @Body() input: RejectSponsorshipRequestInputDTO): Promise<RejectSponsorshipRequestResultDTO | null> {
|
||||
return this.sponsorService.rejectSponsorshipRequest(requestId, input.respondedBy, input.reason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/IL
|
||||
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/ISponsorshipPricingRepository';
|
||||
import { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import { INotificationService } from '@core/notifications/application/ports/INotificationService';
|
||||
import { IPaymentGateway } from '@core/racing/application/ports/IPaymentGateway';
|
||||
import { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
|
||||
import { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
// Import use cases
|
||||
@@ -152,7 +156,7 @@ export const SponsorProviders: Provider[] = [
|
||||
},
|
||||
{
|
||||
provide: ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN,
|
||||
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, notificationService: any, paymentGateway: any, walletRepository: any, leagueWalletRepository: any, logger: Logger) =>
|
||||
useFactory: (sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, notificationService: INotificationService, paymentGateway: IPaymentGateway, walletRepository: IWalletRepository, leagueWalletRepository: ILeagueWalletRepository, logger: Logger) =>
|
||||
new AcceptSponsorshipRequestUseCase(sponsorshipRequestRepo, seasonSponsorshipRepo, seasonRepo, notificationService, paymentGateway, walletRepository, leagueWalletRepository, logger),
|
||||
inject: [SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, 'INotificationService', 'IPaymentGateway', 'IWalletRepository', 'ILeagueWalletRepository', LOGGER_TOKEN],
|
||||
},
|
||||
|
||||
258
apps/api/src/domain/sponsor/SponsorService.test.ts
Normal file
258
apps/api/src/domain/sponsor/SponsorService.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
||||
import { SponsorService } from './SponsorService';
|
||||
import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
|
||||
import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
|
||||
import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
|
||||
import type { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
import type { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
|
||||
import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||
import type { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
|
||||
import type { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
|
||||
describe('SponsorService', () => {
|
||||
let service: SponsorService;
|
||||
let getSponsorshipPricingUseCase: { execute: Mock };
|
||||
let getSponsorsUseCase: { execute: Mock };
|
||||
let createSponsorUseCase: { execute: Mock };
|
||||
let getSponsorDashboardUseCase: { execute: Mock };
|
||||
let getSponsorSponsorshipsUseCase: { execute: Mock };
|
||||
let getSponsorUseCase: { execute: Mock };
|
||||
let getPendingSponsorshipRequestsUseCase: { execute: Mock };
|
||||
let acceptSponsorshipRequestUseCase: { execute: Mock };
|
||||
let rejectSponsorshipRequestUseCase: { execute: Mock };
|
||||
let logger: {
|
||||
debug: Mock;
|
||||
info: Mock;
|
||||
warn: Mock;
|
||||
error: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getSponsorshipPricingUseCase = { execute: vi.fn() };
|
||||
getSponsorsUseCase = { execute: vi.fn() };
|
||||
createSponsorUseCase = { execute: vi.fn() };
|
||||
getSponsorDashboardUseCase = { execute: vi.fn() };
|
||||
getSponsorSponsorshipsUseCase = { execute: vi.fn() };
|
||||
getSponsorUseCase = { execute: vi.fn() };
|
||||
getPendingSponsorshipRequestsUseCase = { execute: vi.fn() };
|
||||
acceptSponsorshipRequestUseCase = { execute: vi.fn() };
|
||||
rejectSponsorshipRequestUseCase = { execute: vi.fn() };
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
service = new SponsorService(
|
||||
getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase,
|
||||
getSponsorsUseCase as unknown as GetSponsorsUseCase,
|
||||
createSponsorUseCase as unknown as CreateSponsorUseCase,
|
||||
getSponsorDashboardUseCase as unknown as GetSponsorDashboardUseCase,
|
||||
getSponsorSponsorshipsUseCase as unknown as GetSponsorSponsorshipsUseCase,
|
||||
getSponsorUseCase as unknown as GetSponsorUseCase,
|
||||
getPendingSponsorshipRequestsUseCase as unknown as GetPendingSponsorshipRequestsUseCase,
|
||||
acceptSponsorshipRequestUseCase as unknown as AcceptSponsorshipRequestUseCase,
|
||||
rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase,
|
||||
logger as unknown as Logger,
|
||||
);
|
||||
});
|
||||
|
||||
describe('getEntitySponsorshipPricing', () => {
|
||||
it('should return sponsorship pricing', async () => {
|
||||
const mockPresenter = {
|
||||
viewModel: { entityType: 'season', entityId: 'season-1', pricing: [] },
|
||||
};
|
||||
getSponsorshipPricingUseCase.execute.mockResolvedValue(undefined);
|
||||
|
||||
// Mock the presenter
|
||||
const originalGetSponsorshipPricingPresenter = await import('./presenters/GetSponsorshipPricingPresenter');
|
||||
const mockPresenterClass = vi.fn().mockImplementation(() => mockPresenter);
|
||||
vi.doMock('./presenters/GetSponsorshipPricingPresenter', () => ({
|
||||
GetSponsorshipPricingPresenter: mockPresenterClass,
|
||||
}));
|
||||
|
||||
const result = await service.getEntitySponsorshipPricing();
|
||||
|
||||
expect(result).toEqual(mockPresenter.viewModel);
|
||||
expect(getSponsorshipPricingUseCase.execute).toHaveBeenCalledWith(undefined, mockPresenter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsors', () => {
|
||||
it('should return sponsors list', async () => {
|
||||
const mockPresenter = {
|
||||
viewModel: { sponsors: [] },
|
||||
};
|
||||
getSponsorsUseCase.execute.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.getSponsors();
|
||||
|
||||
expect(result).toEqual(mockPresenter.viewModel);
|
||||
expect(getSponsorsUseCase.execute).toHaveBeenCalledWith(undefined, expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSponsor', () => {
|
||||
it('should create sponsor successfully', async () => {
|
||||
const input = { name: 'Test Sponsor', contactEmail: 'test@example.com' };
|
||||
const mockPresenter = {
|
||||
viewModel: { id: 'sponsor-1', name: 'Test Sponsor' },
|
||||
};
|
||||
createSponsorUseCase.execute.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.createSponsor(input);
|
||||
|
||||
expect(result).toEqual(mockPresenter.viewModel);
|
||||
expect(createSponsorUseCase.execute).toHaveBeenCalledWith(input, expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsorDashboard', () => {
|
||||
it('should return sponsor dashboard', async () => {
|
||||
const params = { sponsorId: 'sponsor-1' };
|
||||
const mockPresenter = {
|
||||
viewModel: { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] },
|
||||
};
|
||||
getSponsorDashboardUseCase.execute.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.getSponsorDashboard(params);
|
||||
|
||||
expect(result).toEqual(mockPresenter.viewModel);
|
||||
expect(getSponsorDashboardUseCase.execute).toHaveBeenCalledWith(params, expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsorSponsorships', () => {
|
||||
it('should return sponsor sponsorships', async () => {
|
||||
const params = { sponsorId: 'sponsor-1' };
|
||||
const mockPresenter = {
|
||||
viewModel: { sponsorId: 'sponsor-1', sponsorships: [] },
|
||||
};
|
||||
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.getSponsorSponsorships(params);
|
||||
|
||||
expect(result).toEqual(mockPresenter.viewModel);
|
||||
expect(getSponsorSponsorshipsUseCase.execute).toHaveBeenCalledWith(params, expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSponsor', () => {
|
||||
it('should return sponsor when found', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
const mockSponsor = { id: sponsorId, name: 'Test Sponsor' };
|
||||
getSponsorUseCase.execute.mockResolvedValue(Result.ok(mockSponsor));
|
||||
|
||||
const result = await service.getSponsor(sponsorId);
|
||||
|
||||
expect(result).toEqual(mockSponsor);
|
||||
expect(getSponsorUseCase.execute).toHaveBeenCalledWith({ sponsorId });
|
||||
});
|
||||
|
||||
it('should return null when sponsor not found', async () => {
|
||||
const sponsorId = 'sponsor-1';
|
||||
getSponsorUseCase.execute.mockResolvedValue(Result.err({ code: 'NOT_FOUND' }));
|
||||
|
||||
const result = await service.getSponsor(sponsorId);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPendingSponsorshipRequests', () => {
|
||||
it('should return pending sponsorship requests', async () => {
|
||||
const params = { entityType: 'season' as const, entityId: 'season-1' };
|
||||
const mockResult = {
|
||||
entityType: 'season',
|
||||
entityId: 'season-1',
|
||||
requests: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(mockResult));
|
||||
|
||||
const result = await service.getPendingSponsorshipRequests(params);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(getPendingSponsorshipRequestsUseCase.execute).toHaveBeenCalledWith(params);
|
||||
});
|
||||
|
||||
it('should return empty result on error', async () => {
|
||||
const params = { entityType: 'season' as const, entityId: 'season-1' };
|
||||
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
|
||||
|
||||
const result = await service.getPendingSponsorshipRequests(params);
|
||||
|
||||
expect(result).toEqual({
|
||||
entityType: 'season',
|
||||
entityId: 'season-1',
|
||||
requests: [],
|
||||
totalCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('acceptSponsorshipRequest', () => {
|
||||
it('should accept sponsorship request successfully', async () => {
|
||||
const requestId = 'request-1';
|
||||
const respondedBy = 'user-1';
|
||||
const mockResult = {
|
||||
requestId,
|
||||
sponsorshipId: 'sponsorship-1',
|
||||
status: 'accepted' as const,
|
||||
acceptedAt: new Date(),
|
||||
platformFee: 10,
|
||||
netAmount: 90,
|
||||
};
|
||||
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(mockResult));
|
||||
|
||||
const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(acceptSponsorshipRequestUseCase.execute).toHaveBeenCalledWith({ requestId, respondedBy });
|
||||
});
|
||||
|
||||
it('should return null on error', async () => {
|
||||
const requestId = 'request-1';
|
||||
const respondedBy = 'user-1';
|
||||
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.err({ code: 'NOT_FOUND' }));
|
||||
|
||||
const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejectSponsorshipRequest', () => {
|
||||
it('should reject sponsorship request successfully', async () => {
|
||||
const requestId = 'request-1';
|
||||
const respondedBy = 'user-1';
|
||||
const reason = 'Not interested';
|
||||
const mockResult = {
|
||||
requestId,
|
||||
status: 'rejected' as const,
|
||||
rejectedAt: new Date(),
|
||||
reason,
|
||||
};
|
||||
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(mockResult));
|
||||
|
||||
const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
|
||||
|
||||
expect(result).toEqual(mockResult);
|
||||
expect(rejectSponsorshipRequestUseCase.execute).toHaveBeenCalledWith({ requestId, respondedBy, reason });
|
||||
});
|
||||
|
||||
it('should return null on error', async () => {
|
||||
const requestId = 'request-1';
|
||||
const respondedBy = 'user-1';
|
||||
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.err({ code: 'NOT_FOUND' }));
|
||||
|
||||
const result = await service.rejectSponsorshipRequest(requestId, respondedBy);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,11 +11,6 @@ import { GetSponsorOutputDTO } from './dtos/GetSponsorOutputDTO';
|
||||
import { GetPendingSponsorshipRequestsOutputDTO } from './dtos/GetPendingSponsorshipRequestsOutputDTO';
|
||||
import { AcceptSponsorshipRequestInputDTO } from './dtos/AcceptSponsorshipRequestInputDTO';
|
||||
import { RejectSponsorshipRequestInputDTO } from './dtos/RejectSponsorshipRequestInputDTO';
|
||||
import { SponsorDTO } from './dtos/SponsorDTO';
|
||||
import { SponsorDashboardMetricsDTO } from './dtos/SponsorDashboardMetricsDTO';
|
||||
import { SponsoredLeagueDTO } from './dtos/SponsoredLeagueDTO';
|
||||
import { SponsorDashboardInvestmentDTO } from './dtos/SponsorDashboardInvestmentDTO';
|
||||
import { SponsorshipDetailDTO } from './dtos/SponsorshipDetailDTO';
|
||||
|
||||
// Use cases
|
||||
import { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
|
||||
@@ -24,16 +19,13 @@ import { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateS
|
||||
import { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
import { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
|
||||
import { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||
import { GetPendingSponsorshipRequestsUseCase, GetPendingSponsorshipRequestsDTO } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
|
||||
import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
|
||||
import type { SponsorableEntityType } from '@core/racing/domain/entities/SponsorshipRequest';
|
||||
import type { AcceptSponsorshipRequestResultDTO } from '@core/racing/application/dtos/AcceptSponsorshipRequestResultDTO';
|
||||
import type { RejectSponsorshipRequestResultDTO } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
|
||||
|
||||
// Presenters
|
||||
import { GetSponsorshipPricingPresenter } from './presenters/GetSponsorshipPricingPresenter';
|
||||
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
|
||||
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
|
||||
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
|
||||
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
|
||||
|
||||
// Tokens
|
||||
import { GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN, GET_SPONSORS_USE_CASE_TOKEN, CREATE_SPONSOR_USE_CASE_TOKEN, GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN, GET_SPONSOR_USE_CASE_TOKEN, GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN, ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN, LOGGER_TOKEN } from './SponsorProviders';
|
||||
@@ -57,41 +49,56 @@ export class SponsorService {
|
||||
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDTO> {
|
||||
this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
|
||||
|
||||
const presenter = new GetSponsorshipPricingPresenter();
|
||||
await this.getSponsorshipPricingUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
const result = await this.getSponsorshipPricingUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to fetch sponsorship pricing.', result.error);
|
||||
return { entityType: 'season', entityId: '', pricing: [] };
|
||||
}
|
||||
return result.value as GetEntitySponsorshipPricingResultDTO;
|
||||
}
|
||||
|
||||
async getSponsors(): Promise<GetSponsorsOutputDTO> {
|
||||
this.logger.debug('[SponsorService] Fetching sponsors.');
|
||||
|
||||
const presenter = new GetSponsorsPresenter();
|
||||
await this.getSponsorsUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
const result = await this.getSponsorsUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to fetch sponsors.', result.error);
|
||||
return { sponsors: [] };
|
||||
}
|
||||
return result.value as GetSponsorsOutputDTO;
|
||||
}
|
||||
|
||||
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorOutputDTO> {
|
||||
this.logger.debug('[SponsorService] Creating sponsor.', { input });
|
||||
|
||||
const presenter = new CreateSponsorPresenter();
|
||||
await this.createSponsorUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
const result = await this.createSponsorUseCase.execute(input);
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to create sponsor.', result.error);
|
||||
throw new Error(result.error.details?.message || 'Failed to create sponsor');
|
||||
}
|
||||
return result.value as CreateSponsorOutputDTO;
|
||||
}
|
||||
|
||||
async getSponsorDashboard(params: GetSponsorDashboardQueryParamsDTO): Promise<SponsorDashboardDTO | null> {
|
||||
this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params });
|
||||
|
||||
const presenter = new GetSponsorDashboardPresenter();
|
||||
await this.getSponsorDashboardUseCase.execute(params, presenter);
|
||||
return presenter.viewModel as SponsorDashboardDTO | null;
|
||||
const result = await this.getSponsorDashboardUseCase.execute(params);
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to fetch sponsor dashboard.', result.error);
|
||||
return null;
|
||||
}
|
||||
return result.value as SponsorDashboardDTO | null;
|
||||
}
|
||||
|
||||
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParamsDTO): Promise<SponsorSponsorshipsDTO | null> {
|
||||
this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params });
|
||||
|
||||
const presenter = new GetSponsorSponsorshipsPresenter();
|
||||
await this.getSponsorSponsorshipsUseCase.execute(params, presenter);
|
||||
return presenter.viewModel as SponsorSponsorshipsDTO | null;
|
||||
const result = await this.getSponsorSponsorshipsUseCase.execute(params);
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to fetch sponsor sponsorships.', result.error);
|
||||
return null;
|
||||
}
|
||||
return result.value as SponsorSponsorshipsDTO | null;
|
||||
}
|
||||
|
||||
async getSponsor(sponsorId: string): Promise<GetSponsorOutputDTO | null> {
|
||||
@@ -105,18 +112,18 @@ export class SponsorService {
|
||||
return result.value as GetSponsorOutputDTO | null;
|
||||
}
|
||||
|
||||
async getPendingSponsorshipRequests(params: { entityType: string; entityId: string }): Promise<GetPendingSponsorshipRequestsOutputDTO> {
|
||||
async getPendingSponsorshipRequests(params: { entityType: SponsorableEntityType; entityId: string }): Promise<GetPendingSponsorshipRequestsOutputDTO> {
|
||||
this.logger.debug('[SponsorService] Fetching pending sponsorship requests.', { params });
|
||||
|
||||
const result = await this.getPendingSponsorshipRequestsUseCase.execute(params as any);
|
||||
const result = await this.getPendingSponsorshipRequestsUseCase.execute(params as GetPendingSponsorshipRequestsDTO);
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to fetch pending sponsorship requests.', result.error);
|
||||
return { entityType: params.entityType as any, entityId: params.entityId, requests: [], totalCount: 0 };
|
||||
return { entityType: params.entityType, entityId: params.entityId, requests: [], totalCount: 0 };
|
||||
}
|
||||
return result.value as GetPendingSponsorshipRequestsOutputDTO;
|
||||
}
|
||||
|
||||
async acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<{ requestId: string; sponsorshipId: string; status: string; acceptedAt: Date; platformFee: number; netAmount: number } | null> {
|
||||
async acceptSponsorshipRequest(requestId: string, respondedBy: string): Promise<AcceptSponsorshipRequestResultDTO | null> {
|
||||
this.logger.debug('[SponsorService] Accepting sponsorship request.', { requestId, respondedBy });
|
||||
|
||||
const result = await this.acceptSponsorshipRequestUseCase.execute({ requestId, respondedBy });
|
||||
@@ -127,7 +134,7 @@ export class SponsorService {
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async rejectSponsorshipRequest(requestId: string, respondedBy: string, reason?: string): Promise<{ requestId: string; status: string; rejectedAt: Date } | null> {
|
||||
async rejectSponsorshipRequest(requestId: string, respondedBy: string, reason?: string): Promise<RejectSponsorshipRequestResultDTO | null> {
|
||||
this.logger.debug('[SponsorService] Rejecting sponsorship request.', { requestId, respondedBy, reason });
|
||||
|
||||
const result = await this.rejectSponsorshipRequestUseCase.execute({ requestId, respondedBy, reason });
|
||||
|
||||
@@ -5,5 +5,5 @@ export class AcceptSponsorshipRequestInputDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
respondedBy: string;
|
||||
respondedBy!: string;
|
||||
}
|
||||
@@ -5,12 +5,12 @@ export class CreateSponsorInputDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
name!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
contactEmail: string;
|
||||
contactEmail!: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { CreateSponsorPresenter } from './CreateSponsorPresenter';
|
||||
|
||||
describe('CreateSponsorPresenter', () => {
|
||||
let presenter: CreateSponsorPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new CreateSponsorPresenter();
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should store the result', () => {
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor', contactEmail: 'test@example.com' };
|
||||
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewModel', () => {
|
||||
it('should return null when not presented', () => {
|
||||
expect(presenter.getViewModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.getViewModel()).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewModel', () => {
|
||||
it('should throw error when not presented', () => {
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { id: 'sponsor-1', name: 'Test Sponsor' };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetEntitySponsorshipPricingPresenter } from './GetEntitySponsorshipPricingPresenter';
|
||||
|
||||
describe('GetEntitySponsorshipPricingPresenter', () => {
|
||||
let presenter: GetEntitySponsorshipPricingPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new GetEntitySponsorshipPricingPresenter();
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should store the result', () => {
|
||||
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
|
||||
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewModel', () => {
|
||||
it('should return null when not presented', () => {
|
||||
expect(presenter.getViewModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.getViewModel()).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewModel', () => {
|
||||
it('should throw error when not presented', () => {
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { entityType: 'season', entityId: 'season-1', pricing: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetSponsorDashboardPresenter } from './GetSponsorDashboardPresenter';
|
||||
|
||||
describe('GetSponsorDashboardPresenter', () => {
|
||||
let presenter: GetSponsorDashboardPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new GetSponsorDashboardPresenter();
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should store the result', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
|
||||
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewModel', () => {
|
||||
it('should return null when not presented', () => {
|
||||
expect(presenter.getViewModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.getViewModel()).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewModel', () => {
|
||||
it('should throw error when not presented', () => {
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', metrics: {}, sponsoredLeagues: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetSponsorSponsorshipsPresenter } from './GetSponsorSponsorshipsPresenter';
|
||||
|
||||
describe('GetSponsorSponsorshipsPresenter', () => {
|
||||
let presenter: GetSponsorSponsorshipsPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new GetSponsorSponsorshipsPresenter();
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should store the result', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
|
||||
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewModel', () => {
|
||||
it('should return null when not presented', () => {
|
||||
expect(presenter.getViewModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.getViewModel()).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewModel', () => {
|
||||
it('should throw error when not presented', () => {
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsorId: 'sponsor-1', sponsorName: 'Test Sponsor', sponsorships: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetSponsorsPresenter } from './GetSponsorsPresenter';
|
||||
|
||||
describe('GetSponsorsPresenter', () => {
|
||||
let presenter: GetSponsorsPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new GetSponsorsPresenter();
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { sponsors: [] };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should store the result', () => {
|
||||
const mockResult = {
|
||||
sponsors: [
|
||||
{ id: 'sponsor-1', name: 'Sponsor One', contactEmail: 's1@example.com' },
|
||||
{ id: 'sponsor-2', name: 'Sponsor Two', contactEmail: 's2@example.com' },
|
||||
],
|
||||
};
|
||||
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewModel', () => {
|
||||
it('should return null when not presented', () => {
|
||||
expect(presenter.getViewModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsors: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.getViewModel()).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewModel', () => {
|
||||
it('should throw error when not presented', () => {
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsors: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetSponsorshipPricingPresenter } from './GetSponsorshipPricingPresenter';
|
||||
|
||||
describe('GetSponsorshipPricingPresenter', () => {
|
||||
let presenter: GetSponsorshipPricingPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new GetSponsorshipPricingPresenter();
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { tiers: [] };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should store the result', () => {
|
||||
const mockResult = { tiers: [] };
|
||||
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewModel', () => {
|
||||
it('should return null when not presented', () => {
|
||||
expect(presenter.getViewModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { tiers: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.getViewModel()).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewModel', () => {
|
||||
it('should throw error when not presented', () => {
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { tiers: [] };
|
||||
presenter.present(mockResult);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
149
apps/api/src/domain/team/TeamController.test.ts
Normal file
149
apps/api/src/domain/team/TeamController.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { TeamController } from './TeamController';
|
||||
import { TeamService } from './TeamService';
|
||||
import type { Request } from 'express';
|
||||
import { CreateTeamInputDTO, UpdateTeamInputDTO } from './dtos/CreateTeamInputDTO';
|
||||
|
||||
describe('TeamController', () => {
|
||||
let controller: TeamController;
|
||||
let service: ReturnType<typeof vi.mocked<TeamService>>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TeamController],
|
||||
providers: [
|
||||
{
|
||||
provide: TeamService,
|
||||
useValue: {
|
||||
getAll: vi.fn(),
|
||||
getDetails: vi.fn(),
|
||||
getMembers: vi.fn(),
|
||||
getJoinRequests: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
getDriverTeam: vi.fn(),
|
||||
getMembership: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<TeamController>(TeamController);
|
||||
service = vi.mocked(module.get(TeamService));
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all teams', async () => {
|
||||
const result = { teams: [] };
|
||||
service.getAll.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getAll();
|
||||
|
||||
expect(service.getAll).toHaveBeenCalled();
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDetails', () => {
|
||||
it('should return team details', async () => {
|
||||
const teamId = 'team-123';
|
||||
const userId = 'user-456';
|
||||
const result = { id: teamId, name: 'Team' };
|
||||
service.getDetails.mockResolvedValue(result);
|
||||
|
||||
const mockReq: Partial<Request> = { ['user']: { userId } };
|
||||
|
||||
const response = await controller.getDetails(teamId, mockReq as Request);
|
||||
|
||||
expect(service.getDetails).toHaveBeenCalledWith(teamId, userId);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMembers', () => {
|
||||
it('should return team members', async () => {
|
||||
const teamId = 'team-123';
|
||||
const result = { members: [] };
|
||||
service.getMembers.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getMembers(teamId);
|
||||
|
||||
expect(service.getMembers).toHaveBeenCalledWith(teamId);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getJoinRequests', () => {
|
||||
it('should return join requests', async () => {
|
||||
const teamId = 'team-123';
|
||||
const result = { requests: [] };
|
||||
service.getJoinRequests.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getJoinRequests(teamId);
|
||||
|
||||
expect(service.getJoinRequests).toHaveBeenCalledWith(teamId);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create team', async () => {
|
||||
const input: CreateTeamInputDTO = { name: 'New Team' };
|
||||
const userId = 'user-123';
|
||||
const result = { teamId: 'team-456' };
|
||||
service.create.mockResolvedValue(result);
|
||||
|
||||
const mockReq: Partial<Request> = { ['user']: { userId } };
|
||||
|
||||
const response = await controller.create(input, mockReq as Request);
|
||||
|
||||
expect(service.create).toHaveBeenCalledWith(input, userId);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update team', async () => {
|
||||
const teamId = 'team-123';
|
||||
const input: UpdateTeamInputDTO = { name: 'Updated Team' };
|
||||
const userId = 'user-456';
|
||||
const result = { success: true };
|
||||
service.update.mockResolvedValue(result);
|
||||
|
||||
const mockReq: Partial<Request> = { ['user']: { userId } };
|
||||
|
||||
const response = await controller.update(teamId, input, mockReq as Request);
|
||||
|
||||
expect(service.update).toHaveBeenCalledWith(teamId, input, userId);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriverTeam', () => {
|
||||
it('should return driver team', async () => {
|
||||
const driverId = 'driver-123';
|
||||
const result = { teamId: 'team-456' };
|
||||
service.getDriverTeam.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getDriverTeam(driverId);
|
||||
|
||||
expect(service.getDriverTeam).toHaveBeenCalledWith(driverId);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMembership', () => {
|
||||
it('should return team membership', async () => {
|
||||
const teamId = 'team-123';
|
||||
const driverId = 'driver-456';
|
||||
const result = { role: 'member' };
|
||||
service.getMembership.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.getMembership(teamId, driverId);
|
||||
|
||||
expect(service.getMembership).toHaveBeenCalledWith(teamId, driverId);
|
||||
expect(response).toEqual(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
30
apps/api/src/domain/team/TeamModule.test.ts
Normal file
30
apps/api/src/domain/team/TeamModule.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TeamModule } from './TeamModule';
|
||||
import { TeamController } from './TeamController';
|
||||
import { TeamService } from './TeamService';
|
||||
|
||||
describe('TeamModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [TeamModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should compile the module', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide TeamController', () => {
|
||||
const controller = module.get<TeamController>(TeamController);
|
||||
expect(controller).toBeDefined();
|
||||
expect(controller).toBeInstanceOf(TeamController);
|
||||
});
|
||||
|
||||
it('should provide TeamService', () => {
|
||||
const service = module.get<TeamService>(TeamService);
|
||||
expect(service).toBeDefined();
|
||||
expect(service).toBeInstanceOf(TeamService);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,110 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { TeamService } from './TeamService';
|
||||
|
||||
// Import core interfaces
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import type { ITeamRepository } from '@core/racing/domain/repositories/ITeamRepository';
|
||||
import type { ITeamMembershipRepository } from '@core/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryTeamRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
|
||||
// Import use cases
|
||||
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
|
||||
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
|
||||
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
|
||||
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
|
||||
|
||||
// Define injection tokens
|
||||
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
|
||||
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
|
||||
export const TeamProviders: Provider[] = [
|
||||
TeamService,
|
||||
TeamService, // Provide the service itself
|
||||
{
|
||||
provide: TEAM_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IMAGE_SERVICE_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Use cases
|
||||
{
|
||||
provide: GetAllTeamsUseCase,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
|
||||
new GetAllTeamsUseCase(teamRepo, membershipRepo, logger),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GetTeamDetailsUseCase,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
|
||||
new GetTeamDetailsUseCase(teamRepo, membershipRepo),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GetTeamMembersUseCase,
|
||||
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
|
||||
new GetTeamMembersUseCase(membershipRepo, driverRepo, imageService, logger),
|
||||
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GetTeamJoinRequestsUseCase,
|
||||
useFactory: (membershipRepo: ITeamMembershipRepository, driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
|
||||
new GetTeamJoinRequestsUseCase(membershipRepo, driverRepo, imageService, logger),
|
||||
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: CreateTeamUseCase,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
|
||||
new CreateTeamUseCase(teamRepo, membershipRepo),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: UpdateTeamUseCase,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
|
||||
new UpdateTeamUseCase(teamRepo, membershipRepo),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GetDriverTeamUseCase,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
|
||||
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GetTeamMembershipUseCase,
|
||||
useFactory: (membershipRepo: ITeamMembershipRepository, logger: Logger) =>
|
||||
new GetTeamMembershipUseCase(membershipRepo, logger),
|
||||
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
@@ -5,8 +5,7 @@ import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriv
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
|
||||
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
|
||||
import { AllTeamsViewModel, DriverTeamViewModel, GetDriverTeamQuery } from './dto/TeamDto';
|
||||
import { TEAM_GET_ALL_USE_CASE_TOKEN, TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, TEAM_LOGGER_TOKEN } from './TeamProviders';
|
||||
import { AllTeamsViewModel, DriverTeamViewModel } from './dtos/TeamDto';
|
||||
|
||||
describe('TeamService', () => {
|
||||
let service: TeamService;
|
||||
@@ -31,138 +30,72 @@ describe('TeamService', () => {
|
||||
providers: [
|
||||
TeamService,
|
||||
{
|
||||
provide: TEAM_GET_ALL_USE_CASE_TOKEN,
|
||||
provide: GetAllTeamsUseCase,
|
||||
useValue: mockGetAllTeamsUseCase,
|
||||
},
|
||||
{
|
||||
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
|
||||
provide: GetDriverTeamUseCase,
|
||||
useValue: mockGetDriverTeamUseCase,
|
||||
},
|
||||
{
|
||||
provide: TEAM_LOGGER_TOKEN,
|
||||
provide: 'Logger',
|
||||
useValue: mockLogger,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TeamService>(TeamService);
|
||||
getAllTeamsUseCase = module.get(TEAM_GET_ALL_USE_CASE_TOKEN);
|
||||
getDriverTeamUseCase = module.get(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN);
|
||||
logger = module.get(TEAM_LOGGER_TOKEN);
|
||||
getAllTeamsUseCase = module.get(GetAllTeamsUseCase);
|
||||
getDriverTeamUseCase = module.get(GetDriverTeamUseCase);
|
||||
logger = module.get('Logger');
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAllTeams', () => {
|
||||
it('should create presenter, call use case, and return view model', async () => {
|
||||
const mockViewModel: AllTeamsViewModel = {
|
||||
teams: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
describe('getAll', () => {
|
||||
it('should call use case and return result', async () => {
|
||||
const mockResult = { isOk: () => true, value: { teams: [], totalCount: 0 } };
|
||||
getAllTeamsUseCase.execute.mockResolvedValue(mockResult as any);
|
||||
|
||||
const mockPresenter = {
|
||||
reset: jest.fn(),
|
||||
present: jest.fn(),
|
||||
get viewModel(): AllTeamsViewModel {
|
||||
return mockViewModel;
|
||||
},
|
||||
getViewModel: jest.fn().mockReturnValue({ teams: [], totalCount: 0 }),
|
||||
};
|
||||
|
||||
// Mock the presenter constructor
|
||||
const originalConstructor = AllTeamsPresenter;
|
||||
(AllTeamsPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
|
||||
|
||||
// Mock the use case to call the presenter
|
||||
getAllTeamsUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
presenter.present({ teams: [] });
|
||||
});
|
||||
const result = await service.getAll();
|
||||
|
||||
const result = await service.getAllTeams();
|
||||
|
||||
expect(AllTeamsPresenter).toHaveBeenCalled();
|
||||
expect(getAllTeamsUseCase.execute).toHaveBeenCalledWith(undefined, mockPresenter);
|
||||
expect(result).toBe(mockViewModel);
|
||||
|
||||
// Restore
|
||||
AllTeamsPresenter = originalConstructor;
|
||||
expect(getAllTeamsUseCase.execute).toHaveBeenCalled();
|
||||
expect(result).toEqual({ teams: [], totalCount: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDriverTeam', () => {
|
||||
it('should create presenter, call use case, and return view model', async () => {
|
||||
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' };
|
||||
const mockViewModel: DriverTeamViewModel = {
|
||||
team: {
|
||||
id: 'team1',
|
||||
name: 'Team 1',
|
||||
tag: 'T1',
|
||||
description: 'Description',
|
||||
ownerId: 'driver1',
|
||||
leagues: [],
|
||||
},
|
||||
membership: {
|
||||
role: 'owner' as any,
|
||||
joinedAt: new Date(),
|
||||
isActive: true,
|
||||
},
|
||||
isOwner: true,
|
||||
canManage: true,
|
||||
};
|
||||
it('should call use case and return result', async () => {
|
||||
const mockResult = { isOk: () => true, value: {} };
|
||||
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
|
||||
|
||||
const mockPresenter = {
|
||||
reset: jest.fn(),
|
||||
present: jest.fn(),
|
||||
get viewModel(): DriverTeamViewModel {
|
||||
return mockViewModel;
|
||||
},
|
||||
getViewModel: jest.fn().mockReturnValue({} as DriverTeamViewModel),
|
||||
};
|
||||
|
||||
// Mock the presenter constructor
|
||||
const originalConstructor = DriverTeamPresenter;
|
||||
(DriverTeamPresenter as any) = jest.fn().mockImplementation(() => mockPresenter);
|
||||
|
||||
// Mock the use case to call the presenter
|
||||
getDriverTeamUseCase.execute.mockImplementation(async (input, presenter) => {
|
||||
presenter.present({
|
||||
team: {
|
||||
id: 'team1',
|
||||
name: 'Team 1',
|
||||
tag: 'T1',
|
||||
description: 'Description',
|
||||
ownerId: 'driver1',
|
||||
leagues: [],
|
||||
},
|
||||
membership: {
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
},
|
||||
driverId: 'driver1',
|
||||
});
|
||||
});
|
||||
const result = await service.getDriverTeam('driver1');
|
||||
|
||||
const result = await service.getDriverTeam(query);
|
||||
|
||||
expect(DriverTeamPresenter).toHaveBeenCalled();
|
||||
expect(getDriverTeamUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver1' }, mockPresenter);
|
||||
expect(result).toBe(mockViewModel);
|
||||
|
||||
// Restore
|
||||
DriverTeamPresenter = originalConstructor;
|
||||
expect(getDriverTeamUseCase.execute).toHaveBeenCalledWith({ driverId: 'driver1' });
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return null on error', async () => {
|
||||
const query: GetDriverTeamQuery = { teamId: 'team1', driverId: 'driver1' };
|
||||
const mockResult = { isErr: () => true, error: {} };
|
||||
getDriverTeamUseCase.execute.mockResolvedValue(mockResult as any);
|
||||
|
||||
// Mock the use case to throw an error
|
||||
getDriverTeamUseCase.execute.mockRejectedValue(new Error('Team not found'));
|
||||
|
||||
const result = await service.getDriverTeam(query);
|
||||
const result = await service.getDriverTeam('driver1');
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { GetAllTeamsOutputDTO } from './dtos/GetAllTeamsOutputDTO';
|
||||
import { GetTeamDetailsOutputDTO } from './dtos/GetTeamDetailsOutputDTO';
|
||||
import { GetTeamMembersOutputDTO } from './dtos/GetTeamMembersOutputDTO';
|
||||
@@ -10,63 +10,162 @@ import { UpdateTeamOutputDTO } from './dtos/UpdateTeamOutputDTO';
|
||||
import { GetDriverTeamOutputDTO } from './dtos/GetDriverTeamOutputDTO';
|
||||
import { GetTeamMembershipOutputDTO } from './dtos/GetTeamMembershipOutputDTO';
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
|
||||
// Use cases
|
||||
import { GetAllTeamsUseCase } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
import { GetTeamDetailsUseCase } from '@core/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { GetTeamMembersUseCase } from '@core/racing/application/use-cases/GetTeamMembersUseCase';
|
||||
import { GetTeamJoinRequestsUseCase } from '@core/racing/application/use-cases/GetTeamJoinRequestsUseCase';
|
||||
import { CreateTeamUseCase } from '@core/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { UpdateTeamUseCase } from '@core/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { GetDriverTeamUseCase } from '@core/racing/application/use-cases/GetDriverTeamUseCase';
|
||||
import { GetTeamMembershipUseCase } from '@core/racing/application/use-cases/GetTeamMembershipUseCase';
|
||||
|
||||
// API Presenters
|
||||
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
|
||||
import { TeamDetailsPresenter } from './presenters/TeamDetailsPresenter';
|
||||
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
|
||||
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
|
||||
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './TeamProviders';
|
||||
|
||||
@Injectable()
|
||||
export class TeamService {
|
||||
constructor(
|
||||
private readonly getAllTeamsUseCase: GetAllTeamsUseCase,
|
||||
private readonly getTeamDetailsUseCase: GetTeamDetailsUseCase,
|
||||
private readonly getTeamMembersUseCase: GetTeamMembersUseCase,
|
||||
private readonly getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase,
|
||||
private readonly createTeamUseCase: CreateTeamUseCase,
|
||||
private readonly updateTeamUseCase: UpdateTeamUseCase,
|
||||
private readonly getDriverTeamUseCase: GetDriverTeamUseCase,
|
||||
private readonly getTeamMembershipUseCase: GetTeamMembershipUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async getAll(): Promise<GetAllTeamsOutputDTO> {
|
||||
// TODO: Implement getAll teams logic
|
||||
return {
|
||||
teams: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
this.logger.debug('[TeamService] Fetching all teams.');
|
||||
|
||||
const presenter = new AllTeamsPresenter();
|
||||
const result = await this.getAllTeamsUseCase.execute();
|
||||
if (result.isErr()) {
|
||||
this.logger.error('Error fetching all teams', result.error);
|
||||
return { teams: [], totalCount: 0 };
|
||||
}
|
||||
await presenter.present(result.value);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getDetails(teamId: string, userId?: string): Promise<GetTeamDetailsOutputDTO | null> {
|
||||
// TODO: Implement get team details logic
|
||||
return null;
|
||||
this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}, userId: ${userId}`);
|
||||
|
||||
const presenter = new TeamDetailsPresenter();
|
||||
const result = await this.getTeamDetailsUseCase.execute({ teamId, driverId: userId || '' });
|
||||
if (result.isErr()) {
|
||||
this.logger.error(`Error fetching team details for teamId: ${teamId}`, result.error);
|
||||
return null;
|
||||
}
|
||||
await presenter.present(result.value);
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async getMembers(teamId: string): Promise<GetTeamMembersOutputDTO> {
|
||||
// TODO: Implement get team members logic
|
||||
return {
|
||||
members: [],
|
||||
totalCount: 0,
|
||||
ownerCount: 0,
|
||||
managerCount: 0,
|
||||
memberCount: 0,
|
||||
};
|
||||
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
|
||||
|
||||
const presenter = new TeamMembersPresenter();
|
||||
const result = await this.getTeamMembersUseCase.execute({ teamId });
|
||||
if (result.isErr()) {
|
||||
this.logger.error(`Error fetching team members for teamId: ${teamId}`, result.error);
|
||||
return {
|
||||
members: [],
|
||||
totalCount: 0,
|
||||
ownerCount: 0,
|
||||
managerCount: 0,
|
||||
memberCount: 0,
|
||||
};
|
||||
}
|
||||
await presenter.present(result.value);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async getJoinRequests(teamId: string): Promise<GetTeamJoinRequestsOutputDTO> {
|
||||
// TODO: Implement get team join requests logic
|
||||
return {
|
||||
requests: [],
|
||||
pendingCount: 0,
|
||||
totalCount: 0,
|
||||
};
|
||||
this.logger.debug(`[TeamService] Fetching team join requests for teamId: ${teamId}`);
|
||||
|
||||
const presenter = new TeamJoinRequestsPresenter();
|
||||
const result = await this.getTeamJoinRequestsUseCase.execute({ teamId });
|
||||
if (result.isErr()) {
|
||||
this.logger.error(`Error fetching team join requests for teamId: ${teamId}`, result.error);
|
||||
return {
|
||||
requests: [],
|
||||
pendingCount: 0,
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
await presenter.present(result.value);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async create(input: CreateTeamInputDTO, userId?: string): Promise<CreateTeamOutputDTO> {
|
||||
// TODO: Implement create team logic
|
||||
return {
|
||||
id: 'placeholder-id',
|
||||
success: true,
|
||||
this.logger.debug('[TeamService] Creating team', { input, userId });
|
||||
|
||||
const command = {
|
||||
name: input.name,
|
||||
tag: input.tag,
|
||||
description: input.description,
|
||||
ownerId: userId || '',
|
||||
};
|
||||
const result = await this.createTeamUseCase.execute(command);
|
||||
if (result.isErr()) {
|
||||
this.logger.error('Error creating team', result.error);
|
||||
return { id: '', success: false };
|
||||
}
|
||||
return { id: result.value.id, success: true };
|
||||
}
|
||||
|
||||
async update(teamId: string, input: UpdateTeamInputDTO, userId?: string): Promise<UpdateTeamOutputDTO> {
|
||||
// TODO: Implement update team logic
|
||||
return {
|
||||
success: true,
|
||||
this.logger.debug(`[TeamService] Updating team ${teamId}`, { input, userId });
|
||||
|
||||
const command = {
|
||||
teamId,
|
||||
name: input.name,
|
||||
tag: input.tag,
|
||||
description: input.description,
|
||||
performerId: userId || '',
|
||||
};
|
||||
const result = await this.updateTeamUseCase.execute(command);
|
||||
if (result.isErr()) {
|
||||
this.logger.error(`Error updating team ${teamId}`, result.error);
|
||||
return { success: false };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
|
||||
// TODO: Implement get driver team logic
|
||||
return null;
|
||||
this.logger.debug(`[TeamService] Fetching driver team for driverId: ${driverId}`);
|
||||
|
||||
const result = await this.getDriverTeamUseCase.execute({ driverId });
|
||||
if (result.isErr()) {
|
||||
this.logger.error(`Error fetching driver team for driverId: ${driverId}`, result.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const presenter = new DriverTeamPresenter();
|
||||
await presenter.present(result.value);
|
||||
return presenter.getViewModel();
|
||||
}
|
||||
|
||||
async getMembership(teamId: string, driverId: string): Promise<GetTeamMembershipOutputDTO | null> {
|
||||
// TODO: Implement get team membership logic
|
||||
return null;
|
||||
this.logger.debug(`[TeamService] Fetching team membership for teamId: ${teamId}, driverId: ${driverId}`);
|
||||
|
||||
const result = await this.getTeamMembershipUseCase.execute({ teamId, driverId });
|
||||
if (result.isErr()) {
|
||||
this.logger.error(`Error fetching team membership for teamId: ${teamId}, driverId: ${driverId}`, result.error);
|
||||
return null;
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ITeamsLeaderboardPresenter, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
import { ITeamsLeaderboardPresenter, TeamsLeaderboardResultDTO, TeamsLeaderboardViewModel, TeamLeaderboardItemViewModel } from '@core/racing/application/presenters/ITeamsLeaderboardPresenter';
|
||||
|
||||
export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
|
||||
private result: TeamsLeaderboardViewModel | null = null;
|
||||
@@ -9,7 +9,7 @@ export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
|
||||
|
||||
present(dto: TeamsLeaderboardResultDTO) {
|
||||
this.result = {
|
||||
teams: dto.teams as any, // Cast to match the view model
|
||||
teams: dto.teams as TeamLeaderboardItemViewModel[],
|
||||
recruitingCount: dto.recruitingCount,
|
||||
groupsBySkillLevel: {
|
||||
beginner: [],
|
||||
@@ -17,7 +17,7 @@ export class TeamsLeaderboardPresenter implements ITeamsLeaderboardPresenter {
|
||||
advanced: [],
|
||||
pro: [],
|
||||
},
|
||||
topTeams: dto.teams.slice(0, 10) as any,
|
||||
topTeams: (dto.teams as TeamLeaderboardItemViewModel[]).slice(0, 10),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||
|
||||
export interface GetAnalyticsMetricsInput {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
export interface GetAnalyticsMetricsOutput {
|
||||
pageViews: number;
|
||||
uniqueVisitors: number;
|
||||
averageSessionDuration: number;
|
||||
bounceRate: number;
|
||||
}
|
||||
|
||||
export class GetAnalyticsMetricsUseCase {
|
||||
constructor(
|
||||
private readonly pageViewRepository: IPageViewRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: GetAnalyticsMetricsInput = {}): Promise<GetAnalyticsMetricsOutput> {
|
||||
try {
|
||||
const startDate = input.startDate ?? new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
|
||||
const endDate = input.endDate ?? new Date();
|
||||
|
||||
// For now, return placeholder values as actual implementation would require
|
||||
// aggregating data across all entities or specifying which entity
|
||||
// This is a simplified version
|
||||
const pageViews = 0;
|
||||
const uniqueVisitors = 0;
|
||||
const averageSessionDuration = 0;
|
||||
const bounceRate = 0;
|
||||
|
||||
this.logger.info('Analytics metrics retrieved', {
|
||||
startDate,
|
||||
endDate,
|
||||
pageViews,
|
||||
uniqueVisitors,
|
||||
});
|
||||
|
||||
return {
|
||||
pageViews,
|
||||
uniqueVisitors,
|
||||
averageSessionDuration,
|
||||
bounceRate,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get analytics metrics', { error, input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
export interface GetDashboardDataInput {}
|
||||
|
||||
export interface GetDashboardDataOutput {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
totalRaces: number;
|
||||
totalLeagues: number;
|
||||
}
|
||||
|
||||
export class GetDashboardDataUseCase {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(_input: GetDashboardDataInput = {}): Promise<GetDashboardDataOutput> {
|
||||
try {
|
||||
// Placeholder implementation - would need repositories from identity and racing domains
|
||||
const totalUsers = 0;
|
||||
const activeUsers = 0;
|
||||
const totalRaces = 0;
|
||||
const totalLeagues = 0;
|
||||
|
||||
this.logger.info('Dashboard data retrieved', {
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
totalRaces,
|
||||
totalLeagues,
|
||||
});
|
||||
|
||||
return {
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
totalRaces,
|
||||
totalLeagues,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get dashboard data', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
/**
|
||||
* Use Case: RecordEngagementUseCase
|
||||
*
|
||||
* Records an engagement event when a visitor interacts with an entity.
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { EngagementEvent, type EngagementAction, type EngagementEntityType } from '../../domain/entities/EngagementEvent';
|
||||
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
|
||||
import { EngagementEvent } from '../../domain/entities/EngagementEvent';
|
||||
import type { EngagementAction, EngagementEntityType } from '../../domain/types/EngagementEvent';
|
||||
|
||||
export interface RecordEngagementInput {
|
||||
action: EngagementAction;
|
||||
@@ -24,43 +18,41 @@ export interface RecordEngagementOutput {
|
||||
engagementWeight: number;
|
||||
}
|
||||
|
||||
export class RecordEngagementUseCase
|
||||
implements AsyncUseCase<RecordEngagementInput, RecordEngagementOutput> {
|
||||
export class RecordEngagementUseCase {
|
||||
constructor(
|
||||
private readonly engagementRepository: IEngagementRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||
this.logger.debug('Executing RecordEngagementUseCase', { input });
|
||||
try {
|
||||
const eventId = `eng-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof EngagementEvent.create>[0], 'timestamp'> = {
|
||||
id: eventId,
|
||||
const engagementEvent = EngagementEvent.create({
|
||||
id: crypto.randomUUID(),
|
||||
action: input.action,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
actorId: input.actorId,
|
||||
actorType: input.actorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const event = EngagementEvent.create({
|
||||
...baseProps,
|
||||
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
||||
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
||||
metadata: input.metadata,
|
||||
});
|
||||
|
||||
await this.engagementRepository.save(event);
|
||||
this.logger.info('Engagement recorded successfully', { eventId, input });
|
||||
await this.engagementRepository.save(engagementEvent);
|
||||
|
||||
this.logger.info('Engagement event recorded', {
|
||||
engagementId: engagementEvent.id,
|
||||
action: input.action,
|
||||
entityId: input.entityId,
|
||||
entityType: input.entityType,
|
||||
});
|
||||
|
||||
return {
|
||||
eventId,
|
||||
engagementWeight: event.getEngagementWeight(),
|
||||
eventId: engagementEvent.id,
|
||||
engagementWeight: engagementEvent.getEngagementWeight(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Error recording engagement', error instanceof Error ? error : new Error(String(error)), { input });
|
||||
this.logger.error('Failed to record engagement event', { error: error as Error, input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,7 @@
|
||||
/**
|
||||
* Use Case: RecordPageViewUseCase
|
||||
*
|
||||
* Records a page view event when a visitor accesses an entity page.
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||
import { PageView } from '../../domain/entities/PageView';
|
||||
import type { EntityType, VisitorType } from '../../domain/types/PageView';
|
||||
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
|
||||
|
||||
export interface RecordPageViewInput {
|
||||
entityType: EntityType;
|
||||
@@ -25,41 +18,40 @@ export interface RecordPageViewOutput {
|
||||
pageViewId: string;
|
||||
}
|
||||
|
||||
export class RecordPageViewUseCase
|
||||
implements AsyncUseCase<RecordPageViewInput, RecordPageViewOutput> {
|
||||
export class RecordPageViewUseCase {
|
||||
constructor(
|
||||
private readonly pageViewRepository: IPageViewRepository,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||
this.logger.debug('Executing RecordPageViewUseCase', { input });
|
||||
try {
|
||||
const pageViewId = `pv-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseProps: Omit<Parameters<typeof PageView.create>[0], 'timestamp'> = {
|
||||
id: pageViewId,
|
||||
const pageView = PageView.create({
|
||||
id: crypto.randomUUID(),
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
visitorId: input.visitorId,
|
||||
visitorType: input.visitorType,
|
||||
sessionId: input.sessionId,
|
||||
};
|
||||
|
||||
const pageView = PageView.create({
|
||||
...baseProps,
|
||||
...(input.visitorId !== undefined ? { visitorId: input.visitorId } : {}),
|
||||
...(input.referrer !== undefined ? { referrer: input.referrer } : {}),
|
||||
...(input.userAgent !== undefined ? { userAgent: input.userAgent } : {}),
|
||||
...(input.country !== undefined ? { country: input.country } : {}),
|
||||
referrer: input.referrer,
|
||||
userAgent: input.userAgent,
|
||||
country: input.country,
|
||||
});
|
||||
|
||||
await this.pageViewRepository.save(pageView);
|
||||
this.logger.info('Page view recorded successfully', { pageViewId, input });
|
||||
return { pageViewId };
|
||||
|
||||
this.logger.info('Page view recorded', {
|
||||
pageViewId: pageView.id,
|
||||
entityId: input.entityId,
|
||||
entityType: input.entityType,
|
||||
});
|
||||
|
||||
return {
|
||||
pageViewId: pageView.id,
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.logger.error('Error recording page view', err, { input });
|
||||
this.logger.error('Failed to record page view', { error, input });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
core/analytics/domain/repositories/IPageViewRepository.ts
Normal file
12
core/analytics/domain/repositories/IPageViewRepository.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { PageView } from '../entities/PageView';
|
||||
|
||||
export interface IPageViewRepository {
|
||||
save(pageView: PageView): Promise<void>;
|
||||
findById(id: string): Promise<PageView | null>;
|
||||
findByEntityId(entityId: string): Promise<PageView[]>;
|
||||
findBySessionId(sessionId: string): Promise<PageView[]>;
|
||||
countByEntityId(entityId: string): Promise<number>;
|
||||
getUniqueVisitorsCount(entityId: string, startDate: Date, endDate: Date): Promise<number>;
|
||||
getAverageSessionDuration(entityId: string, startDate: Date, endDate: Date): Promise<number>;
|
||||
getBounceRate(entityId: string, startDate: Date, endDate: Date): Promise<number>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user