refactor use cases

This commit is contained in:
2026-01-08 15:34:51 +01:00
parent d984ab24a8
commit 52e9a2f6a7
362 changed files with 5192 additions and 8409 deletions

View File

@@ -3,50 +3,31 @@ import { Module } from '@nestjs/common';
import { InMemoryAdminPersistenceModule } from '../../persistence/inmemory/InMemoryAdminPersistenceModule';
import { AdminService } from './AdminService';
import { AdminController } from './AdminController';
import { ListUsersPresenter } from './presenters/ListUsersPresenter';
import { DashboardStatsPresenter } from './presenters/DashboardStatsPresenter';
import { AuthModule } from '../auth/AuthModule';
import { ListUsersUseCase } from '@core/admin/application/use-cases/ListUsersUseCase';
import { GetDashboardStatsUseCase } from './use-cases/GetDashboardStatsUseCase';
import { InitializationLogger } from '../../shared/logging/InitializationLogger';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { ListUsersResult } from '@core/admin/application/use-cases/ListUsersUseCase';
import type { DashboardStatsResult } from './use-cases/GetDashboardStatsUseCase';
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
export const ADMIN_USER_REPOSITORY_TOKEN = 'IAdminUserRepository';
export const LIST_USERS_OUTPUT_PORT_TOKEN = 'ListUsersOutputPort';
export const DASHBOARD_STATS_OUTPUT_PORT_TOKEN = 'DashboardStatsOutputPort';
const initLogger = InitializationLogger.getInstance();
const adminProviders: Provider[] = [
AdminService,
ListUsersPresenter,
DashboardStatsPresenter,
{
provide: LIST_USERS_OUTPUT_PORT_TOKEN,
useExisting: ListUsersPresenter,
},
{
provide: DASHBOARD_STATS_OUTPUT_PORT_TOKEN,
useExisting: DashboardStatsPresenter,
},
{
provide: ListUsersUseCase,
useFactory: (
repository: IAdminUserRepository,
output: UseCaseOutputPort<ListUsersResult>,
) => new ListUsersUseCase(repository, output),
inject: [ADMIN_USER_REPOSITORY_TOKEN, LIST_USERS_OUTPUT_PORT_TOKEN],
) => new ListUsersUseCase(repository),
inject: [ADMIN_USER_REPOSITORY_TOKEN],
},
{
provide: GetDashboardStatsUseCase,
useFactory: (
repository: IAdminUserRepository,
output: UseCaseOutputPort<DashboardStatsResult>,
) => new GetDashboardStatsUseCase(repository, output),
inject: [ADMIN_USER_REPOSITORY_TOKEN, DASHBOARD_STATS_OUTPUT_PORT_TOKEN],
) => new GetDashboardStatsUseCase(repository),
inject: [ADMIN_USER_REPOSITORY_TOKEN],
},
];

View File

