resolve todos in website
This commit is contained in:
@@ -210,6 +210,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["core/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "ExportDefaultDeclaration",
|
||||
"message": "Default exports are forbidden. Use named exports instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetAnalyticsMetricsUseCase, type GetAnalyticsMetricsInput } from './GetAnalyticsMetricsUseCase';
|
||||
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetAnalyticsMetricsUseCase', () => {
|
||||
let pageViewRepository: {
|
||||
save: Mock;
|
||||
findById: Mock;
|
||||
findByEntityId: Mock;
|
||||
findBySessionId: Mock;
|
||||
countByEntityId: Mock;
|
||||
getUniqueVisitorsCount: Mock;
|
||||
getAverageSessionDuration: Mock;
|
||||
getBounceRate: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let useCase: GetAnalyticsMetricsUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
pageViewRepository = {
|
||||
save: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
findByEntityId: vi.fn(),
|
||||
findBySessionId: vi.fn(),
|
||||
countByEntityId: vi.fn(),
|
||||
getUniqueVisitorsCount: vi.fn(),
|
||||
getAverageSessionDuration: vi.fn(),
|
||||
getBounceRate: vi.fn(),
|
||||
} as unknown as IPageViewRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new GetAnalyticsMetricsUseCase(
|
||||
pageViewRepository as unknown as IPageViewRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns default metrics and logs retrieval when no input is provided', async () => {
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result).toEqual({
|
||||
pageViews: 0,
|
||||
uniqueVisitors: 0,
|
||||
averageSessionDuration: 0,
|
||||
bounceRate: 0,
|
||||
});
|
||||
|
||||
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses provided date range and logs error when execute throws', async () => {
|
||||
const input: GetAnalyticsMetricsInput = {
|
||||
startDate: new Date('2024-01-01'),
|
||||
endDate: new Date('2024-01-31'),
|
||||
};
|
||||
|
||||
const erroringUseCase = new GetAnalyticsMetricsUseCase(
|
||||
pageViewRepository as unknown as IPageViewRepository,
|
||||
logger,
|
||||
);
|
||||
|
||||
// Simulate an error by temporarily spying on logger.info to throw
|
||||
(logger.info as unknown as Mock).mockImplementation(() => {
|
||||
throw new Error('Logging failed');
|
||||
});
|
||||
|
||||
await expect(erroringUseCase.execute(input)).rejects.toThrow('Logging failed');
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { GetDashboardDataUseCase } from './GetDashboardDataUseCase';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
describe('GetDashboardDataUseCase', () => {
|
||||
let logger: Logger;
|
||||
let useCase: GetDashboardDataUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new GetDashboardDataUseCase(logger);
|
||||
});
|
||||
|
||||
it('returns placeholder dashboard metrics and logs retrieval', async () => {
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(result).toEqual({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
totalRaces: 0,
|
||||
totalLeagues: 0,
|
||||
});
|
||||
|
||||
expect((logger.info as unknown as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ export class GetDashboardDataUseCase {
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(_input: GetDashboardDataInput = {}): Promise<GetDashboardDataOutput> {
|
||||
async execute(): Promise<GetDashboardDataOutput> {
|
||||
try {
|
||||
// Placeholder implementation - would need repositories from identity and racing domains
|
||||
const totalUsers = 0;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetEntityAnalyticsQuery, type GetEntityAnalyticsInput } from './GetEntityAnalyticsQuery';
|
||||
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
|
||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||
import type { IAnalyticsSnapshotRepository } from '@core/analytics/domain/repositories/IAnalyticsSnapshotRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { EntityType } from '../../domain/types/PageView';
|
||||
|
||||
describe('GetEntityAnalyticsQuery', () => {
|
||||
let pageViewRepository: {
|
||||
countByEntityId: Mock;
|
||||
countUniqueVisitors: Mock;
|
||||
};
|
||||
let engagementRepository: {
|
||||
getSponsorClicksForEntity: Mock;
|
||||
};
|
||||
let snapshotRepository: IAnalyticsSnapshotRepository;
|
||||
let logger: Logger;
|
||||
let useCase: GetEntityAnalyticsQuery;
|
||||
|
||||
beforeEach(() => {
|
||||
pageViewRepository = {
|
||||
countByEntityId: vi.fn(),
|
||||
countUniqueVisitors: vi.fn(),
|
||||
} as unknown as IPageViewRepository as any;
|
||||
|
||||
engagementRepository = {
|
||||
getSponsorClicksForEntity: vi.fn(),
|
||||
} as unknown as IEngagementRepository as any;
|
||||
|
||||
snapshotRepository = {} as IAnalyticsSnapshotRepository;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new GetEntityAnalyticsQuery(
|
||||
pageViewRepository as unknown as IPageViewRepository,
|
||||
engagementRepository as unknown as IEngagementRepository,
|
||||
snapshotRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('aggregates entity analytics and returns summary and trends', async () => {
|
||||
const input: GetEntityAnalyticsInput = {
|
||||
entityType: 'league' as EntityType,
|
||||
entityId: 'league-1',
|
||||
period: 'weekly',
|
||||
};
|
||||
|
||||
pageViewRepository.countByEntityId
|
||||
.mockResolvedValueOnce(100) // current period total page views
|
||||
.mockResolvedValueOnce(150); // previous period full page views
|
||||
|
||||
pageViewRepository.countUniqueVisitors
|
||||
.mockResolvedValueOnce(40) // current period uniques
|
||||
.mockResolvedValueOnce(60); // previous period full uniques
|
||||
|
||||
engagementRepository.getSponsorClicksForEntity
|
||||
.mockResolvedValueOnce(10) // current clicks
|
||||
.mockResolvedValueOnce(5); // for engagement score
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(result.entityId).toBe(input.entityId);
|
||||
expect(result.entityType).toBe(input.entityType);
|
||||
|
||||
expect(result.summary.totalPageViews).toBe(100);
|
||||
expect(result.summary.uniqueVisitors).toBe(40);
|
||||
expect(result.summary.sponsorClicks).toBe(10);
|
||||
expect(typeof result.summary.engagementScore).toBe('number');
|
||||
expect(result.summary.exposureValue).toBeGreaterThan(0);
|
||||
|
||||
expect(result.trends.pageViewsChange).toBeDefined();
|
||||
expect(result.trends.uniqueVisitorsChange).toBeDefined();
|
||||
|
||||
expect(result.period.start).toBeInstanceOf(Date);
|
||||
expect(result.period.end).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('propagates repository errors', async () => {
|
||||
const input: GetEntityAnalyticsInput = {
|
||||
entityType: 'league' as EntityType,
|
||||
entityId: 'league-1',
|
||||
};
|
||||
|
||||
pageViewRepository.countByEntityId.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
await expect(useCase.execute(input)).rejects.toThrow('DB error');
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -5,8 +5,7 @@
|
||||
* Returns metrics formatted for display to sponsors and admins.
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import type { IPageViewRepository } from '../repositories/IPageViewRepository';
|
||||
import type { IEngagementRepository } from '@core/analytics/domain/repositories/IEngagementRepository';
|
||||
import type { IAnalyticsSnapshotRepository } from '@core/analytics/domain/repositories/IAnalyticsSnapshotRepository';
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { RecordEngagementUseCase, type RecordEngagementInput } from './RecordEngagementUseCase';
|
||||
import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository';
|
||||
import { EngagementEvent } from '../../domain/entities/EngagementEvent';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { EngagementAction, EngagementEntityType } from '../../domain/types/EngagementEvent';
|
||||
|
||||
describe('RecordEngagementUseCase', () => {
|
||||
let engagementRepository: {
|
||||
save: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let useCase: RecordEngagementUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
engagementRepository = {
|
||||
save: vi.fn(),
|
||||
} as unknown as IEngagementRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new RecordEngagementUseCase(
|
||||
engagementRepository as unknown as IEngagementRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates and saves an EngagementEvent and returns its id and weight', async () => {
|
||||
const input: RecordEngagementInput = {
|
||||
action: 'view' as EngagementAction,
|
||||
entityType: 'league' as EngagementEntityType,
|
||||
entityId: 'league-1',
|
||||
actorId: 'driver-1',
|
||||
actorType: 'driver',
|
||||
sessionId: 'session-1',
|
||||
metadata: { foo: 'bar' },
|
||||
};
|
||||
|
||||
engagementRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(engagementRepository.save).toHaveBeenCalledTimes(1);
|
||||
const saved = (engagementRepository.save as unknown as Mock).mock.calls[0][0] as EngagementEvent;
|
||||
|
||||
expect(saved).toBeInstanceOf(EngagementEvent);
|
||||
expect(saved.id).toBeDefined();
|
||||
expect(saved.entityId).toBe(input.entityId);
|
||||
expect(saved.entityType).toBe(input.entityType);
|
||||
expect(result.eventId).toBe(saved.id);
|
||||
expect(typeof result.engagementWeight).toBe('number');
|
||||
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs and rethrows when repository save fails', async () => {
|
||||
const input: RecordEngagementInput = {
|
||||
action: 'view' as EngagementAction,
|
||||
entityType: 'league' as EngagementEntityType,
|
||||
entityId: 'league-1',
|
||||
actorType: 'anonymous',
|
||||
sessionId: 'session-1',
|
||||
};
|
||||
|
||||
const error = new Error('DB error');
|
||||
engagementRepository.save.mockRejectedValue(error);
|
||||
|
||||
await expect(useCase.execute(input)).rejects.toThrow('DB error');
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { RecordPageViewUseCase, type RecordPageViewInput } from './RecordPageViewUseCase';
|
||||
import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository';
|
||||
import { PageView } from '../../domain/entities/PageView';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { EntityType, VisitorType } from '../../domain/types/PageView';
|
||||
|
||||
describe('RecordPageViewUseCase', () => {
|
||||
let pageViewRepository: {
|
||||
save: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let useCase: RecordPageViewUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
pageViewRepository = {
|
||||
save: vi.fn(),
|
||||
} as unknown as IPageViewRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new RecordPageViewUseCase(
|
||||
pageViewRepository as unknown as IPageViewRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates and saves a PageView and returns its id', async () => {
|
||||
const input: RecordPageViewInput = {
|
||||
entityType: 'league' as EntityType,
|
||||
entityId: 'league-1',
|
||||
visitorId: 'visitor-1',
|
||||
visitorType: 'anonymous' as VisitorType,
|
||||
sessionId: 'session-1',
|
||||
referrer: 'https://example.com',
|
||||
userAgent: 'jest',
|
||||
country: 'US',
|
||||
};
|
||||
|
||||
pageViewRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCase.execute(input);
|
||||
|
||||
expect(pageViewRepository.save).toHaveBeenCalledTimes(1);
|
||||
const saved = (pageViewRepository.save as unknown as Mock).mock.calls[0][0] as PageView;
|
||||
|
||||
expect(saved).toBeInstanceOf(PageView);
|
||||
expect(saved.id).toBeDefined();
|
||||
expect(saved.entityId).toBe(input.entityId);
|
||||
expect(saved.entityType).toBe(input.entityType);
|
||||
expect(result.pageViewId).toBe(saved.id);
|
||||
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs and rethrows when repository save fails', async () => {
|
||||
const input: RecordPageViewInput = {
|
||||
entityType: 'league' as EntityType,
|
||||
entityId: 'league-1',
|
||||
visitorType: 'anonymous' as VisitorType,
|
||||
sessionId: 'session-1',
|
||||
};
|
||||
|
||||
const error = new Error('DB error');
|
||||
pageViewRepository.save.mockRejectedValue(error);
|
||||
|
||||
await expect(useCase.execute(input)).rejects.toThrow('DB error');
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,3 @@ export * from './application/use-cases/RecordEngagementUseCase';
|
||||
export * from './application/use-cases/GetEntityAnalyticsQuery';
|
||||
|
||||
// Infrastructure (moved to adapters)
|
||||
export type { IPageViewRepository } from './application/repositories/IPageViewRepository';
|
||||
export type { IEngagementRepository } from './domain/repositories/IEngagementRepository';
|
||||
export type { IAnalyticsSnapshotRepository } from './domain/repositories/IAnalyticsSnapshotRepository';
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetCurrentUserSessionUseCase } from './GetCurrentUserSessionUseCase';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||
|
||||
describe('GetCurrentUserSessionUseCase', () => {
|
||||
let sessionPort: {
|
||||
getCurrentSession: Mock;
|
||||
createSession: Mock;
|
||||
clearSession: Mock;
|
||||
};
|
||||
|
||||
let useCase: GetCurrentUserSessionUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
sessionPort = {
|
||||
getCurrentSession: vi.fn(),
|
||||
createSession: vi.fn(),
|
||||
clearSession: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new GetCurrentUserSessionUseCase(sessionPort as unknown as IdentitySessionPort);
|
||||
});
|
||||
|
||||
it('returns the current auth session when one exists', async () => {
|
||||
const session: AuthSessionDTO = {
|
||||
user: {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
primaryDriverId: 'driver-1',
|
||||
},
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 1000,
|
||||
token: 'token-123',
|
||||
};
|
||||
|
||||
sessionPort.getCurrentSession.mockResolvedValue(session);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
|
||||
it('returns null when there is no active session', async () => {
|
||||
sessionPort.getCurrentSession.mockResolvedValue(null);
|
||||
|
||||
const result = await useCase.execute();
|
||||
|
||||
expect(sessionPort.getCurrentSession).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
48
core/identity/application/use-cases/GetUserUseCase.test.ts
Normal file
48
core/identity/application/use-cases/GetUserUseCase.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetUserUseCase } from './GetUserUseCase';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
|
||||
|
||||
describe('GetUserUseCase', () => {
|
||||
let userRepository: {
|
||||
findById: Mock;
|
||||
};
|
||||
|
||||
let useCase: GetUserUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {
|
||||
findById: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new GetUserUseCase(userRepository as unknown as IUserRepository);
|
||||
});
|
||||
|
||||
it('returns a User when the user exists', async () => {
|
||||
const storedUser: StoredUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
passwordHash: 'hash',
|
||||
salt: 'salt',
|
||||
primaryDriverId: 'driver-1',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
userRepository.findById.mockResolvedValue(storedUser);
|
||||
|
||||
const result = await useCase.execute('user-1');
|
||||
|
||||
expect(userRepository.findById).toHaveBeenCalledWith('user-1');
|
||||
expect(result).toBeInstanceOf(User);
|
||||
expect(result.getId().value).toBe('user-1');
|
||||
expect(result.getDisplayName()).toBe('Test User');
|
||||
});
|
||||
|
||||
it('throws when the user does not exist', async () => {
|
||||
userRepository.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(useCase.execute('missing-user')).rejects.toThrow('User not found');
|
||||
expect(userRepository.findById).toHaveBeenCalledWith('missing-user');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { HandleAuthCallbackUseCase } from './HandleAuthCallbackUseCase';
|
||||
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import type { AuthCallbackCommandDTO } from '../dto/AuthCallbackCommandDTO';
|
||||
import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||
|
||||
describe('HandleAuthCallbackUseCase', () => {
|
||||
let provider: {
|
||||
completeAuth: Mock;
|
||||
};
|
||||
let sessionPort: {
|
||||
createSession: Mock;
|
||||
getCurrentSession: Mock;
|
||||
clearSession: Mock;
|
||||
};
|
||||
let useCase: HandleAuthCallbackUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = {
|
||||
completeAuth: vi.fn(),
|
||||
};
|
||||
sessionPort = {
|
||||
createSession: vi.fn(),
|
||||
getCurrentSession: vi.fn(),
|
||||
clearSession: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new HandleAuthCallbackUseCase(
|
||||
provider as unknown as IdentityProviderPort,
|
||||
sessionPort as unknown as IdentitySessionPort,
|
||||
);
|
||||
});
|
||||
|
||||
it('completes auth and creates a session', async () => {
|
||||
const command: AuthCallbackCommandDTO = {
|
||||
code: 'auth-code',
|
||||
state: 'state-123',
|
||||
redirectUri: 'https://app/callback',
|
||||
};
|
||||
|
||||
const user: AuthenticatedUserDTO = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
};
|
||||
|
||||
const session: AuthSessionDTO = {
|
||||
user,
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 1000,
|
||||
token: 'session-token',
|
||||
};
|
||||
|
||||
provider.completeAuth.mockResolvedValue(user);
|
||||
sessionPort.createSession.mockResolvedValue(session);
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(provider.completeAuth).toHaveBeenCalledWith(command);
|
||||
expect(sessionPort.createSession).toHaveBeenCalledWith(user);
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
});
|
||||
81
core/identity/application/use-cases/LoginUseCase.test.ts
Normal file
81
core/identity/application/use-cases/LoginUseCase.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { LoginUseCase } from './LoginUseCase';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
|
||||
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||
import { User } from '../../domain/entities/User';
|
||||
|
||||
describe('LoginUseCase', () => {
|
||||
let authRepo: {
|
||||
findByEmail: Mock;
|
||||
};
|
||||
let passwordService: {
|
||||
verify: Mock;
|
||||
};
|
||||
let useCase: LoginUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
authRepo = {
|
||||
findByEmail: vi.fn(),
|
||||
};
|
||||
passwordService = {
|
||||
verify: vi.fn(),
|
||||
};
|
||||
useCase = new LoginUseCase(
|
||||
authRepo as unknown as IAuthRepository,
|
||||
passwordService as unknown as IPasswordHashingService,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the user when credentials are valid', async () => {
|
||||
const email = 'test@example.com';
|
||||
const password = 'password123';
|
||||
const emailVO = EmailAddress.create(email);
|
||||
|
||||
const user = User.create({
|
||||
id: { value: 'user-1' } as any,
|
||||
displayName: 'Test User',
|
||||
email: emailVO.value,
|
||||
});
|
||||
|
||||
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
|
||||
|
||||
authRepo.findByEmail.mockResolvedValue(user);
|
||||
passwordService.verify.mockResolvedValue(true);
|
||||
|
||||
const result = await useCase.execute(email, password);
|
||||
|
||||
expect(authRepo.findByEmail).toHaveBeenCalledWith(emailVO);
|
||||
expect(passwordService.verify).toHaveBeenCalledWith(password, 'stored-hash');
|
||||
expect(result).toBe(user);
|
||||
});
|
||||
|
||||
it('throws when user is not found', async () => {
|
||||
const email = 'missing@example.com';
|
||||
|
||||
authRepo.findByEmail.mockResolvedValue(null);
|
||||
|
||||
await expect(useCase.execute(email, 'password')).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
|
||||
it('throws when password is invalid', async () => {
|
||||
const email = 'test@example.com';
|
||||
const password = 'wrong-password';
|
||||
const emailVO = EmailAddress.create(email);
|
||||
|
||||
const user = User.create({
|
||||
id: { value: 'user-1' } as any,
|
||||
displayName: 'Test User',
|
||||
email: emailVO.value,
|
||||
});
|
||||
|
||||
(user as any).getPasswordHash = () => ({ value: 'stored-hash' });
|
||||
|
||||
authRepo.findByEmail.mockResolvedValue(user);
|
||||
passwordService.verify.mockResolvedValue(false);
|
||||
|
||||
await expect(useCase.execute(email, password)).rejects.toThrow('Invalid credentials');
|
||||
expect(authRepo.findByEmail).toHaveBeenCalled();
|
||||
expect(passwordService.verify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { LoginWithEmailUseCase, type LoginCommandDTO } from './LoginWithEmailUseCase';
|
||||
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||
|
||||
describe('LoginWithEmailUseCase', () => {
|
||||
let userRepository: {
|
||||
findByEmail: Mock;
|
||||
};
|
||||
let sessionPort: {
|
||||
createSession: Mock;
|
||||
getCurrentSession: Mock;
|
||||
clearSession: Mock;
|
||||
};
|
||||
let useCase: LoginWithEmailUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {
|
||||
findByEmail: vi.fn(),
|
||||
};
|
||||
sessionPort = {
|
||||
createSession: vi.fn(),
|
||||
getCurrentSession: vi.fn(),
|
||||
clearSession: vi.fn(),
|
||||
};
|
||||
useCase = new LoginWithEmailUseCase(
|
||||
userRepository as unknown as IUserRepository,
|
||||
sessionPort as unknown as IdentitySessionPort,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a session for valid credentials', async () => {
|
||||
const command: LoginCommandDTO = {
|
||||
email: 'Test@Example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
passwordHash: 'hashed-password',
|
||||
salt: 'salt',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const session: AuthSessionDTO = {
|
||||
user: {
|
||||
id: storedUser.id,
|
||||
email: storedUser.email,
|
||||
displayName: storedUser.displayName,
|
||||
},
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 1000,
|
||||
token: 'token-123',
|
||||
};
|
||||
|
||||
userRepository.findByEmail.mockResolvedValue(storedUser);
|
||||
sessionPort.createSession.mockResolvedValue(session);
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(userRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
|
||||
expect(sessionPort.createSession).toHaveBeenCalledWith({
|
||||
id: storedUser.id,
|
||||
email: storedUser.email,
|
||||
displayName: storedUser.displayName,
|
||||
});
|
||||
expect(result).toEqual(session);
|
||||
});
|
||||
|
||||
it('throws when email or password is missing', async () => {
|
||||
await expect(useCase.execute({ email: '', password: 'x' })).rejects.toThrow('Email and password are required');
|
||||
await expect(useCase.execute({ email: 'a@example.com', password: '' })).rejects.toThrow('Email and password are required');
|
||||
});
|
||||
|
||||
it('throws when user does not exist', async () => {
|
||||
const command: LoginCommandDTO = {
|
||||
email: 'missing@example.com',
|
||||
password: 'password',
|
||||
};
|
||||
|
||||
userRepository.findByEmail.mockResolvedValue(null);
|
||||
|
||||
await expect(useCase.execute(command)).rejects.toThrow('Invalid email or password');
|
||||
});
|
||||
|
||||
it('throws when password is invalid', async () => {
|
||||
const command: LoginCommandDTO = {
|
||||
email: 'test@example.com',
|
||||
password: 'wrong',
|
||||
};
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
passwordHash: 'different-hash',
|
||||
salt: 'salt',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
userRepository.findByEmail.mockResolvedValue(storedUser);
|
||||
|
||||
await expect(useCase.execute(command)).rejects.toThrow('Invalid email or password');
|
||||
});
|
||||
});
|
||||
28
core/identity/application/use-cases/LogoutUseCase.test.ts
Normal file
28
core/identity/application/use-cases/LogoutUseCase.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { LogoutUseCase } from './LogoutUseCase';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
|
||||
describe('LogoutUseCase', () => {
|
||||
let sessionPort: {
|
||||
clearSession: Mock;
|
||||
getCurrentSession: Mock;
|
||||
createSession: Mock;
|
||||
};
|
||||
let useCase: LogoutUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
sessionPort = {
|
||||
clearSession: vi.fn(),
|
||||
getCurrentSession: vi.fn(),
|
||||
createSession: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new LogoutUseCase(sessionPort as unknown as IdentitySessionPort);
|
||||
});
|
||||
|
||||
it('clears the current session', async () => {
|
||||
await useCase.execute();
|
||||
|
||||
expect(sessionPort.clearSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
71
core/identity/application/use-cases/SignupUseCase.test.ts
Normal file
71
core/identity/application/use-cases/SignupUseCase.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { SignupUseCase } from './SignupUseCase';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { UserId } from '../../domain/value-objects/UserId';
|
||||
import { User } from '../../domain/entities/User';
|
||||
import type { IAuthRepository } from '../../domain/repositories/IAuthRepository';
|
||||
import type { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||
|
||||
vi.mock('../../domain/value-objects/PasswordHash', () => ({
|
||||
PasswordHash: {
|
||||
fromHash: (hash: string) => ({ value: hash }),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SignupUseCase', () => {
|
||||
let authRepo: {
|
||||
findByEmail: Mock;
|
||||
save: Mock;
|
||||
};
|
||||
let passwordService: {
|
||||
hash: Mock;
|
||||
};
|
||||
let useCase: SignupUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
authRepo = {
|
||||
findByEmail: vi.fn(),
|
||||
save: vi.fn(),
|
||||
};
|
||||
passwordService = {
|
||||
hash: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new SignupUseCase(
|
||||
authRepo as unknown as IAuthRepository,
|
||||
passwordService as unknown as IPasswordHashingService,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates and saves a new user when email is free', async () => {
|
||||
const email = 'new@example.com';
|
||||
const password = 'password123';
|
||||
const displayName = 'New User';
|
||||
|
||||
authRepo.findByEmail.mockResolvedValue(null);
|
||||
passwordService.hash.mockResolvedValue('hashed-password');
|
||||
|
||||
const result = await useCase.execute(email, password, displayName);
|
||||
|
||||
expect(authRepo.findByEmail).toHaveBeenCalledWith(EmailAddress.create(email));
|
||||
expect(passwordService.hash).toHaveBeenCalledWith(password);
|
||||
expect(authRepo.save).toHaveBeenCalled();
|
||||
|
||||
expect(result).toBeInstanceOf(User);
|
||||
expect(result.getDisplayName()).toBe(displayName);
|
||||
});
|
||||
|
||||
it('throws when user already exists', async () => {
|
||||
const email = 'existing@example.com';
|
||||
|
||||
const existingUser = User.create({
|
||||
id: UserId.create(),
|
||||
displayName: 'Existing User',
|
||||
email,
|
||||
});
|
||||
|
||||
authRepo.findByEmail.mockResolvedValue(existingUser);
|
||||
|
||||
await expect(useCase.execute(email, 'password', 'Existing User')).rejects.toThrow('User already exists');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { SignupWithEmailUseCase, type SignupCommandDTO } from './SignupWithEmailUseCase';
|
||||
import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository';
|
||||
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
|
||||
import type { AuthSessionDTO } from '../dto/AuthSessionDTO';
|
||||
|
||||
describe('SignupWithEmailUseCase', () => {
|
||||
let userRepository: {
|
||||
findByEmail: Mock;
|
||||
create: Mock;
|
||||
};
|
||||
let sessionPort: {
|
||||
createSession: Mock;
|
||||
getCurrentSession: Mock;
|
||||
clearSession: Mock;
|
||||
};
|
||||
let useCase: SignupWithEmailUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {
|
||||
findByEmail: vi.fn(),
|
||||
create: vi.fn(),
|
||||
};
|
||||
sessionPort = {
|
||||
createSession: vi.fn(),
|
||||
getCurrentSession: vi.fn(),
|
||||
clearSession: vi.fn(),
|
||||
};
|
||||
useCase = new SignupWithEmailUseCase(
|
||||
userRepository as unknown as IUserRepository,
|
||||
sessionPort as unknown as IdentitySessionPort,
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a new user and session for valid input', async () => {
|
||||
const command: SignupCommandDTO = {
|
||||
email: 'new@example.com',
|
||||
password: 'password123',
|
||||
displayName: 'New User',
|
||||
};
|
||||
|
||||
userRepository.findByEmail.mockResolvedValue(null);
|
||||
|
||||
const session: AuthSessionDTO = {
|
||||
user: {
|
||||
id: 'user-1',
|
||||
email: command.email.toLowerCase(),
|
||||
displayName: command.displayName,
|
||||
},
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 1000,
|
||||
token: 'session-token',
|
||||
};
|
||||
|
||||
sessionPort.createSession.mockResolvedValue(session);
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(userRepository.findByEmail).toHaveBeenCalledWith(command.email);
|
||||
expect(userRepository.create).toHaveBeenCalled();
|
||||
expect(sessionPort.createSession).toHaveBeenCalledWith({
|
||||
id: expect.any(String),
|
||||
email: command.email.toLowerCase(),
|
||||
displayName: command.displayName,
|
||||
});
|
||||
|
||||
expect(result.session).toEqual(session);
|
||||
expect(result.isNewUser).toBe(true);
|
||||
});
|
||||
|
||||
it('throws when email format is invalid', async () => {
|
||||
const command: SignupCommandDTO = {
|
||||
email: 'invalid-email',
|
||||
password: 'password123',
|
||||
displayName: 'User',
|
||||
};
|
||||
|
||||
await expect(useCase.execute(command)).rejects.toThrow('Invalid email format');
|
||||
});
|
||||
|
||||
it('throws when password is too short', async () => {
|
||||
const command: SignupCommandDTO = {
|
||||
email: 'valid@example.com',
|
||||
password: 'short',
|
||||
displayName: 'User',
|
||||
};
|
||||
|
||||
await expect(useCase.execute(command)).rejects.toThrow('Password must be at least 8 characters');
|
||||
});
|
||||
|
||||
it('throws when display name is too short', async () => {
|
||||
const command: SignupCommandDTO = {
|
||||
email: 'valid@example.com',
|
||||
password: 'password123',
|
||||
displayName: ' ',
|
||||
};
|
||||
|
||||
await expect(useCase.execute(command)).rejects.toThrow('Display name must be at least 2 characters');
|
||||
});
|
||||
|
||||
it('throws when email already exists', async () => {
|
||||
const command: SignupCommandDTO = {
|
||||
email: 'existing@example.com',
|
||||
password: 'password123',
|
||||
displayName: 'Existing User',
|
||||
};
|
||||
|
||||
const existingUser: StoredUser = {
|
||||
id: 'user-1',
|
||||
email: command.email,
|
||||
displayName: command.displayName,
|
||||
passwordHash: 'hash',
|
||||
salt: 'salt',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
userRepository.findByEmail.mockResolvedValue(existingUser);
|
||||
|
||||
await expect(useCase.execute(command)).rejects.toThrow('An account with this email already exists');
|
||||
});
|
||||
});
|
||||
35
core/identity/application/use-cases/StartAuthUseCase.test.ts
Normal file
35
core/identity/application/use-cases/StartAuthUseCase.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { StartAuthUseCase } from './StartAuthUseCase';
|
||||
import type { IdentityProviderPort } from '../ports/IdentityProviderPort';
|
||||
import type { StartAuthCommandDTO } from '../dto/StartAuthCommandDTO';
|
||||
|
||||
describe('StartAuthUseCase', () => {
|
||||
let provider: {
|
||||
startAuth: Mock;
|
||||
};
|
||||
let useCase: StartAuthUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = {
|
||||
startAuth: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new StartAuthUseCase(provider as unknown as IdentityProviderPort);
|
||||
});
|
||||
|
||||
it('delegates to the identity provider to start auth', async () => {
|
||||
const command: StartAuthCommandDTO = {
|
||||
redirectUri: 'https://app/callback',
|
||||
provider: 'demo',
|
||||
};
|
||||
|
||||
const expected = { redirectUrl: 'https://auth/redirect', state: 'state-123' };
|
||||
|
||||
provider.startAuth.mockResolvedValue(expected);
|
||||
|
||||
const result = await useCase.execute(command);
|
||||
|
||||
expect(provider.startAuth).toHaveBeenCalledWith(command);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { CreateAchievementUseCase, type IAchievementRepository } from './CreateAchievementUseCase';
|
||||
import { Achievement } from '@core/identity/domain/entities/Achievement';
|
||||
|
||||
describe('CreateAchievementUseCase', () => {
|
||||
let achievementRepository: {
|
||||
save: Mock;
|
||||
findById: Mock;
|
||||
};
|
||||
let useCase: CreateAchievementUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
achievementRepository = {
|
||||
save: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
};
|
||||
|
||||
useCase = new CreateAchievementUseCase(achievementRepository as unknown as IAchievementRepository);
|
||||
});
|
||||
|
||||
it('creates an achievement and persists it', async () => {
|
||||
const props = {
|
||||
id: 'achv-1',
|
||||
name: 'First Win',
|
||||
description: 'Awarded for winning your first race',
|
||||
category: 'driver' as const,
|
||||
rarity: 'common' as const,
|
||||
iconUrl: 'https://example.com/icon.png',
|
||||
points: 50,
|
||||
requirements: [
|
||||
{
|
||||
type: 'wins',
|
||||
value: 1,
|
||||
operator: '>=',
|
||||
},
|
||||
],
|
||||
isSecret: false,
|
||||
};
|
||||
|
||||
achievementRepository.save.mockResolvedValue(undefined);
|
||||
|
||||
const result = await useCase.execute(props);
|
||||
|
||||
expect(result).toBeInstanceOf(Achievement);
|
||||
expect(result.id).toBe(props.id);
|
||||
expect(result.name).toBe(props.name);
|
||||
expect(result.description).toBe(props.description);
|
||||
expect(result.category).toBe(props.category);
|
||||
expect(result.points).toBe(props.points);
|
||||
expect(result.requirements).toHaveLength(1);
|
||||
expect(achievementRepository.save).toHaveBeenCalledWith(result);
|
||||
});
|
||||
});
|
||||
@@ -11,9 +11,6 @@ export * from './domain/repositories/ISponsorAccountRepository';
|
||||
export * from './domain/repositories/IUserRatingRepository';
|
||||
export * from './domain/repositories/IAchievementRepository';
|
||||
|
||||
export * from './infrastructure/repositories/InMemoryUserRatingRepository';
|
||||
export * from './infrastructure/repositories/InMemoryAchievementRepository';
|
||||
|
||||
export * from './application/dto/AuthenticatedUserDTO';
|
||||
export * from './application/dto/AuthSessionDTO';
|
||||
export * from './application/dto/AuthCallbackCommandDTO';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetLeagueStandingsUseCaseImpl } from './GetLeagueStandingsUseCaseImpl';
|
||||
import type { ILeagueStandingsRepository, RawStanding } from '../ports/ILeagueStandingsRepository';
|
||||
|
||||
describe('GetLeagueStandingsUseCaseImpl', () => {
|
||||
let repository: {
|
||||
getLeagueStandings: Mock;
|
||||
};
|
||||
let useCase: GetLeagueStandingsUseCaseImpl;
|
||||
|
||||
beforeEach(() => {
|
||||
repository = {
|
||||
getLeagueStandings: vi.fn(),
|
||||
} as unknown as ILeagueStandingsRepository as any;
|
||||
|
||||
useCase = new GetLeagueStandingsUseCaseImpl(repository as unknown as ILeagueStandingsRepository);
|
||||
});
|
||||
|
||||
it('maps raw standings from repository to view model', async () => {
|
||||
const leagueId = 'league-1';
|
||||
const rawStandings: RawStanding[] = [
|
||||
{
|
||||
id: 's1',
|
||||
leagueId,
|
||||
seasonId: 'season-1',
|
||||
driverId: 'driver-1',
|
||||
position: 1,
|
||||
points: 100,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
racesCompleted: 10,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
leagueId,
|
||||
seasonId: null,
|
||||
driverId: 'driver-2',
|
||||
position: 2,
|
||||
points: 80,
|
||||
wins: 1,
|
||||
podiums: null,
|
||||
racesCompleted: 10,
|
||||
},
|
||||
];
|
||||
|
||||
repository.getLeagueStandings.mockResolvedValue(rawStandings);
|
||||
|
||||
const result = await useCase.execute(leagueId);
|
||||
|
||||
expect(repository.getLeagueStandings).toHaveBeenCalledWith(leagueId);
|
||||
expect(result.leagueId).toBe(leagueId);
|
||||
expect(result.standings).toEqual([
|
||||
{
|
||||
id: 's1',
|
||||
leagueId,
|
||||
seasonId: 'season-1',
|
||||
driverId: 'driver-1',
|
||||
position: 1,
|
||||
points: 100,
|
||||
wins: 3,
|
||||
podiums: 5,
|
||||
racesCompleted: 10,
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
leagueId,
|
||||
seasonId: '',
|
||||
driverId: 'driver-2',
|
||||
position: 2,
|
||||
points: 80,
|
||||
wins: 1,
|
||||
podiums: 0,
|
||||
racesCompleted: 10,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
97
core/media/application/use-cases/DeleteMediaUseCase.test.ts
Normal file
97
core/media/application/use-cases/DeleteMediaUseCase.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { DeleteMediaUseCase } from './DeleteMediaUseCase';
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { MediaStoragePort } from '../ports/MediaStoragePort';
|
||||
import type { IDeleteMediaPresenter } from '../presenters/IDeleteMediaPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Media } from '../../domain/entities/Media';
|
||||
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||
|
||||
describe('DeleteMediaUseCase', () => {
|
||||
let mediaRepo: {
|
||||
findById: Mock;
|
||||
delete: Mock;
|
||||
};
|
||||
let mediaStorage: {
|
||||
deleteMedia: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let presenter: IDeleteMediaPresenter & { result?: any };
|
||||
let useCase: DeleteMediaUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mediaRepo = {
|
||||
findById: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
} as unknown as IMediaRepository as any;
|
||||
|
||||
mediaStorage = {
|
||||
deleteMedia: vi.fn(),
|
||||
} as unknown as MediaStoragePort as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
presenter = {
|
||||
present: vi.fn((result) => {
|
||||
(presenter as any).result = result;
|
||||
}),
|
||||
} as unknown as IDeleteMediaPresenter & { result?: any };
|
||||
|
||||
useCase = new DeleteMediaUseCase(
|
||||
mediaRepo as unknown as IMediaRepository,
|
||||
mediaStorage as unknown as MediaStoragePort,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error result when media is not found', async () => {
|
||||
mediaRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute({ mediaId: 'missing' }, presenter);
|
||||
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('missing');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes media from storage and repository on success', async () => {
|
||||
const media = Media.create({
|
||||
id: 'media-1',
|
||||
filename: 'file.png',
|
||||
originalName: 'file.png',
|
||||
mimeType: 'image/png',
|
||||
size: 123,
|
||||
url: MediaUrl.create('https://example.com/file.png'),
|
||||
type: 'image',
|
||||
uploadedBy: 'user-1',
|
||||
});
|
||||
|
||||
mediaRepo.findById.mockResolvedValue(media);
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('media-1');
|
||||
expect(mediaStorage.deleteMedia).toHaveBeenCalledWith(media.url.value);
|
||||
expect(mediaRepo.delete).toHaveBeenCalledWith('media-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({ success: true });
|
||||
});
|
||||
|
||||
it('handles errors and presents failure result', async () => {
|
||||
mediaRepo.findById.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while deleting media',
|
||||
});
|
||||
});
|
||||
});
|
||||
93
core/media/application/use-cases/GetAvatarUseCase.test.ts
Normal file
93
core/media/application/use-cases/GetAvatarUseCase.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetAvatarUseCase } from './GetAvatarUseCase';
|
||||
import type { IAvatarRepository } from '../../domain/repositories/IAvatarRepository';
|
||||
import type { IGetAvatarPresenter } from '../presenters/IGetAvatarPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Avatar } from '../../domain/entities/Avatar';
|
||||
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||
|
||||
interface TestPresenter extends IGetAvatarPresenter {
|
||||
result?: any;
|
||||
}
|
||||
|
||||
describe('GetAvatarUseCase', () => {
|
||||
let avatarRepo: {
|
||||
findActiveByDriverId: Mock;
|
||||
save: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let presenter: TestPresenter;
|
||||
let useCase: GetAvatarUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
avatarRepo = {
|
||||
findActiveByDriverId: vi.fn(),
|
||||
save: vi.fn(),
|
||||
} as unknown as IAvatarRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
presenter = {
|
||||
present: vi.fn((result) => {
|
||||
presenter.result = result;
|
||||
}),
|
||||
} as unknown as TestPresenter;
|
||||
|
||||
useCase = new GetAvatarUseCase(
|
||||
avatarRepo as unknown as IAvatarRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('presents error when no avatar exists for driver', async () => {
|
||||
avatarRepo.findActiveByDriverId.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||
|
||||
expect(avatarRepo.findActiveByDriverId).toHaveBeenCalledWith('driver-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Avatar not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('presents avatar details when avatar exists', async () => {
|
||||
const avatar = Avatar.create({
|
||||
id: 'avatar-1',
|
||||
driverId: 'driver-1',
|
||||
mediaUrl: MediaUrl.create('https://example.com/avatar.png'),
|
||||
});
|
||||
|
||||
avatarRepo.findActiveByDriverId.mockResolvedValue(avatar);
|
||||
|
||||
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||
|
||||
expect(avatarRepo.findActiveByDriverId).toHaveBeenCalledWith('driver-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
avatar: {
|
||||
id: avatar.id,
|
||||
driverId: avatar.driverId,
|
||||
mediaUrl: avatar.mediaUrl.value,
|
||||
selectedAt: avatar.selectedAt,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles errors by logging and presenting failure', async () => {
|
||||
avatarRepo.findActiveByDriverId.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
await useCase.execute({ driverId: 'driver-1' }, presenter);
|
||||
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving avatar',
|
||||
});
|
||||
});
|
||||
});
|
||||
102
core/media/application/use-cases/GetMediaUseCase.test.ts
Normal file
102
core/media/application/use-cases/GetMediaUseCase.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetMediaUseCase } from './GetMediaUseCase';
|
||||
import type { IMediaRepository } from '../../domain/repositories/IMediaRepository';
|
||||
import type { IGetMediaPresenter } from '../presenters/IGetMediaPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Media } from '../../domain/entities/Media';
|
||||
import { MediaUrl } from '../../domain/value-objects/MediaUrl';
|
||||
|
||||
interface TestPresenter extends IGetMediaPresenter {
|
||||
result?: any;
|
||||
}
|
||||
|
||||
describe('GetMediaUseCase', () => {
|
||||
let mediaRepo: {
|
||||
findById: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let presenter: TestPresenter;
|
||||
let useCase: GetMediaUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
mediaRepo = {
|
||||
findById: vi.fn(),
|
||||
} as unknown as IMediaRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
presenter = {
|
||||
present: vi.fn((result) => {
|
||||
presenter.result = result;
|
||||
}),
|
||||
} as unknown as TestPresenter;
|
||||
|
||||
useCase = new GetMediaUseCase(
|
||||
mediaRepo as unknown as IMediaRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('presents error when media is not found', async () => {
|
||||
mediaRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute({ mediaId: 'missing' }, presenter);
|
||||
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('missing');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Media not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('presents media details when media exists', async () => {
|
||||
const media = Media.create({
|
||||
id: 'media-1',
|
||||
filename: 'file.png',
|
||||
originalName: 'file.png',
|
||||
mimeType: 'image/png',
|
||||
size: 123,
|
||||
url: MediaUrl.create('https://example.com/file.png'),
|
||||
type: 'image',
|
||||
uploadedBy: 'user-1',
|
||||
});
|
||||
|
||||
mediaRepo.findById.mockResolvedValue(media);
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
|
||||
expect(mediaRepo.findById).toHaveBeenCalledWith('media-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
media: {
|
||||
id: media.id,
|
||||
filename: media.filename,
|
||||
originalName: media.originalName,
|
||||
mimeType: media.mimeType,
|
||||
size: media.size,
|
||||
url: media.url.value,
|
||||
type: media.type,
|
||||
uploadedBy: media.uploadedBy,
|
||||
uploadedAt: media.uploadedAt,
|
||||
metadata: media.metadata,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles errors by logging and presenting failure', async () => {
|
||||
mediaRepo.findById.mockRejectedValue(new Error('DB error'));
|
||||
|
||||
await useCase.execute({ mediaId: 'media-1' }, presenter);
|
||||
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Internal error occurred while retrieving media',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { RequestAvatarGenerationUseCase } from './RequestAvatarGenerationUseCase';
|
||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||
import type { FaceValidationPort } from '../ports/FaceValidationPort';
|
||||
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||
import type { RequestAvatarGenerationInput } from './RequestAvatarGenerationUseCase';
|
||||
|
||||
describe('RequestAvatarGenerationUseCase', () => {
|
||||
let avatarRepo: {
|
||||
save: Mock;
|
||||
findById: Mock;
|
||||
};
|
||||
let faceValidation: {
|
||||
validateFacePhoto: Mock;
|
||||
};
|
||||
let avatarGeneration: {
|
||||
generateAvatars: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let useCase: RequestAvatarGenerationUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
avatarRepo = {
|
||||
save: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
} as unknown as IAvatarGenerationRepository as any;
|
||||
|
||||
faceValidation = {
|
||||
validateFacePhoto: vi.fn(),
|
||||
} as unknown as FaceValidationPort as any;
|
||||
|
||||
avatarGeneration = {
|
||||
generateAvatars: vi.fn(),
|
||||
} as unknown as AvatarGenerationPort as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new RequestAvatarGenerationUseCase(
|
||||
avatarRepo as unknown as IAvatarGenerationRepository,
|
||||
faceValidation as unknown as FaceValidationPort,
|
||||
avatarGeneration as unknown as AvatarGenerationPort,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
const createPresenter = () => {
|
||||
const presenter: { present: Mock; result?: any } = {
|
||||
present: vi.fn((result) => {
|
||||
presenter.result = result;
|
||||
}),
|
||||
result: undefined,
|
||||
};
|
||||
return presenter;
|
||||
};
|
||||
|
||||
it('fails when face validation fails', async () => {
|
||||
const input: RequestAvatarGenerationInput = {
|
||||
userId: 'user-1',
|
||||
facePhotoData: 'photo-data',
|
||||
suitColor: 'red',
|
||||
style: 'realistic',
|
||||
};
|
||||
|
||||
const presenter = createPresenter();
|
||||
|
||||
faceValidation.validateFacePhoto.mockResolvedValue({
|
||||
isValid: false,
|
||||
hasFace: false,
|
||||
faceCount: 0,
|
||||
errorMessage: 'No face detected',
|
||||
});
|
||||
|
||||
await useCase.execute(input, presenter as any);
|
||||
|
||||
expect((presenter.present as Mock)).toHaveBeenCalledWith({
|
||||
requestId: expect.any(String),
|
||||
status: 'failed',
|
||||
errorMessage: 'No face detected',
|
||||
});
|
||||
});
|
||||
|
||||
it('completes request and returns avatar URLs on success', async () => {
|
||||
const input: RequestAvatarGenerationInput = {
|
||||
userId: 'user-1',
|
||||
facePhotoData: 'photo-data',
|
||||
suitColor: 'red',
|
||||
style: 'realistic',
|
||||
};
|
||||
|
||||
const presenter = createPresenter();
|
||||
|
||||
faceValidation.validateFacePhoto.mockResolvedValue({
|
||||
isValid: true,
|
||||
hasFace: true,
|
||||
faceCount: 1,
|
||||
});
|
||||
|
||||
avatarGeneration.generateAvatars.mockResolvedValue({
|
||||
success: true,
|
||||
avatars: [
|
||||
{ url: 'https://example.com/avatar1.png' },
|
||||
{ url: 'https://example.com/avatar2.png' },
|
||||
],
|
||||
});
|
||||
|
||||
await useCase.execute(input, presenter as any);
|
||||
|
||||
expect(faceValidation.validateFacePhoto).toHaveBeenCalled();
|
||||
expect(avatarGeneration.generateAvatars).toHaveBeenCalled();
|
||||
expect((presenter.present as Mock)).toHaveBeenCalledWith({
|
||||
requestId: expect.any(String),
|
||||
status: 'completed',
|
||||
avatarUrls: [
|
||||
'https://example.com/avatar1.png',
|
||||
'https://example.com/avatar2.png',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
76
core/media/application/use-cases/SelectAvatarUseCase.test.ts
Normal file
76
core/media/application/use-cases/SelectAvatarUseCase.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { SelectAvatarUseCase } from './SelectAvatarUseCase';
|
||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||
import type { ISelectAvatarPresenter } from '../presenters/ISelectAvatarPresenter';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||
|
||||
interface TestPresenter extends ISelectAvatarPresenter {
|
||||
result?: any;
|
||||
}
|
||||
|
||||
describe('SelectAvatarUseCase', () => {
|
||||
let avatarRepo: {
|
||||
findById: Mock;
|
||||
save: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
let presenter: TestPresenter;
|
||||
let useCase: SelectAvatarUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
avatarRepo = {
|
||||
findById: vi.fn(),
|
||||
save: vi.fn(),
|
||||
} as unknown as IAvatarGenerationRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
presenter = {
|
||||
present: vi.fn((result) => {
|
||||
presenter.result = result;
|
||||
}),
|
||||
} as unknown as TestPresenter;
|
||||
|
||||
useCase = new SelectAvatarUseCase(
|
||||
avatarRepo as unknown as IAvatarGenerationRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error when request is not found', async () => {
|
||||
avatarRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute({ requestId: 'req-1', selectedIndex: 0 }, presenter);
|
||||
|
||||
expect(avatarRepo.findById).toHaveBeenCalledWith('req-1');
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation request not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when request is not completed', async () => {
|
||||
const request = AvatarGenerationRequest.create({
|
||||
id: 'req-1',
|
||||
userId: 'user-1',
|
||||
facePhotoUrl: 'photo',
|
||||
suitColor: 'red',
|
||||
style: 'realistic',
|
||||
});
|
||||
|
||||
avatarRepo.findById.mockResolvedValue(request);
|
||||
|
||||
await useCase.execute({ requestId: 'req-1', selectedIndex: 0 }, presenter);
|
||||
|
||||
expect((presenter.present as unknown as Mock)).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
errorMessage: 'Avatar generation is not completed yet',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetUnreadNotificationsUseCase } from './GetUnreadNotificationsUseCase';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Notification } from '../../domain/entities/Notification';
|
||||
|
||||
interface NotificationRepositoryMock {
|
||||
findUnreadByRecipientId: Mock;
|
||||
}
|
||||
|
||||
describe('GetUnreadNotificationsUseCase', () => {
|
||||
let notificationRepository: NotificationRepositoryMock;
|
||||
let logger: Logger;
|
||||
let useCase: GetUnreadNotificationsUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationRepository = {
|
||||
findUnreadByRecipientId: vi.fn(),
|
||||
} as unknown as INotificationRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new GetUnreadNotificationsUseCase(
|
||||
notificationRepository as unknown as INotificationRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns unread notifications and total count', async () => {
|
||||
const recipientId = 'driver-1';
|
||||
const notifications: Notification[] = [
|
||||
Notification.create({
|
||||
id: 'n1',
|
||||
recipientId,
|
||||
type: 'info',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
}),
|
||||
];
|
||||
|
||||
notificationRepository.findUnreadByRecipientId.mockResolvedValue(notifications);
|
||||
|
||||
const result = await useCase.execute(recipientId);
|
||||
|
||||
expect(notificationRepository.findUnreadByRecipientId).toHaveBeenCalledWith(recipientId);
|
||||
expect(result.notifications).toEqual(notifications);
|
||||
expect(result.totalCount).toBe(1);
|
||||
});
|
||||
|
||||
it('handles repository errors by logging and rethrowing', async () => {
|
||||
const recipientId = 'driver-1';
|
||||
const error = new Error('DB error');
|
||||
notificationRepository.findUnreadByRecipientId.mockRejectedValue(error);
|
||||
|
||||
await expect(useCase.execute(recipientId)).rejects.toThrow('DB error');
|
||||
expect((logger.error as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,7 @@
|
||||
* Retrieves unread notifications for a recipient.
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import type { Notification } from '../../domain/entities/Notification';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { MarkNotificationReadUseCase } from './MarkNotificationReadUseCase';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import { Notification } from '../../domain/entities/Notification';
|
||||
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||
|
||||
interface NotificationRepositoryMock {
|
||||
findById: Mock;
|
||||
update: Mock;
|
||||
markAllAsReadByRecipientId: Mock;
|
||||
}
|
||||
|
||||
describe('MarkNotificationReadUseCase', () => {
|
||||
let notificationRepository: NotificationRepositoryMock;
|
||||
let logger: Logger;
|
||||
let useCase: MarkNotificationReadUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationRepository = {
|
||||
findById: vi.fn(),
|
||||
update: vi.fn(),
|
||||
markAllAsReadByRecipientId: vi.fn(),
|
||||
} as unknown as INotificationRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
|
||||
useCase = new MarkNotificationReadUseCase(
|
||||
notificationRepository as unknown as INotificationRepository,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when notification is not found', async () => {
|
||||
notificationRepository.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
useCase.execute({ notificationId: 'n1', recipientId: 'driver-1' }),
|
||||
).rejects.toThrow(NotificationDomainError);
|
||||
|
||||
expect((logger.warn as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when recipientId does not match', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-2',
|
||||
type: 'info',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
notificationRepository.findById.mockResolvedValue(notification);
|
||||
|
||||
await expect(
|
||||
useCase.execute({ notificationId: 'n1', recipientId: 'driver-1' }),
|
||||
).rejects.toThrow(NotificationDomainError);
|
||||
});
|
||||
|
||||
it('marks notification as read when unread', async () => {
|
||||
const notification = Notification.create({
|
||||
id: 'n1',
|
||||
recipientId: 'driver-1',
|
||||
type: 'info',
|
||||
title: 'Test',
|
||||
body: 'Body',
|
||||
channel: 'in_app',
|
||||
});
|
||||
|
||||
notificationRepository.findById.mockResolvedValue(notification);
|
||||
|
||||
await useCase.execute({ notificationId: 'n1', recipientId: 'driver-1' });
|
||||
|
||||
expect(notificationRepository.update).toHaveBeenCalled();
|
||||
expect((logger.info as unknown as Mock)).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,9 @@
|
||||
* Marks a notification as read.
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
export interface MarkNotificationReadCommand {
|
||||
notificationId: string;
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import {
|
||||
GetNotificationPreferencesQuery,
|
||||
UpdateChannelPreferenceUseCase,
|
||||
UpdateTypePreferenceUseCase,
|
||||
UpdateQuietHoursUseCase,
|
||||
SetDigestModeUseCase,
|
||||
} from './NotificationPreferencesUseCases';
|
||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||
import type { NotificationPreference , ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { NotificationChannel, NotificationType } from '../../domain/types/NotificationTypes';
|
||||
import { NotificationDomainError } from '../../domain/errors/NotificationDomainError';
|
||||
|
||||
describe('NotificationPreferencesUseCases', () => {
|
||||
let preferenceRepository: {
|
||||
getOrCreateDefault: Mock;
|
||||
save: Mock;
|
||||
};
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(() => {
|
||||
preferenceRepository = {
|
||||
getOrCreateDefault: vi.fn(),
|
||||
save: vi.fn(),
|
||||
} as unknown as INotificationPreferenceRepository as any;
|
||||
|
||||
logger = {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
} as unknown as Logger;
|
||||
});
|
||||
|
||||
it('GetNotificationPreferencesQuery returns preferences from repository', async () => {
|
||||
const preference = {
|
||||
id: 'pref-1',
|
||||
} as unknown as NotificationPreference;
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const useCase = new GetNotificationPreferencesQuery(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
logger,
|
||||
);
|
||||
|
||||
const result = await useCase.execute('driver-1');
|
||||
|
||||
expect(preferenceRepository.getOrCreateDefault).toHaveBeenCalledWith('driver-1');
|
||||
expect(result).toBe(preference);
|
||||
});
|
||||
|
||||
it('UpdateChannelPreferenceUseCase updates channel preference', async () => {
|
||||
const preference = {
|
||||
updateChannel: vi.fn().mockReturnThis(),
|
||||
} as unknown as NotificationPreference;
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const useCase = new UpdateChannelPreferenceUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
logger,
|
||||
);
|
||||
|
||||
await useCase.execute({
|
||||
driverId: 'driver-1',
|
||||
channel: 'email' as NotificationChannel,
|
||||
preference: 'enabled' as ChannelPreference,
|
||||
});
|
||||
|
||||
expect(preference.updateChannel).toHaveBeenCalled();
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
});
|
||||
|
||||
it('UpdateTypePreferenceUseCase updates type preference', async () => {
|
||||
const preference = {
|
||||
updateTypePreference: vi.fn().mockReturnThis(),
|
||||
} as unknown as NotificationPreference;
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const useCase = new UpdateTypePreferenceUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
logger,
|
||||
);
|
||||
|
||||
await useCase.execute({
|
||||
driverId: 'driver-1',
|
||||
type: 'info' as NotificationType,
|
||||
preference: 'enabled' as TypePreference,
|
||||
});
|
||||
|
||||
expect(preference.updateTypePreference).toHaveBeenCalled();
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
});
|
||||
|
||||
it('UpdateQuietHoursUseCase validates hours and updates preferences', async () => {
|
||||
const preference = {
|
||||
updateQuietHours: vi.fn().mockReturnThis(),
|
||||
} as unknown as NotificationPreference;
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const useCase = new UpdateQuietHoursUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
logger,
|
||||
);
|
||||
|
||||
await useCase.execute({
|
||||
driverId: 'driver-1',
|
||||
startHour: 22,
|
||||
endHour: 7,
|
||||
});
|
||||
|
||||
expect(preference.updateQuietHours).toHaveBeenCalledWith(22, 7);
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
});
|
||||
|
||||
it('UpdateQuietHoursUseCase throws on invalid hours', async () => {
|
||||
const useCase = new UpdateQuietHoursUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
logger,
|
||||
);
|
||||
|
||||
await expect(
|
||||
useCase.execute({ driverId: 'd1', startHour: -1, endHour: 10 }),
|
||||
).rejects.toThrow(NotificationDomainError);
|
||||
|
||||
await expect(
|
||||
useCase.execute({ driverId: 'd1', startHour: 10, endHour: 24 }),
|
||||
).rejects.toThrow(NotificationDomainError);
|
||||
});
|
||||
|
||||
it('SetDigestModeUseCase sets digest mode with valid frequency', async () => {
|
||||
const preference = {
|
||||
setDigestMode: vi.fn().mockReturnThis(),
|
||||
} as unknown as NotificationPreference;
|
||||
|
||||
preferenceRepository.getOrCreateDefault.mockResolvedValue(preference);
|
||||
|
||||
const useCase = new SetDigestModeUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
);
|
||||
|
||||
await useCase.execute({
|
||||
driverId: 'driver-1',
|
||||
enabled: true,
|
||||
frequencyHours: 4,
|
||||
});
|
||||
|
||||
expect(preference.setDigestMode).toHaveBeenCalledWith(true, 4);
|
||||
expect(preferenceRepository.save).toHaveBeenCalledWith(preference);
|
||||
});
|
||||
|
||||
it('SetDigestModeUseCase throws on invalid frequency', async () => {
|
||||
const useCase = new SetDigestModeUseCase(
|
||||
preferenceRepository as unknown as INotificationPreferenceRepository,
|
||||
);
|
||||
|
||||
await expect(
|
||||
useCase.execute({ driverId: 'driver-1', enabled: true, frequencyHours: 0 }),
|
||||
).rejects.toThrow(NotificationDomainError);
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,7 @@
|
||||
* Manages user notification preferences.
|
||||
*/
|
||||
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { NotificationPreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { ChannelPreference, TypePreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||
|
||||
@@ -7,5 +7,5 @@ export { InMemoryNotificationRepository } from './repositories/InMemoryNotificat
|
||||
export { InMemoryNotificationPreferenceRepository } from './repositories/InMemoryNotificationPreferenceRepository';
|
||||
|
||||
// Adapters
|
||||
export { InAppNotificationAdapter } from './/InAppNotificationAdapter';
|
||||
export { NotificationGatewayRegistry } from './/NotificationGatewayRegistry';
|
||||
export { InAppNotificationAdapter } from "./InAppNotificationAdapter";
|
||||
export { NotificationGatewayRegistry } from "./NotificationGatewayRegistry";
|
||||
@@ -0,0 +1,133 @@
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { GetMembershipFeesUseCase, type GetMembershipFeesInput } from './GetMembershipFeesUseCase';
|
||||
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '../../domain/repositories/IMembershipFeeRepository';
|
||||
import type { IGetMembershipFeesPresenter, GetMembershipFeesResultDTO, GetMembershipFeesViewModel } from '../presenters/IGetMembershipFeesPresenter';
|
||||
|
||||
interface TestPresenter extends IGetMembershipFeesPresenter {
|
||||
reset: Mock;
|
||||
present: Mock;
|
||||
lastDto?: GetMembershipFeesResultDTO;
|
||||
viewModel?: GetMembershipFeesViewModel;
|
||||
}
|
||||
|
||||
describe('GetMembershipFeesUseCase', () => {
|
||||
let membershipFeeRepository: {
|
||||
findByLeagueId: Mock;
|
||||
};
|
||||
let memberPaymentRepository: {
|
||||
findByLeagueIdAndDriverId: Mock;
|
||||
};
|
||||
let presenter: TestPresenter;
|
||||
let useCase: GetMembershipFeesUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
membershipFeeRepository = {
|
||||
findByLeagueId: vi.fn(),
|
||||
} as unknown as IMembershipFeeRepository as any;
|
||||
|
||||
memberPaymentRepository = {
|
||||
findByLeagueIdAndDriverId: vi.fn(),
|
||||
} as unknown as IMemberPaymentRepository as any;
|
||||
|
||||
presenter = {
|
||||
reset: vi.fn(),
|
||||
present: vi.fn((dto: GetMembershipFeesResultDTO) => {
|
||||
presenter.lastDto = dto;
|
||||
}),
|
||||
toViewModel: vi.fn((dto: GetMembershipFeesResultDTO) => ({
|
||||
fee: dto.fee,
|
||||
payments: dto.payments,
|
||||
})),
|
||||
} as unknown as TestPresenter;
|
||||
|
||||
useCase = new GetMembershipFeesUseCase(
|
||||
membershipFeeRepository as unknown as IMembershipFeeRepository,
|
||||
memberPaymentRepository as unknown as IMemberPaymentRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when leagueId is missing', async () => {
|
||||
const input = { leagueId: '' } as GetMembershipFeesInput;
|
||||
|
||||
await expect(useCase.execute(input, presenter)).rejects.toThrow('leagueId is required');
|
||||
expect(presenter.reset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns null fee and empty payments when no fee exists', async () => {
|
||||
const input: GetMembershipFeesInput = { leagueId: 'league-1' };
|
||||
|
||||
membershipFeeRepository.findByLeagueId.mockResolvedValue(null);
|
||||
|
||||
await useCase.execute(input, presenter);
|
||||
|
||||
expect(membershipFeeRepository.findByLeagueId).toHaveBeenCalledWith('league-1');
|
||||
expect(memberPaymentRepository.findByLeagueIdAndDriverId).not.toHaveBeenCalled();
|
||||
expect(presenter.present).toHaveBeenCalledWith({
|
||||
fee: null,
|
||||
payments: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps fee and payments when fee and driverId are provided', async () => {
|
||||
const input: GetMembershipFeesInput = { leagueId: 'league-1', driverId: 'driver-1' };
|
||||
|
||||
const fee = {
|
||||
id: 'fee-1',
|
||||
leagueId: 'league-1',
|
||||
seasonId: 'season-1',
|
||||
type: 'season',
|
||||
amount: 100,
|
||||
enabled: true,
|
||||
createdAt: new Date('2024-01-01'),
|
||||
updatedAt: new Date('2024-01-02'),
|
||||
};
|
||||
|
||||
const payments = [
|
||||
{
|
||||
id: 'pay-1',
|
||||
feeId: 'fee-1',
|
||||
driverId: 'driver-1',
|
||||
amount: 100,
|
||||
platformFee: 5,
|
||||
netAmount: 95,
|
||||
status: 'paid',
|
||||
dueDate: new Date('2024-02-01'),
|
||||
paidAt: new Date('2024-01-15'),
|
||||
},
|
||||
];
|
||||
|
||||
membershipFeeRepository.findByLeagueId.mockResolvedValue(fee);
|
||||
memberPaymentRepository.findByLeagueIdAndDriverId.mockResolvedValue(payments);
|
||||
|
||||
await useCase.execute(input, presenter);
|
||||
|
||||
expect(membershipFeeRepository.findByLeagueId).toHaveBeenCalledWith('league-1');
|
||||
expect(memberPaymentRepository.findByLeagueIdAndDriverId).toHaveBeenCalledWith('league-1', 'driver-1', membershipFeeRepository as unknown as IMembershipFeeRepository);
|
||||
|
||||
expect(presenter.present).toHaveBeenCalledWith({
|
||||
fee: {
|
||||
id: fee.id,
|
||||
leagueId: fee.leagueId,
|
||||
seasonId: fee.seasonId,
|
||||
type: fee.type,
|
||||
amount: fee.amount,
|
||||
enabled: fee.enabled,
|
||||
createdAt: fee.createdAt,
|
||||
updatedAt: fee.updatedAt,
|
||||
},
|
||||
payments: [
|
||||
{
|
||||
id: 'pay-1',
|
||||
feeId: 'fee-1',
|
||||
driverId: 'driver-1',
|
||||
amount: 100,
|
||||
platformFee: 5,
|
||||
netAmount: 95,
|
||||
status: 'paid',
|
||||
dueDate: payments[0].dueDate,
|
||||
paidAt: payments[0].paidAt,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
374
core/racing/application/services/SeasonApplicationService.ts
Normal file
374
core/racing/application/services/SeasonApplicationService.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import { Season } from '../../domain/entities/Season';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { LeagueConfigFormModel } from '../dto/LeagueConfigFormDTO';
|
||||
import { SeasonSchedule } from '../../domain/value-objects/SeasonSchedule';
|
||||
import { SeasonScoringConfig } from '../../domain/value-objects/SeasonScoringConfig';
|
||||
import { SeasonDropPolicy } from '../../domain/value-objects/SeasonDropPolicy';
|
||||
import { SeasonStewardingConfig } from '../../domain/value-objects/SeasonStewardingConfig';
|
||||
import { RaceTimeOfDay } from '../../domain/value-objects/RaceTimeOfDay';
|
||||
import { LeagueTimezone } from '../../domain/value-objects/LeagueTimezone';
|
||||
import { RecurrenceStrategyFactory } from '../../domain/value-objects/RecurrenceStrategy';
|
||||
import { WeekdaySet } from '../../domain/value-objects/WeekdaySet';
|
||||
import { MonthlyRecurrencePattern } from '../../domain/value-objects/MonthlyRecurrencePattern';
|
||||
import type { Weekday } from '../../domain/types/Weekday';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface CreateSeasonForLeagueCommand {
|
||||
leagueId: string;
|
||||
name: string;
|
||||
gameId: string;
|
||||
sourceSeasonId?: string;
|
||||
config?: LeagueConfigFormModel;
|
||||
}
|
||||
|
||||
export interface CreateSeasonForLeagueResultDTO {
|
||||
seasonId: string;
|
||||
}
|
||||
|
||||
export interface SeasonSummaryDTO {
|
||||
seasonId: string;
|
||||
leagueId: string;
|
||||
name: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
export interface ListSeasonsForLeagueQuery {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export interface ListSeasonsForLeagueResultDTO {
|
||||
items: SeasonSummaryDTO[];
|
||||
}
|
||||
|
||||
export interface GetSeasonDetailsQuery {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
}
|
||||
|
||||
export interface SeasonDetailsDTO {
|
||||
seasonId: string;
|
||||
leagueId: string;
|
||||
gameId: string;
|
||||
name: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
maxDrivers?: number;
|
||||
schedule?: {
|
||||
startDate: Date;
|
||||
plannedRounds: number;
|
||||
};
|
||||
scoring?: {
|
||||
scoringPresetId: string;
|
||||
customScoringEnabled: boolean;
|
||||
};
|
||||
dropPolicy?: {
|
||||
strategy: import('../../domain/value-objects/SeasonDropPolicy').SeasonDropStrategy;
|
||||
n?: number;
|
||||
};
|
||||
stewarding?: {
|
||||
decisionMode: import('../../domain/entities/League').StewardingDecisionMode;
|
||||
requiredVotes?: number;
|
||||
requireDefense: boolean;
|
||||
defenseTimeLimit: number;
|
||||
voteTimeLimit: number;
|
||||
protestDeadlineHours: number;
|
||||
stewardingClosesHours: number;
|
||||
notifyAccusedOnProtest: boolean;
|
||||
notifyOnVoteRequired: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type SeasonLifecycleTransition = 'activate' | 'complete' | 'archive' | 'cancel';
|
||||
|
||||
export interface ManageSeasonLifecycleCommand {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
transition: SeasonLifecycleTransition;
|
||||
}
|
||||
|
||||
export interface ManageSeasonLifecycleResultDTO {
|
||||
seasonId: string;
|
||||
status: import('../../domain/entities/Season').SeasonStatus;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
|
||||
export class SeasonApplicationService {
|
||||
constructor(
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
private readonly seasonRepository: ISeasonRepository,
|
||||
) {}
|
||||
|
||||
async createSeasonForLeague(command: CreateSeasonForLeagueCommand): Promise<CreateSeasonForLeagueResultDTO> {
|
||||
const league = await this.leagueRepository.findById(command.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${command.leagueId}`);
|
||||
}
|
||||
|
||||
let baseSeasonProps: {
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
} = {};
|
||||
|
||||
if (command.sourceSeasonId) {
|
||||
const source = await this.seasonRepository.findById(command.sourceSeasonId);
|
||||
if (!source) {
|
||||
throw new Error(`Source Season not found: ${command.sourceSeasonId}`);
|
||||
}
|
||||
baseSeasonProps = {
|
||||
...(source.schedule !== undefined ? { schedule: source.schedule } : {}),
|
||||
...(source.scoringConfig !== undefined ? { scoringConfig: source.scoringConfig } : {}),
|
||||
...(source.dropPolicy !== undefined ? { dropPolicy: source.dropPolicy } : {}),
|
||||
...(source.stewardingConfig !== undefined ? { stewardingConfig: source.stewardingConfig } : {}),
|
||||
...(source.maxDrivers !== undefined ? { maxDrivers: source.maxDrivers } : {}),
|
||||
};
|
||||
} else if (command.config) {
|
||||
baseSeasonProps = this.deriveSeasonPropsFromConfig(command.config);
|
||||
}
|
||||
|
||||
const seasonId = uuidv4();
|
||||
|
||||
const season = Season.create({
|
||||
id: seasonId,
|
||||
leagueId: league.id,
|
||||
gameId: command.gameId,
|
||||
name: command.name,
|
||||
year: new Date().getFullYear(),
|
||||
status: 'planned',
|
||||
...(baseSeasonProps?.schedule ? { schedule: baseSeasonProps.schedule } : {}),
|
||||
...(baseSeasonProps?.scoringConfig ? { scoringConfig: baseSeasonProps.scoringConfig } : {}),
|
||||
...(baseSeasonProps?.dropPolicy ? { dropPolicy: baseSeasonProps.dropPolicy } : {}),
|
||||
...(baseSeasonProps?.stewardingConfig ? { stewardingConfig: baseSeasonProps.stewardingConfig } : {}),
|
||||
...(baseSeasonProps?.maxDrivers !== undefined ? { maxDrivers: baseSeasonProps.maxDrivers } : {}),
|
||||
});
|
||||
|
||||
await this.seasonRepository.add(season);
|
||||
|
||||
return { seasonId };
|
||||
}
|
||||
|
||||
async listSeasonsForLeague(query: ListSeasonsForLeagueQuery): Promise<ListSeasonsForLeagueResultDTO> {
|
||||
const league = await this.leagueRepository.findById(query.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${query.leagueId}`);
|
||||
}
|
||||
|
||||
const seasons = await this.seasonRepository.listByLeague(league.id);
|
||||
const items: SeasonSummaryDTO[] = seasons.map((s) => ({
|
||||
seasonId: s.id,
|
||||
leagueId: s.leagueId,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
...(s.startDate !== undefined ? { startDate: s.startDate } : {}),
|
||||
...(s.endDate !== undefined ? { endDate: s.endDate } : {}),
|
||||
isPrimary: false,
|
||||
}));
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
async getSeasonDetails(query: GetSeasonDetailsQuery): Promise<SeasonDetailsDTO> {
|
||||
const league = await this.leagueRepository.findById(query.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${query.leagueId}`);
|
||||
}
|
||||
|
||||
const season = await this.seasonRepository.findById(query.seasonId);
|
||||
if (!season || season.leagueId !== league.id) {
|
||||
throw new Error(`Season ${query.seasonId} does not belong to league ${league.id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
seasonId: season.id,
|
||||
leagueId: season.leagueId,
|
||||
gameId: season.gameId,
|
||||
name: season.name,
|
||||
status: season.status,
|
||||
...(season.startDate !== undefined ? { startDate: season.startDate } : {}),
|
||||
...(season.endDate !== undefined ? { endDate: season.endDate } : {}),
|
||||
...(season.maxDrivers !== undefined ? { maxDrivers: season.maxDrivers } : {}),
|
||||
...(season.schedule
|
||||
? {
|
||||
schedule: {
|
||||
startDate: season.schedule.startDate,
|
||||
plannedRounds: season.schedule.plannedRounds,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(season.scoringConfig
|
||||
? {
|
||||
scoring: {
|
||||
scoringPresetId: season.scoringConfig.scoringPresetId,
|
||||
customScoringEnabled: season.scoringConfig.customScoringEnabled ?? false,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(season.dropPolicy
|
||||
? {
|
||||
dropPolicy: {
|
||||
strategy: season.dropPolicy.strategy,
|
||||
...(season.dropPolicy.n !== undefined ? { n: season.dropPolicy.n } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(season.stewardingConfig
|
||||
? {
|
||||
stewarding: {
|
||||
decisionMode: season.stewardingConfig.decisionMode,
|
||||
...(season.stewardingConfig.requiredVotes !== undefined
|
||||
? { requiredVotes: season.stewardingConfig.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: season.stewardingConfig.requireDefense,
|
||||
defenseTimeLimit: season.stewardingConfig.defenseTimeLimit,
|
||||
voteTimeLimit: season.stewardingConfig.voteTimeLimit,
|
||||
protestDeadlineHours: season.stewardingConfig.protestDeadlineHours,
|
||||
stewardingClosesHours: season.stewardingConfig.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: season.stewardingConfig.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: season.stewardingConfig.notifyOnVoteRequired,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
async manageSeasonLifecycle(command: ManageSeasonLifecycleCommand): Promise<ManageSeasonLifecycleResultDTO> {
|
||||
const league = await this.leagueRepository.findById(command.leagueId);
|
||||
if (!league) {
|
||||
throw new Error(`League not found: ${command.leagueId}`);
|
||||
}
|
||||
|
||||
const season = await this.seasonRepository.findById(command.seasonId);
|
||||
if (!season || season.leagueId !== league.id) {
|
||||
throw new Error(`Season ${command.seasonId} does not belong to league ${league.id}`);
|
||||
}
|
||||
|
||||
let updated: Season;
|
||||
switch (command.transition) {
|
||||
case 'activate':
|
||||
updated = season.activate();
|
||||
break;
|
||||
case 'complete':
|
||||
updated = season.complete();
|
||||
break;
|
||||
case 'archive':
|
||||
updated = season.archive();
|
||||
break;
|
||||
case 'cancel':
|
||||
updated = season.cancel();
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported Season lifecycle transition');
|
||||
}
|
||||
|
||||
await this.seasonRepository.update(updated);
|
||||
|
||||
return {
|
||||
seasonId: updated.id,
|
||||
status: updated.status,
|
||||
...(updated.startDate !== undefined ? { startDate: updated.startDate } : {}),
|
||||
...(updated.endDate !== undefined ? { endDate: updated.endDate } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private deriveSeasonPropsFromConfig(config: LeagueConfigFormModel): {
|
||||
schedule?: SeasonSchedule;
|
||||
scoringConfig?: SeasonScoringConfig;
|
||||
dropPolicy?: SeasonDropPolicy;
|
||||
stewardingConfig?: SeasonStewardingConfig;
|
||||
maxDrivers?: number;
|
||||
} {
|
||||
const schedule = this.buildScheduleFromTimings(config);
|
||||
const scoringConfig = new SeasonScoringConfig({
|
||||
scoringPresetId: config.scoring.patternId ?? 'custom',
|
||||
customScoringEnabled: config.scoring.customScoringEnabled ?? false,
|
||||
});
|
||||
const dropPolicy = new SeasonDropPolicy({
|
||||
strategy: config.dropPolicy.strategy,
|
||||
...(config.dropPolicy.n !== undefined ? { n: config.dropPolicy.n } : {}),
|
||||
});
|
||||
const stewardingConfig = new SeasonStewardingConfig({
|
||||
decisionMode: config.stewarding.decisionMode,
|
||||
...(config.stewarding.requiredVotes !== undefined
|
||||
? { requiredVotes: config.stewarding.requiredVotes }
|
||||
: {}),
|
||||
requireDefense: config.stewarding.requireDefense,
|
||||
defenseTimeLimit: config.stewarding.defenseTimeLimit,
|
||||
voteTimeLimit: config.stewarding.voteTimeLimit,
|
||||
protestDeadlineHours: config.stewarding.protestDeadlineHours,
|
||||
stewardingClosesHours: config.stewarding.stewardingClosesHours,
|
||||
notifyAccusedOnProtest: config.stewarding.notifyAccusedOnProtest,
|
||||
notifyOnVoteRequired: config.stewarding.notifyOnVoteRequired,
|
||||
});
|
||||
|
||||
const structure = config.structure;
|
||||
const maxDrivers =
|
||||
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
||||
? structure.maxDrivers
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...(schedule !== undefined ? { schedule } : {}),
|
||||
scoringConfig,
|
||||
dropPolicy,
|
||||
stewardingConfig,
|
||||
...(maxDrivers !== undefined ? { maxDrivers } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private buildScheduleFromTimings(config: LeagueConfigFormModel): SeasonSchedule | undefined {
|
||||
const { timings } = config;
|
||||
if (!timings.seasonStartDate || !timings.raceStartTime) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const startDate = new Date(timings.seasonStartDate);
|
||||
const timeOfDay = RaceTimeOfDay.fromString(timings.raceStartTime);
|
||||
const timezoneId = timings.timezoneId ?? 'UTC';
|
||||
const timezone = new LeagueTimezone(timezoneId);
|
||||
|
||||
const plannedRounds =
|
||||
typeof timings.roundsPlanned === 'number' && timings.roundsPlanned > 0
|
||||
? timings.roundsPlanned
|
||||
: timings.sessionCount;
|
||||
|
||||
const recurrence = (() => {
|
||||
const weekdays: WeekdaySet =
|
||||
timings.weekdays && timings.weekdays.length > 0
|
||||
? WeekdaySet.fromArray(timings.weekdays as unknown as Weekday[])
|
||||
: WeekdaySet.fromArray(['Mon']);
|
||||
switch (timings.recurrenceStrategy) {
|
||||
case 'everyNWeeks':
|
||||
return RecurrenceStrategyFactory.everyNWeeks(
|
||||
timings.intervalWeeks ?? 2,
|
||||
weekdays,
|
||||
);
|
||||
case 'monthlyNthWeekday': {
|
||||
const pattern = new MonthlyRecurrencePattern({
|
||||
ordinal: (timings.monthlyOrdinal ?? 1) as 1 | 2 | 3 | 4,
|
||||
weekday: (timings.monthlyWeekday ?? 'Mon') as Weekday,
|
||||
});
|
||||
return RecurrenceStrategyFactory.monthlyNthWeekday(pattern);
|
||||
}
|
||||
case 'weekly':
|
||||
default:
|
||||
return RecurrenceStrategyFactory.weekly(weekdays);
|
||||
}
|
||||
})();
|
||||
|
||||
return new SeasonSchedule({
|
||||
startDate,
|
||||
timeOfDay,
|
||||
timezone,
|
||||
recurrence,
|
||||
plannedRounds,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,8 @@ import type { ISponsorshipRequestRepository } from '../../domain/repositories/IS
|
||||
import type { ISponsorshipPricingRepository } from '../../domain/repositories/ISponsorshipPricingRepository';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import { Money } from '../../domain/value-objects/Money';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { ApplyForSponsorshipPort } from '../ports/input/ApplyForSponsorshipPort';
|
||||
import type { ApplyForSponsorshipResultPort } from '../ports/output/ApplyForSponsorshipResultPort';
|
||||
|
||||
@@ -11,9 +11,8 @@ import type { IProtestRepository } from '../../domain/repositories/IProtestRepos
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { ApplyPenaltyCommandPort } from '../ports/input/ApplyPenaltyCommandPort';
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { CancelRaceCommandDTO } from '../dto/CancelRaceCommandDTO';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import type { IRaceEventRepository } from '../../domain/repositories/IRaceEventRepository';
|
||||
import type { IRaceRegistrationRepository } from '../../domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepository';
|
||||
|
||||
@@ -4,8 +4,7 @@ import { Season } from '../../domain/entities/Season';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '../../domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import type { GetLeagueScoringPresetByIdInputPort } from '../ports/input/GetLeagueScoringPresetByIdInputPort';
|
||||
import type { LeagueScoringPresetOutputPort } from '../ports/output/LeagueScoringPresetOutputPort';
|
||||
import {
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Sponsor } from '../../domain/entities/Sponsor';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { CreateSponsorOutputPort } from '../ports/output/CreateSponsorOutputPort';
|
||||
|
||||
@@ -12,8 +12,7 @@ import type {
|
||||
TeamMembershipStatus,
|
||||
TeamRole,
|
||||
} from '../../domain/types/TeamMembership';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { CreateTeamOutputPort } from '../ports/output/CreateTeamOutputPort';
|
||||
|
||||
@@ -5,8 +5,7 @@ import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarO
|
||||
import type { TeamJoinRequestsOutputPort } from '../ports/output/TeamJoinRequestsOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team join requests.
|
||||
|
||||
@@ -5,8 +5,7 @@ import type { GetDriverAvatarOutputPort } from '../ports/output/GetDriverAvatarO
|
||||
import type { TeamMembersOutputPort } from '../ports/output/TeamMembersOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving team members.
|
||||
|
||||
@@ -5,8 +5,7 @@ import type { TeamsLeaderboardOutputPort, SkillLevel } from '../ports/output/Tea
|
||||
import { SkillLevelService } from '@core/racing/domain/services/SkillLevelService';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
|
||||
interface DriverStatsAdapter {
|
||||
rating: number | null;
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { IDriverRepository } from '../../domain/repositories/IDriverReposit
|
||||
import type { TotalDriversOutputPort } from '../ports/output/TotalDriversOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
|
||||
/**
|
||||
* Use Case for retrieving total number of drivers.
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { ILeagueRepository } from '../../domain/repositories/ILeagueReposit
|
||||
import type { GetTotalLeaguesOutputPort } from '../ports/output/GetTotalLeaguesOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
|
||||
export class GetTotalLeaguesUseCase implements AsyncUseCase<void, GetTotalLeaguesOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { IRaceRepository } from '../../domain/repositories/IRaceRepository'
|
||||
import type { GetTotalRacesOutputPort } from '../ports/output/GetTotalRacesOutputPort';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
|
||||
export class GetTotalRacesUseCase implements AsyncUseCase<void, GetTotalRacesOutputPort, 'REPOSITORY_ERROR'>
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { Logger , AsyncUseCase } from '@core/shared/application';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { LeagueMembership, type MembershipRole, type MembershipStatus } from '../../domain/entities/LeagueMembership';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import { Result as SharedResult } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { JoinLeagueOutputPort } from '../ports/output/JoinLeagueOutputPort';
|
||||
|
||||
@@ -10,8 +10,7 @@ import type { IPenaltyRepository } from '../../domain/repositories/IPenaltyRepos
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/RejectLeagueJoinRequestUseCase';
|
||||
import { RejectLeagueJoinRequestPresenter } from '@apps/api/src/modules/league/presenters/RejectLeagueJoinRequestPresenter';
|
||||
import { describe, it, expect, vi, type Mock } from 'vitest';
|
||||
import { RejectLeagueJoinRequestUseCase, type RejectLeagueJoinRequestUseCaseParams } from './RejectLeagueJoinRequestUseCase';
|
||||
import type { ILeagueMembershipRepository } from '../../domain/repositories/ILeagueMembershipRepository';
|
||||
|
||||
interface LeagueMembershipRepositoryMock {
|
||||
removeJoinRequest: Mock;
|
||||
}
|
||||
|
||||
describe('RejectLeagueJoinRequestUseCase', () => {
|
||||
let leagueMembershipRepository: LeagueMembershipRepositoryMock;
|
||||
let useCase: RejectLeagueJoinRequestUseCase;
|
||||
let leagueMembershipRepository: jest.Mocked<ILeagueMembershipRepository>;
|
||||
let presenter: RejectLeagueJoinRequestPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
leagueMembershipRepository = {
|
||||
removeJoinRequest: jest.fn(),
|
||||
} as unknown;
|
||||
presenter = new RejectLeagueJoinRequestPresenter();
|
||||
useCase = new RejectLeagueJoinRequestUseCase(leagueMembershipRepository);
|
||||
removeJoinRequest: vi.fn(),
|
||||
} as unknown as ILeagueMembershipRepository as any;
|
||||
|
||||
useCase = new RejectLeagueJoinRequestUseCase(
|
||||
leagueMembershipRepository as unknown as ILeagueMembershipRepository,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject join request', async () => {
|
||||
const requestId = 'req-1';
|
||||
it('removes the join request and returns success output', async () => {
|
||||
const params: RejectLeagueJoinRequestUseCaseParams = {
|
||||
requestId: 'req-1',
|
||||
};
|
||||
|
||||
await useCase.execute({ requestId }, presenter);
|
||||
(leagueMembershipRepository.removeJoinRequest as Mock).mockResolvedValue(undefined);
|
||||
|
||||
expect(leagueMembershipRepository.removeJoinRequest).toHaveBeenCalledWith(requestId);
|
||||
expect(presenter.viewModel).toEqual({ success: true, message: 'Join request rejected.' });
|
||||
const result = await useCase.execute(params);
|
||||
|
||||
expect(leagueMembershipRepository.removeJoinRequest).toHaveBeenCalledWith('req-1');
|
||||
expect(result.isOk()).toBe(true);
|
||||
expect(result.unwrap()).toEqual({ success: true, message: 'Join request rejected.' });
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { ReopenRaceCommandDTO } from '../dto/ReopenRaceCommandDTO';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ChampionshipType } from '../types/ChampionshipType';
|
||||
import type { SessionType } from '../types/SessionType';
|
||||
import type { ChampionshipType } from "./ChampionshipType";
|
||||
import type { SessionType } from "./SessionType";
|
||||
import type { PointsTable } from '../value-objects/PointsTable';
|
||||
import type { BonusRule } from '../types/BonusRule';
|
||||
import type { DropScorePolicy } from '../types/DropScorePolicy';
|
||||
import type { BonusRule } from "./BonusRule";
|
||||
import type { DropScorePolicy } from "./DropScorePolicy";
|
||||
|
||||
/**
|
||||
* Domain Type: ChampionshipConfig
|
||||
|
||||
@@ -37,10 +37,6 @@ export * from './domain/repositories/ISponsorRepository';
|
||||
export * from './domain/repositories/ISeasonSponsorshipRepository';
|
||||
export * from './domain/repositories/ISponsorshipRequestRepository';
|
||||
export * from './domain/repositories/ISponsorshipPricingRepository';
|
||||
export * from './infrastructure/repositories/InMemorySponsorRepository';
|
||||
export * from './infrastructure/repositories/InMemorySeasonSponsorshipRepository';
|
||||
export * from './infrastructure/repositories/InMemorySponsorshipRequestRepository';
|
||||
export * from './infrastructure/repositories/InMemorySponsorshipPricingRepository';
|
||||
|
||||
export * from './application/dtos/LeagueDriverSeasonStatsDTO';
|
||||
export * from './application/dtos/LeagueScoringConfigDTO';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import type { ISocialGraphRepository } from '../../domain/repositories/ISocialGraphRepository';
|
||||
import type { CurrentUserSocialDTO } from '../dto/CurrentUserSocialDTO';
|
||||
import type { FriendDTO } from '../dto/FriendDTO';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AsyncUseCase } from '@core/shared/application';
|
||||
import type { AsyncUseCase , Logger } from '@core/shared/application';
|
||||
import type { IFeedRepository } from '../../domain/repositories/IFeedRepository';
|
||||
import type { FeedItemDTO } from '../dto/FeedItemDTO';
|
||||
import type { FeedItem } from '../../domain/types/FeedItem';
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
IUserFeedPresenter,
|
||||
UserFeedViewModel,
|
||||
} from '../presenters/ISocialPresenters';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
export interface GetUserFeedParams {
|
||||
driverId: string;
|
||||
|
||||
Reference in New Issue
Block a user