@@ -12,18 +12,6 @@ const mockGetDashboardStatsUseCase = {
execute: vi.fn(),
};
// Mock presenters
const mockListUsersPresenter = {
present: vi.fn(),
getViewModel: vi.fn(),
};
const mockDashboardStatsPresenter = {
present: vi.fn(),
responseModel: {},
reset: vi.fn(),
};
describe('AdminService', () => {
describe('TDD - Test First', () => {
let service: AdminService;
@@ -32,9 +20,7 @@ describe('AdminService', () => {
vi.clearAllMocks();
service = new AdminService(
mockListUsersUseCase as any,
mockListUsersPresenter as any,
mockGetDashboardStatsUseCase as any,
mockDashboardStatsPresenter as any
mockGetDashboardStatsUseCase as any
);
});
@@ -50,15 +36,19 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act
const result = await service.listUsers({ actorId: 'actor-1' });
// Assert
expect(mockListUsersUseCase.execute).toHaveBeenCalledWith({ actorId: 'actor-1' });
expect(mockListUsersPresenter.getViewModel).toHaveBeenCalled();
expect(result).toEqual(expectedResult);
expect(result).toEqual({
users: [],
total: 0,
page: 1,
limit: 10,
totalPages: 0,
});
});
it('should return users when they exist', async () => {
@@ -88,16 +78,6 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue({
users: [
{ id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['user'], status: 'active', isSystemAdmin: false, createdAt: user1.createdAt, updatedAt: user1.updatedAt },
{ id: 'user-2', email: 'user2@example.com', displayName: 'User 2', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: user2.createdAt, updatedAt: user2.updatedAt },
],
total: 2,
page: 1,
limit: 10,
totalPages: 1,
});
// Act
const result = await service.listUsers({ actorId: 'actor-1' });
@@ -105,6 +85,11 @@ describe('AdminService', () => {
// Assert
expect(result.users).toHaveLength(2);
expect(result.total).toBe(2);
// Check that users are converted to DTOs
expect(result.users[0]?.id).toBe('user-1');
expect(result.users[0]?.email).toBe('user1@example.com');
expect(result.users[1]?.id).toBe('user-2');
expect(result.users[1]?.email).toBe('user2@example.com');
});
it('should apply filters correctly', async () => {
@@ -126,13 +111,6 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue({
users: [{ id: 'admin-1', email: 'admin@example.com', displayName: 'Admin', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: adminUser.createdAt, updatedAt: adminUser.updatedAt }],
total: 1,
page: 1,
limit: 10,
totalPages: 1,
});
// Act
const result = await service.listUsers({
@@ -163,13 +141,6 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue({
users: [],
total: 50,
page: 3,
limit: 10,
totalPages: 5,
});
// Act
const result = await service.listUsers({
@@ -202,7 +173,6 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act
await service.listUsers({
@@ -232,7 +202,6 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act
await service.listUsers({
@@ -260,7 +229,6 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act
await service.listUsers({
@@ -299,7 +267,6 @@ describe('AdminService', () => {
};
mockListUsersUseCase.execute.mockResolvedValue(Result.ok(expectedResult));
mockListUsersPresenter.getViewModel.mockReturnValue(expectedResult);
// Act
await service.listUsers({

View File

@@ -1,19 +1,18 @@
import { Injectable } from '@nestjs/common';
import { ListUsersUseCase, ListUsersInput } from '@core/admin/application/use-cases/ListUsersUseCase';
import { ListUsersPresenter, ListUsersViewModel } from './presenters/ListUsersPresenter';
import { ListUsersUseCase, ListUsersInput, ListUsersResult } from '@core/admin/application/use-cases/ListUsersUseCase';
import { GetDashboardStatsUseCase, GetDashboardStatsInput } from './use-cases/GetDashboardStatsUseCase';
import { DashboardStatsPresenter, DashboardStatsResponse } from './presenters/DashboardStatsPresenter';
import { UserListResponseDto, UserResponseDto } from './dtos/UserResponseDto';
import { DashboardStatsResponseDto } from './dto/DashboardStatsResponseDto';
import type { AdminUser } from '@core/admin/domain/entities/AdminUser';
@Injectable()
export class AdminService {
constructor(
private readonly listUsersUseCase: ListUsersUseCase,
private readonly listUsersPresenter: ListUsersPresenter,
private readonly getDashboardStatsUseCase: GetDashboardStatsUseCase,
private readonly dashboardStatsPresenter: DashboardStatsPresenter,
) {}
async listUsers(input: ListUsersInput): Promise<ListUsersViewModel> {
async listUsers(input: ListUsersInput): Promise<UserListResponseDto> {
const result = await this.listUsersUseCase.execute(input);
if (result.isErr()) {
@@ -21,12 +20,11 @@ export class AdminService {
throw new Error(`${error.code}: ${error.details.message}`);
}
return this.listUsersPresenter.getViewModel();
const data = result.unwrap();
return this.toListResponseDto(data);
}
async getDashboardStats(input: GetDashboardStatsInput): Promise<DashboardStatsResponse> {
this.dashboardStatsPresenter.reset();
async getDashboardStats(input: GetDashboardStatsInput): Promise<DashboardStatsResponseDto> {
const result = await this.getDashboardStatsUseCase.execute(input);
if (result.isErr()) {
@@ -34,6 +32,54 @@ export class AdminService {
throw new Error(`${error.code}: ${error.details.message}`);
}
return this.dashboardStatsPresenter.responseModel;
const data = result.unwrap();
return data;
}
}
private toListResponseDto(result: ListUsersResult): UserListResponseDto {
return {
users: result.users.map(user => this.toUserResponse(user)),
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
};
}
private toUserResponse(user: AdminUser | Record<string, unknown>): UserResponseDto {
// Handle both domain objects and plain objects
if (user.id && typeof user.id === 'object' && 'value' in (user.id as Record<string, unknown>)) {
// Domain object
const domainUser = user as AdminUser;
const response: UserResponseDto = {
id: domainUser.id.value,
email: domainUser.email.value,
displayName: domainUser.displayName,
roles: domainUser.roles.map(r => r.value),
status: domainUser.status.value,
isSystemAdmin: domainUser.isSystemAdmin(),
createdAt: domainUser.createdAt,
updatedAt: domainUser.updatedAt,
};
if (domainUser.lastLoginAt) response.lastLoginAt = domainUser.lastLoginAt;
if (domainUser.primaryDriverId) response.primaryDriverId = domainUser.primaryDriverId;
return response;
} else {
// Plain object (for tests)
const plainUser = user as Record<string, unknown>;
const response: UserResponseDto = {
id: plainUser.id as string,
email: plainUser.email as string,
displayName: plainUser.displayName as string,
roles: plainUser.roles as string[],
status: plainUser.status as string,
isSystemAdmin: plainUser.isSystemAdmin as boolean,
createdAt: plainUser.createdAt as Date,
updatedAt: plainUser.updatedAt as Date,
};
if (plainUser.lastLoginAt) response.lastLoginAt = plainUser.lastLoginAt as Date;
if (plainUser.primaryDriverId) response.primaryDriverId = plainUser.primaryDriverId as string;
return response;
}
}
}

View File

@@ -1,6 +1,5 @@
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
import { AuthorizationService } from '@core/admin/domain/services/AuthorizationService';
import { UserId } from '@core/admin/domain/value-objects/UserId';
@@ -46,10 +45,9 @@ export type GetDashboardStatsApplicationError = ApplicationErrorCode<GetDashboar
export class GetDashboardStatsUseCase {
constructor(
private readonly adminUserRepo: IAdminUserRepository,
private readonly output: UseCaseOutputPort<DashboardStatsResult>,
) {}
async execute(input: GetDashboardStatsInput): Promise<Result<void, GetDashboardStatsApplicationError>> {
async execute(input: GetDashboardStatsInput): Promise<Result<DashboardStatsResult, GetDashboardStatsApplicationError>> {
try {
// Get actor (current user)
const actor = await this.adminUserRepo.findById(UserId.fromString(input.actorId));
@@ -166,9 +164,7 @@ export class GetDashboardStatsUseCase {
activityTimeline,
};
this.output.present(result);
return Result.ok(undefined);
return Result.ok(result);
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to get dashboard stats';