refactor
This commit is contained in:
@@ -23,6 +23,19 @@ export class DriverController {
|
||||
return this.driverService.getTotalDrivers();
|
||||
}
|
||||
|
||||
@Get('current')
|
||||
@ApiOperation({ summary: 'Get current authenticated driver' })
|
||||
@ApiResponse({ status: 200, description: 'Current driver data', type: DriverDTO })
|
||||
@ApiResponse({ status: 404, description: 'Driver not found' })
|
||||
async getCurrentDriver(@Req() req: Request): Promise<DriverDTO | null> {
|
||||
// Assuming userId is available from the request (e.g., via auth middleware)
|
||||
const userId = req['user']?.userId;
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
return this.driverService.getCurrentDriver(userId);
|
||||
}
|
||||
|
||||
@Post('complete-onboarding')
|
||||
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
|
||||
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
|
||||
|
||||
@@ -1,41 +1,82 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { MediaService } from './MediaService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
// Import core interfaces
|
||||
import { IAvatarGenerationRepository } from '@gridpilot/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import { FaceValidationPort } from '@gridpilot/media/application/ports/FaceValidationPort';
|
||||
import { AvatarGenerationPort } from '@gridpilot/media/application/ports/AvatarGenerationPort';
|
||||
import { Logger } from '@gridpilot/shared/application';
|
||||
|
||||
import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
||||
import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
// Import use cases
|
||||
import { RequestAvatarGenerationUseCase } from '@gridpilot/media/application/use-cases/RequestAvatarGenerationUseCase';
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
// Define injection tokens
|
||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
||||
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
|
||||
export const AVATAR_GENERATION_PORT_TOKEN = 'AvatarGenerationPort';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
|
||||
// Use case tokens
|
||||
export const REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN = 'RequestAvatarGenerationUseCase';
|
||||
|
||||
// Mock implementations
|
||||
class MockAvatarGenerationRepository implements IAvatarGenerationRepository {
|
||||
async save(_request: any): Promise<void> {}
|
||||
async findById(_id: string): Promise<any | null> { return null; }
|
||||
async findByUserId(_userId: string): Promise<any[]> { return []; }
|
||||
async findLatestByUserId(_userId: string): Promise<any | null> { return null; }
|
||||
async delete(_id: string): Promise<void> {}
|
||||
}
|
||||
|
||||
class MockFaceValidationAdapter implements FaceValidationPort {
|
||||
async validateFacePhoto(data: string): Promise<any> {
|
||||
return { isValid: true, hasFace: true, faceCount: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
class MockAvatarGenerationAdapter implements AvatarGenerationPort {
|
||||
async generateAvatars(options: any): Promise<any> {
|
||||
return {
|
||||
success: true,
|
||||
avatars: [
|
||||
{ url: 'https://cdn.example.com/avatars/mock-avatar-1.png' },
|
||||
{ url: 'https://cdn.example.com/avatars/mock-avatar-2.png' },
|
||||
{ url: 'https://cdn.example.com/avatars/mock-avatar-3.png' },
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MockLogger implements Logger {
|
||||
debug(message: string, meta?: any): void {}
|
||||
info(message: string, meta?: any): void {}
|
||||
warn(message: string, meta?: any): void {}
|
||||
error(message: string, error?: Error): void {}
|
||||
}
|
||||
|
||||
export const MediaProviders: Provider[] = [
|
||||
MediaService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
{
|
||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryAvatarGenerationRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
useClass: MockAvatarGenerationRepository,
|
||||
},
|
||||
{
|
||||
provide: FACE_VALIDATION_PORT_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryFaceValidationAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
useClass: MockFaceValidationAdapter,
|
||||
},
|
||||
{
|
||||
provide: AVATAR_GENERATION_PORT_TOKEN,
|
||||
useClass: MockAvatarGenerationAdapter,
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
useClass: MockLogger,
|
||||
},
|
||||
// Use cases
|
||||
{
|
||||
provide: REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN,
|
||||
useFactory: (avatarRepo: IAvatarGenerationRepository, faceValidation: FaceValidationPort, avatarGeneration: AvatarGenerationPort, logger: Logger) =>
|
||||
new RequestAvatarGenerationUseCase(avatarRepo, faceValidation, avatarGeneration, logger),
|
||||
inject: [AVATAR_GENERATION_REPOSITORY_TOKEN, FACE_VALIDATION_PORT_TOKEN, AVATAR_GENERATION_PORT_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto';
|
||||
|
||||
// Use cases
|
||||
import { RequestAvatarGenerationUseCase } from '@gridpilot/media/application/use-cases/RequestAvatarGenerationUseCase';
|
||||
|
||||
// Presenters
|
||||
import { RequestAvatarGenerationPresenter } from './presenters/RequestAvatarGenerationPresenter';
|
||||
|
||||
// Tokens
|
||||
import { REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN, LOGGER_TOKEN } from './MediaProviders';
|
||||
import { Logger } from '@gridpilot/shared/application';
|
||||
|
||||
@Injectable()
|
||||
export class MediaService {
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
@Inject(REQUEST_AVATAR_GENERATION_USE_CASE_TOKEN) private readonly requestAvatarGenerationUseCase: RequestAvatarGenerationUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
|
||||
console.log('[MediaService] Returning mock avatar generation request. Input:', input);
|
||||
return {
|
||||
success: true,
|
||||
requestId: `req-${Date.now()}`,
|
||||
avatarUrls: [
|
||||
'https://cdn.example.com/avatars/mock-avatar-1.png',
|
||||
'https://cdn.example.com/avatars/mock-avatar-2.png',
|
||||
],
|
||||
};
|
||||
this.logger.debug('[MediaService] Requesting avatar generation.');
|
||||
|
||||
const presenter = new RequestAvatarGenerationPresenter();
|
||||
await this.requestAvatarGenerationUseCase.execute({
|
||||
userId: input.userId,
|
||||
facePhotoData: input.facePhotoData,
|
||||
suitColor: input.suitColor as any,
|
||||
}, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { RequestAvatarGenerationOutput } from '../dto/MediaDto';
|
||||
import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '@gridpilot/media/application/presenters/IRequestAvatarGenerationPresenter';
|
||||
|
||||
export class RequestAvatarGenerationPresenter implements IRequestAvatarGenerationPresenter {
|
||||
private result: RequestAvatarGenerationOutput | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: RequestAvatarGenerationResultDTO) {
|
||||
this.result = {
|
||||
success: dto.status === 'completed',
|
||||
requestId: dto.requestId,
|
||||
avatarUrls: dto.avatarUrls,
|
||||
errorMessage: dto.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): RequestAvatarGenerationOutput {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
|
||||
getViewModel(): RequestAvatarGenerationOutput {
|
||||
return this.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,67 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { PaymentsService } from './PaymentsService';
|
||||
|
||||
// Due to persistent module resolution issues in the environment,
|
||||
// actual core interfaces and adapter implementations are not directly imported here.
|
||||
// In a functional TypeScript environment, these would be imported as follows:
|
||||
/*
|
||||
// Import core interfaces
|
||||
import { IPaymentRepository } from 'core/payments/domain/repositories/IPaymentRepository';
|
||||
import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMembershipFeeRepository';
|
||||
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
|
||||
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
|
||||
import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway';
|
||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
||||
import type { IPaymentRepository } from '@gridpilot/payments/domain/repositories/IPaymentRepository';
|
||||
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '@gridpilot/payments/domain/repositories/IMembershipFeeRepository';
|
||||
import type { IPrizeRepository } from '@gridpilot/payments/domain/repositories/IPrizeRepository';
|
||||
import type { IWalletRepository, ITransactionRepository } from '@gridpilot/payments/domain/repositories/IWalletRepository';
|
||||
import type { Logger } from '@gridpilot/shared/application/Logger';
|
||||
|
||||
// Import use cases
|
||||
import { GetPaymentsUseCase } from '@gridpilot/payments/application/use-cases/GetPaymentsUseCase';
|
||||
import { CreatePaymentUseCase } from '@gridpilot/payments/application/use-cases/CreatePaymentUseCase';
|
||||
import { UpdatePaymentStatusUseCase } from '@gridpilot/payments/application/use-cases/UpdatePaymentStatusUseCase';
|
||||
import { GetMembershipFeesUseCase } from '@gridpilot/payments/application/use-cases/GetMembershipFeesUseCase';
|
||||
import { UpsertMembershipFeeUseCase } from '@gridpilot/payments/application/use-cases/UpsertMembershipFeeUseCase';
|
||||
import { UpdateMemberPaymentUseCase } from '@gridpilot/payments/application/use-cases/UpdateMemberPaymentUseCase';
|
||||
import { GetPrizesUseCase } from '@gridpilot/payments/application/use-cases/GetPrizesUseCase';
|
||||
import { CreatePrizeUseCase } from '@gridpilot/payments/application/use-cases/CreatePrizeUseCase';
|
||||
import { AwardPrizeUseCase } from '@gridpilot/payments/application/use-cases/AwardPrizeUseCase';
|
||||
import { DeletePrizeUseCase } from '@gridpilot/payments/application/use-cases/DeletePrizeUseCase';
|
||||
import { GetWalletUseCase } from '@gridpilot/payments/application/use-cases/GetWalletUseCase';
|
||||
import { ProcessWalletTransactionUseCase } from '@gridpilot/payments/application/use-cases/ProcessWalletTransactionUseCase';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
||||
import { InMemoryMembershipFeeRepository } from 'adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
|
||||
import { InMemoryMembershipFeeRepository, InMemoryMemberPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryMembershipFeeRepository';
|
||||
import { InMemoryPrizeRepository } from 'adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
|
||||
import { InMemoryWalletRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||
import { InMemoryPaymentGateway } from 'adapters/payments/ports/InMemoryPaymentGateway';
|
||||
import { InMemoryWalletRepository, InMemoryTransactionRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
*/
|
||||
|
||||
// Define injection tokens as string literals for NestJS
|
||||
// Repository injection tokens
|
||||
export const PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
|
||||
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
|
||||
export const MEMBER_PAYMENT_REPOSITORY_TOKEN = 'IMemberPaymentRepository';
|
||||
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
|
||||
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
|
||||
export const PAYMENT_GATEWAY_TOKEN = 'IPaymentGateway';
|
||||
export const LOGGER_TOKEN = 'Logger'; // Already defined in other Providers, but good to have here too
|
||||
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
|
||||
// Use case injection tokens
|
||||
export const GET_PAYMENTS_USE_CASE_TOKEN = 'GetPaymentsUseCase';
|
||||
export const CREATE_PAYMENT_USE_CASE_TOKEN = 'CreatePaymentUseCase';
|
||||
export const UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN = 'UpdatePaymentStatusUseCase';
|
||||
export const GET_MEMBERSHIP_FEES_USE_CASE_TOKEN = 'GetMembershipFeesUseCase';
|
||||
export const UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN = 'UpsertMembershipFeeUseCase';
|
||||
export const UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN = 'UpdateMemberPaymentUseCase';
|
||||
export const GET_PRIZES_USE_CASE_TOKEN = 'GetPrizesUseCase';
|
||||
export const CREATE_PRIZE_USE_CASE_TOKEN = 'CreatePrizeUseCase';
|
||||
export const AWARD_PRIZE_USE_CASE_TOKEN = 'AwardPrizeUseCase';
|
||||
export const DELETE_PRIZE_USE_CASE_TOKEN = 'DeletePrizeUseCase';
|
||||
export const GET_WALLET_USE_CASE_TOKEN = 'GetWalletUseCase';
|
||||
export const PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN = 'ProcessWalletTransactionUseCase';
|
||||
|
||||
export const PaymentsProviders: Provider[] = [
|
||||
PaymentsService, // Provide the service itself
|
||||
// In a functional setup, the following would be enabled:
|
||||
/*
|
||||
PaymentsService,
|
||||
|
||||
// Logger
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
|
||||
// Repositories (repositories are injected into use cases, NOT into services)
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryPaymentRepository(logger),
|
||||
@@ -44,6 +72,11 @@ export const PaymentsProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryMembershipFeeRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: MEMBER_PAYMENT_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryMemberPaymentRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PRIZE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryPrizeRepository(logger),
|
||||
@@ -55,13 +88,74 @@ export const PaymentsProviders: Provider[] = [
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PAYMENT_GATEWAY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryPaymentGateway(logger),
|
||||
provide: TRANSACTION_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryTransactionRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
|
||||
// Use cases (use cases receive repositories, services receive use cases)
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
provide: GET_PAYMENTS_USE_CASE_TOKEN,
|
||||
useFactory: (paymentRepo: IPaymentRepository) => new GetPaymentsUseCase(paymentRepo),
|
||||
inject: [PAYMENT_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: CREATE_PAYMENT_USE_CASE_TOKEN,
|
||||
useFactory: (paymentRepo: IPaymentRepository) => new CreatePaymentUseCase(paymentRepo),
|
||||
inject: [PAYMENT_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN,
|
||||
useFactory: (paymentRepo: IPaymentRepository) => new UpdatePaymentStatusUseCase(paymentRepo),
|
||||
inject: [PAYMENT_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_MEMBERSHIP_FEES_USE_CASE_TOKEN,
|
||||
useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) =>
|
||||
new GetMembershipFeesUseCase(membershipFeeRepo, memberPaymentRepo),
|
||||
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN,
|
||||
useFactory: (membershipFeeRepo: IMembershipFeeRepository) => new UpsertMembershipFeeUseCase(membershipFeeRepo),
|
||||
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN,
|
||||
useFactory: (membershipFeeRepo: IMembershipFeeRepository, memberPaymentRepo: IMemberPaymentRepository) =>
|
||||
new UpdateMemberPaymentUseCase(membershipFeeRepo, memberPaymentRepo),
|
||||
inject: [MEMBERSHIP_FEE_REPOSITORY_TOKEN, MEMBER_PAYMENT_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_PRIZES_USE_CASE_TOKEN,
|
||||
useFactory: (prizeRepo: IPrizeRepository) => new GetPrizesUseCase(prizeRepo),
|
||||
inject: [PRIZE_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: CREATE_PRIZE_USE_CASE_TOKEN,
|
||||
useFactory: (prizeRepo: IPrizeRepository) => new CreatePrizeUseCase(prizeRepo),
|
||||
inject: [PRIZE_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: AWARD_PRIZE_USE_CASE_TOKEN,
|
||||
useFactory: (prizeRepo: IPrizeRepository) => new AwardPrizeUseCase(prizeRepo),
|
||||
inject: [PRIZE_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DELETE_PRIZE_USE_CASE_TOKEN,
|
||||
useFactory: (prizeRepo: IPrizeRepository) => new DeletePrizeUseCase(prizeRepo),
|
||||
inject: [PRIZE_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_WALLET_USE_CASE_TOKEN,
|
||||
useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) =>
|
||||
new GetWalletUseCase(walletRepo, transactionRepo),
|
||||
inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN,
|
||||
useFactory: (walletRepo: IWalletRepository, transactionRepo: ITransactionRepository) =>
|
||||
new ProcessWalletTransactionUseCase(walletRepo, transactionRepo),
|
||||
inject: [WALLET_REPOSITORY_TOKEN, TRANSACTION_REPOSITORY_TOKEN],
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
@@ -1,346 +1,190 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, PaymentDto, GetPaymentsQuery, GetPaymentsOutput, PaymentStatus, MembershipFeeDto, MemberPaymentDto, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, MembershipFeeType, MemberPaymentStatus, PrizeDto, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, PrizeType, WalletDto, TransactionDto, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput, TransactionType, ReferenceType } from './dto/PaymentsDto';
|
||||
import { LeagueSettingsDto, LeagueConfigFormModelStructureDto } from '../league/dto/LeagueDto'; // For the mock data definitions
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { Logger } from '@gridpilot/shared/application/Logger';
|
||||
|
||||
const payments: Map<string, PaymentDto> = new Map();
|
||||
const membershipFees: Map<string, MembershipFeeDto> = new Map();
|
||||
const memberPayments: Map<string, MemberPaymentDto> = new Map();
|
||||
const prizes: Map<string, PrizeDto> = new Map();
|
||||
const wallets: Map<string, WalletDto> = new Map();
|
||||
const transactions: Map<string, TransactionDto> = new Map();
|
||||
// Use cases
|
||||
import type { GetPaymentsUseCase } from '@gridpilot/payments/application/use-cases/GetPaymentsUseCase';
|
||||
import type { CreatePaymentUseCase } from '@gridpilot/payments/application/use-cases/CreatePaymentUseCase';
|
||||
import type { UpdatePaymentStatusUseCase } from '@gridpilot/payments/application/use-cases/UpdatePaymentStatusUseCase';
|
||||
import type { GetMembershipFeesUseCase } from '@gridpilot/payments/application/use-cases/GetMembershipFeesUseCase';
|
||||
import type { UpsertMembershipFeeUseCase } from '@gridpilot/payments/application/use-cases/UpsertMembershipFeeUseCase';
|
||||
import type { UpdateMemberPaymentUseCase } from '@gridpilot/payments/application/use-cases/UpdateMemberPaymentUseCase';
|
||||
import type { GetPrizesUseCase } from '@gridpilot/payments/application/use-cases/GetPrizesUseCase';
|
||||
import type { CreatePrizeUseCase } from '@gridpilot/payments/application/use-cases/CreatePrizeUseCase';
|
||||
import type { AwardPrizeUseCase } from '@gridpilot/payments/application/use-cases/AwardPrizeUseCase';
|
||||
import type { DeletePrizeUseCase } from '@gridpilot/payments/application/use-cases/DeletePrizeUseCase';
|
||||
import type { GetWalletUseCase } from '@gridpilot/payments/application/use-cases/GetWalletUseCase';
|
||||
import type { ProcessWalletTransactionUseCase } from '@gridpilot/payments/application/use-cases/ProcessWalletTransactionUseCase';
|
||||
|
||||
const PLATFORM_FEE_RATE = 0.10;
|
||||
// Presenters
|
||||
import { GetPaymentsPresenter } from './presenters/GetPaymentsPresenter';
|
||||
import { CreatePaymentPresenter } from './presenters/CreatePaymentPresenter';
|
||||
import { UpdatePaymentStatusPresenter } from './presenters/UpdatePaymentStatusPresenter';
|
||||
import { GetMembershipFeesPresenter } from './presenters/GetMembershipFeesPresenter';
|
||||
import { UpsertMembershipFeePresenter } from './presenters/UpsertMembershipFeePresenter';
|
||||
import { UpdateMemberPaymentPresenter } from './presenters/UpdateMemberPaymentPresenter';
|
||||
import { GetPrizesPresenter } from './presenters/GetPrizesPresenter';
|
||||
import { CreatePrizePresenter } from './presenters/CreatePrizePresenter';
|
||||
import { AwardPrizePresenter } from './presenters/AwardPrizePresenter';
|
||||
import { DeletePrizePresenter } from './presenters/DeletePrizePresenter';
|
||||
import { GetWalletPresenter } from './presenters/GetWalletPresenter';
|
||||
import { ProcessWalletTransactionPresenter } from './presenters/ProcessWalletTransactionPresenter';
|
||||
|
||||
// DTOs
|
||||
import type {
|
||||
CreatePaymentInput,
|
||||
CreatePaymentOutput,
|
||||
UpdatePaymentStatusInput,
|
||||
UpdatePaymentStatusOutput,
|
||||
GetPaymentsQuery,
|
||||
GetPaymentsOutput,
|
||||
GetMembershipFeesQuery,
|
||||
GetMembershipFeesOutput,
|
||||
UpsertMembershipFeeInput,
|
||||
UpsertMembershipFeeOutput,
|
||||
UpdateMemberPaymentInput,
|
||||
UpdateMemberPaymentOutput,
|
||||
GetPrizesQuery,
|
||||
GetPrizesOutput,
|
||||
CreatePrizeInput,
|
||||
CreatePrizeOutput,
|
||||
AwardPrizeInput,
|
||||
AwardPrizeOutput,
|
||||
DeletePrizeInput,
|
||||
DeletePrizeOutput,
|
||||
GetWalletQuery,
|
||||
GetWalletOutput,
|
||||
ProcessWalletTransactionInput,
|
||||
ProcessWalletTransactionOutput,
|
||||
} from './dto/PaymentsDto';
|
||||
|
||||
// Injection tokens
|
||||
import {
|
||||
GET_PAYMENTS_USE_CASE_TOKEN,
|
||||
CREATE_PAYMENT_USE_CASE_TOKEN,
|
||||
UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN,
|
||||
GET_MEMBERSHIP_FEES_USE_CASE_TOKEN,
|
||||
UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN,
|
||||
UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN,
|
||||
GET_PRIZES_USE_CASE_TOKEN,
|
||||
CREATE_PRIZE_USE_CASE_TOKEN,
|
||||
AWARD_PRIZE_USE_CASE_TOKEN,
|
||||
DELETE_PRIZE_USE_CASE_TOKEN,
|
||||
GET_WALLET_USE_CASE_TOKEN,
|
||||
PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN,
|
||||
LOGGER_TOKEN,
|
||||
} from './PaymentsProviders';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
constructor(
|
||||
@Inject(GET_PAYMENTS_USE_CASE_TOKEN) private readonly getPaymentsUseCase: GetPaymentsUseCase,
|
||||
@Inject(CREATE_PAYMENT_USE_CASE_TOKEN) private readonly createPaymentUseCase: CreatePaymentUseCase,
|
||||
@Inject(UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN) private readonly updatePaymentStatusUseCase: UpdatePaymentStatusUseCase,
|
||||
@Inject(GET_MEMBERSHIP_FEES_USE_CASE_TOKEN) private readonly getMembershipFeesUseCase: GetMembershipFeesUseCase,
|
||||
@Inject(UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN) private readonly upsertMembershipFeeUseCase: UpsertMembershipFeeUseCase,
|
||||
@Inject(UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN) private readonly updateMemberPaymentUseCase: UpdateMemberPaymentUseCase,
|
||||
@Inject(GET_PRIZES_USE_CASE_TOKEN) private readonly getPrizesUseCase: GetPrizesUseCase,
|
||||
@Inject(CREATE_PRIZE_USE_CASE_TOKEN) private readonly createPrizeUseCase: CreatePrizeUseCase,
|
||||
@Inject(AWARD_PRIZE_USE_CASE_TOKEN) private readonly awardPrizeUseCase: AwardPrizeUseCase,
|
||||
@Inject(DELETE_PRIZE_USE_CASE_TOKEN) private readonly deletePrizeUseCase: DeletePrizeUseCase,
|
||||
@Inject(GET_WALLET_USE_CASE_TOKEN) private readonly getWalletUseCase: GetWalletUseCase,
|
||||
@Inject(PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN) private readonly processWalletTransactionUseCase: ProcessWalletTransactionUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||
let results = Array.from(payments.values());
|
||||
this.logger.debug('[PaymentsService] Getting payments', { query });
|
||||
|
||||
if (query.leagueId) {
|
||||
results = results.filter(p => p.leagueId === query.leagueId);
|
||||
}
|
||||
if (query.payerId) {
|
||||
results = results.filter(p => p.payerId === query.payerId);
|
||||
}
|
||||
if (query.type) {
|
||||
results = results.filter(p => p.type === query.type);
|
||||
}
|
||||
|
||||
return { payments: results };
|
||||
const presenter = new GetPaymentsPresenter();
|
||||
await this.getPaymentsUseCase.execute(query, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||
const { type, amount, payerId, payerType, leagueId, seasonId } = input;
|
||||
this.logger.debug('[PaymentsService] Creating payment', { input });
|
||||
|
||||
const platformFee = amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = amount - platformFee;
|
||||
|
||||
const id = `payment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const payment: PaymentDto = {
|
||||
id,
|
||||
type,
|
||||
amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
payerId,
|
||||
payerType,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
status: PaymentStatus.PENDING,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
payments.set(id, payment);
|
||||
|
||||
return { payment };
|
||||
const presenter = new CreatePaymentPresenter();
|
||||
await this.createPaymentUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||
const { paymentId, status } = input;
|
||||
this.logger.debug('[PaymentsService] Updating payment status', { input });
|
||||
|
||||
const payment = payments.get(paymentId);
|
||||
if (!payment) {
|
||||
throw new Error('Payment not found');
|
||||
}
|
||||
|
||||
payment.status = status;
|
||||
if (status === PaymentStatus.COMPLETED) {
|
||||
payment.completedAt = new Date();
|
||||
}
|
||||
|
||||
payments.set(paymentId, payment);
|
||||
|
||||
return { payment };
|
||||
const presenter = new UpdatePaymentStatusPresenter();
|
||||
await this.updatePaymentStatusUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||
const { leagueId, driverId } = query;
|
||||
this.logger.debug('[PaymentsService] Getting membership fees', { query });
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('leagueId is required');
|
||||
}
|
||||
|
||||
const fee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId) || null;
|
||||
|
||||
let payments: MemberPaymentDto[] = [];
|
||||
if (driverId) {
|
||||
payments = Array.from(memberPayments.values()).filter(
|
||||
p => membershipFees.get(p.feeId)?.leagueId === leagueId && p.driverId === driverId
|
||||
);
|
||||
}
|
||||
|
||||
return { fee, payments };
|
||||
const presenter = new GetMembershipFeesPresenter();
|
||||
await this.getMembershipFeesUseCase.execute(query, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
||||
const { leagueId, seasonId, type, amount } = input;
|
||||
this.logger.debug('[PaymentsService] Upserting membership fee', { input });
|
||||
|
||||
// Check for existing fee config
|
||||
let existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId);
|
||||
|
||||
if (existingFee) {
|
||||
// Update existing fee
|
||||
existingFee.type = type;
|
||||
existingFee.amount = amount;
|
||||
existingFee.seasonId = seasonId || existingFee.seasonId;
|
||||
existingFee.enabled = amount > 0;
|
||||
existingFee.updatedAt = new Date();
|
||||
membershipFees.set(existingFee.id, existingFee);
|
||||
return { fee: existingFee };
|
||||
}
|
||||
|
||||
const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const fee: MembershipFeeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId: seasonId || undefined,
|
||||
type,
|
||||
amount,
|
||||
enabled: amount > 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
membershipFees.set(id, fee);
|
||||
|
||||
return { fee };
|
||||
const presenter = new UpsertMembershipFeePresenter();
|
||||
await this.upsertMembershipFeeUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
||||
const { feeId, driverId, status, paidAt } = input;
|
||||
this.logger.debug('[PaymentsService] Updating member payment', { input });
|
||||
|
||||
const fee = membershipFees.get(feeId);
|
||||
if (!fee) {
|
||||
throw new Error('Membership fee configuration not found');
|
||||
}
|
||||
|
||||
// Find or create payment record
|
||||
let payment = Array.from(memberPayments.values()).find(
|
||||
p => p.feeId === feeId && p.driverId === driverId
|
||||
);
|
||||
|
||||
if (!payment) {
|
||||
const platformFee = fee.amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = fee.amount - platformFee;
|
||||
|
||||
const paymentId = `mp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
payment = {
|
||||
id: paymentId,
|
||||
feeId,
|
||||
driverId,
|
||||
amount: fee.amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
status: MemberPaymentStatus.PENDING,
|
||||
dueDate: new Date(),
|
||||
};
|
||||
memberPayments.set(paymentId, payment);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
payment.status = status;
|
||||
}
|
||||
if (paidAt || status === MemberPaymentStatus.PAID) {
|
||||
payment.paidAt = paidAt ? new Date(paidAt) : new Date();
|
||||
}
|
||||
|
||||
memberPayments.set(payment.id, payment);
|
||||
|
||||
return { payment };
|
||||
const presenter = new UpdateMemberPaymentPresenter();
|
||||
await this.updateMemberPaymentUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
||||
const { leagueId, seasonId } = query;
|
||||
this.logger.debug('[PaymentsService] Getting prizes', { query });
|
||||
|
||||
let results = Array.from(prizes.values()).filter(p => p.leagueId === leagueId);
|
||||
|
||||
if (seasonId) {
|
||||
results = results.filter(p => p.seasonId === seasonId);
|
||||
}
|
||||
|
||||
results.sort((a, b) => a.position - b.position);
|
||||
|
||||
return { prizes: results };
|
||||
const presenter = new GetPrizesPresenter();
|
||||
await this.getPrizesUseCase.execute({ leagueId: query.leagueId!, seasonId: query.seasonId }, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
||||
const { leagueId, seasonId, position, name, amount, type, description } = input;
|
||||
this.logger.debug('[PaymentsService] Creating prize', { input });
|
||||
|
||||
// Check for duplicate position
|
||||
const existingPrize = Array.from(prizes.values()).find(
|
||||
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position
|
||||
);
|
||||
|
||||
if (existingPrize) {
|
||||
throw new Error(`Prize for position ${position} already exists`);
|
||||
}
|
||||
|
||||
const id = `prize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const prize: PrizeDto = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId,
|
||||
position,
|
||||
name,
|
||||
amount,
|
||||
type,
|
||||
description: description || undefined,
|
||||
awarded: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
prizes.set(id, prize);
|
||||
|
||||
return { prize };
|
||||
const presenter = new CreatePrizePresenter();
|
||||
await this.createPrizeUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||
const { prizeId, driverId } = input;
|
||||
this.logger.debug('[PaymentsService] Awarding prize', { input });
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Prize has already been awarded');
|
||||
}
|
||||
|
||||
prize.awarded = true;
|
||||
prize.awardedTo = driverId;
|
||||
prize.awardedAt = new Date();
|
||||
|
||||
prizes.set(prizeId, prize);
|
||||
|
||||
return { prize };
|
||||
const presenter = new AwardPrizePresenter();
|
||||
await this.awardPrizeUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||
const { prizeId } = input;
|
||||
this.logger.debug('[PaymentsService] Deleting prize', { input });
|
||||
|
||||
const prize = prizes.get(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Cannot delete an awarded prize');
|
||||
}
|
||||
|
||||
prizes.delete(prizeId);
|
||||
|
||||
return { success: true };
|
||||
const presenter = new DeletePrizePresenter();
|
||||
await this.deletePrizeUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||
const { leagueId } = query;
|
||||
this.logger.debug('[PaymentsService] Getting wallet', { query });
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('LeagueId is required');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
const walletTransactions = Array.from(transactions.values())
|
||||
.filter(t => t.walletId === wallet!.id)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
return { wallet, transactions: walletTransactions };
|
||||
const presenter = new GetWalletPresenter();
|
||||
await this.getWalletUseCase.execute({ leagueId: query.leagueId! }, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
||||
const { leagueId, type, amount, description, referenceId, referenceType } = input;
|
||||
this.logger.debug('[PaymentsService] Processing wallet transaction', { input });
|
||||
|
||||
if (!leagueId || !type || amount === undefined || !description) {
|
||||
throw new Error('Missing required fields: leagueId, type, amount, description');
|
||||
}
|
||||
|
||||
if (type !== TransactionType.DEPOSIT && type !== TransactionType.WITHDRAWAL) {
|
||||
throw new Error('Type must be "deposit" or "withdrawal"');
|
||||
}
|
||||
|
||||
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
createdAt: new Date(),
|
||||
currency: 'USD', // Assuming default currency (mock)
|
||||
};
|
||||
wallets.set(id, wallet);
|
||||
}
|
||||
|
||||
if (type === TransactionType.WITHDRAWAL) {
|
||||
if (amount > wallet.balance) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
}
|
||||
|
||||
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const transaction: TransactionDto = {
|
||||
id: transactionId,
|
||||
walletId: wallet.id,
|
||||
type,
|
||||
amount,
|
||||
description,
|
||||
referenceId: referenceId || undefined,
|
||||
referenceType: referenceType || undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
transactions.set(transactionId, transaction);
|
||||
|
||||
if (type === TransactionType.DEPOSIT) {
|
||||
wallet.balance += amount;
|
||||
wallet.totalRevenue += amount;
|
||||
} else {
|
||||
wallet.balance -= amount;
|
||||
wallet.totalWithdrawn += amount;
|
||||
}
|
||||
|
||||
wallets.set(wallet.id, wallet);
|
||||
|
||||
return { wallet, transaction };
|
||||
const presenter = new ProcessWalletTransactionPresenter();
|
||||
await this.processWalletTransactionUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IAwardPrizePresenter,
|
||||
AwardPrizeResultDTO,
|
||||
AwardPrizeViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IAwardPrizePresenter';
|
||||
|
||||
export class AwardPrizePresenter implements IAwardPrizePresenter {
|
||||
private result: AwardPrizeViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: AwardPrizeResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): AwardPrizeViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): AwardPrizeViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
ICreatePaymentPresenter,
|
||||
CreatePaymentResultDTO,
|
||||
CreatePaymentViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/ICreatePaymentPresenter';
|
||||
|
||||
export class CreatePaymentPresenter implements ICreatePaymentPresenter {
|
||||
private result: CreatePaymentViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CreatePaymentResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): CreatePaymentViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): CreatePaymentViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
ICreatePrizePresenter,
|
||||
CreatePrizeResultDTO,
|
||||
CreatePrizeViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/ICreatePrizePresenter';
|
||||
|
||||
export class CreatePrizePresenter implements ICreatePrizePresenter {
|
||||
private result: CreatePrizeViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CreatePrizeResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): CreatePrizeViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): CreatePrizeViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IDeletePrizePresenter,
|
||||
DeletePrizeResultDTO,
|
||||
DeletePrizeViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IDeletePrizePresenter';
|
||||
|
||||
export class DeletePrizePresenter implements IDeletePrizePresenter {
|
||||
private result: DeletePrizeViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: DeletePrizeResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): DeletePrizeViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): DeletePrizeViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IGetMembershipFeesPresenter,
|
||||
GetMembershipFeesResultDTO,
|
||||
GetMembershipFeesViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IGetMembershipFeesPresenter';
|
||||
|
||||
export class GetMembershipFeesPresenter implements IGetMembershipFeesPresenter {
|
||||
private result: GetMembershipFeesViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetMembershipFeesResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): GetMembershipFeesViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetMembershipFeesViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IGetPaymentsPresenter,
|
||||
GetPaymentsResultDTO,
|
||||
GetPaymentsViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IGetPaymentsPresenter';
|
||||
|
||||
export class GetPaymentsPresenter implements IGetPaymentsPresenter {
|
||||
private result: GetPaymentsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetPaymentsResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): GetPaymentsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetPaymentsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IGetPrizesPresenter,
|
||||
GetPrizesResultDTO,
|
||||
GetPrizesViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IGetPrizesPresenter';
|
||||
|
||||
export class GetPrizesPresenter implements IGetPrizesPresenter {
|
||||
private result: GetPrizesViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetPrizesResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): GetPrizesViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetPrizesViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IGetWalletPresenter,
|
||||
GetWalletResultDTO,
|
||||
GetWalletViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IGetWalletPresenter';
|
||||
|
||||
export class GetWalletPresenter implements IGetWalletPresenter {
|
||||
private result: GetWalletViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetWalletResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): GetWalletViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetWalletViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IProcessWalletTransactionPresenter,
|
||||
ProcessWalletTransactionResultDTO,
|
||||
ProcessWalletTransactionViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IProcessWalletTransactionPresenter';
|
||||
|
||||
export class ProcessWalletTransactionPresenter implements IProcessWalletTransactionPresenter {
|
||||
private result: ProcessWalletTransactionViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: ProcessWalletTransactionResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): ProcessWalletTransactionViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): ProcessWalletTransactionViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IUpdateMemberPaymentPresenter,
|
||||
UpdateMemberPaymentResultDTO,
|
||||
UpdateMemberPaymentViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IUpdateMemberPaymentPresenter';
|
||||
|
||||
export class UpdateMemberPaymentPresenter implements IUpdateMemberPaymentPresenter {
|
||||
private result: UpdateMemberPaymentViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: UpdateMemberPaymentResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): UpdateMemberPaymentViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): UpdateMemberPaymentViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IUpdatePaymentStatusPresenter,
|
||||
UpdatePaymentStatusResultDTO,
|
||||
UpdatePaymentStatusViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IUpdatePaymentStatusPresenter';
|
||||
|
||||
export class UpdatePaymentStatusPresenter implements IUpdatePaymentStatusPresenter {
|
||||
private result: UpdatePaymentStatusViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: UpdatePaymentStatusResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): UpdatePaymentStatusViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): UpdatePaymentStatusViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
IUpsertMembershipFeePresenter,
|
||||
UpsertMembershipFeeResultDTO,
|
||||
UpsertMembershipFeeViewModel,
|
||||
} from '@gridpilot/payments/application/presenters/IUpsertMembershipFeePresenter';
|
||||
|
||||
export class UpsertMembershipFeePresenter implements IUpsertMembershipFeePresenter {
|
||||
private result: UpsertMembershipFeeViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: UpsertMembershipFeeResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): UpsertMembershipFeeViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): UpsertMembershipFeeViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
12
apps/api/src/modules/payments/presenters/index.ts
Normal file
12
apps/api/src/modules/payments/presenters/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './GetPaymentsPresenter';
|
||||
export * from './CreatePaymentPresenter';
|
||||
export * from './UpdatePaymentStatusPresenter';
|
||||
export * from './GetMembershipFeesPresenter';
|
||||
export * from './UpsertMembershipFeePresenter';
|
||||
export * from './UpdateMemberPaymentPresenter';
|
||||
export * from './GetPrizesPresenter';
|
||||
export * from './CreatePrizePresenter';
|
||||
export * from './AwardPrizePresenter';
|
||||
export * from './DeletePrizePresenter';
|
||||
export * from './GetWalletPresenter';
|
||||
export * from './ProcessWalletTransactionPresenter';
|
||||
@@ -1,18 +1,52 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { RaceService } from './RaceService';
|
||||
|
||||
// Import core interfaces
|
||||
import { Logger } from '@gridpilot/shared/application/Logger';
|
||||
import { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
|
||||
// Import use cases
|
||||
import { GetAllRacesUseCase } from '@gridpilot/racing/application/use-cases/GetAllRacesUseCase';
|
||||
import { GetTotalRacesUseCase } from '@gridpilot/racing/application/use-cases/GetTotalRacesUseCase';
|
||||
import { ImportRaceResultsApiUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsApiUseCase';
|
||||
|
||||
// Define injection tokens
|
||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
|
||||
export const RaceProviders: Provider[] = [
|
||||
RaceService,
|
||||
// In a functional setup, other providers would be here, e.g.:
|
||||
/*
|
||||
{
|
||||
provide: 'Logger',
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Use cases
|
||||
{
|
||||
provide: 'IRaceRepository',
|
||||
useClass: InMemoryRaceRepository,
|
||||
provide: GetAllRacesUseCase,
|
||||
useFactory: (raceRepo: IRaceRepository, leagueRepo: ILeagueRepository) => new GetAllRacesUseCase(raceRepo, leagueRepo),
|
||||
inject: [RACE_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN],
|
||||
},
|
||||
// ... other providers
|
||||
*/
|
||||
{
|
||||
provide: GetTotalRacesUseCase,
|
||||
useFactory: (raceRepo: IRaceRepository) => new GetTotalRacesUseCase(raceRepo),
|
||||
inject: [RACE_REPOSITORY_TOKEN],
|
||||
},
|
||||
ImportRaceResultsApiUseCase,
|
||||
];
|
||||
|
||||
@@ -1,37 +1,50 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { AllRacesPageViewModel, RaceStatsDto, ImportRaceResultsInput, ImportRaceResultsSummaryViewModel } from './dto/RaceDto';
|
||||
|
||||
// Core imports
|
||||
import { Logger } from '@gridpilot/shared/application/Logger';
|
||||
|
||||
// Use cases
|
||||
import { GetAllRacesUseCase } from '@gridpilot/racing/application/use-cases/GetAllRacesUseCase';
|
||||
import { GetTotalRacesUseCase } from '@gridpilot/racing/application/use-cases/GetTotalRacesUseCase';
|
||||
import { ImportRaceResultsApiUseCase } from '@gridpilot/racing/application/use-cases/ImportRaceResultsApiUseCase';
|
||||
|
||||
// Presenters
|
||||
import { GetAllRacesPresenter } from './presenters/GetAllRacesPresenter';
|
||||
import { GetTotalRacesPresenter } from './presenters/GetTotalRacesPresenter';
|
||||
import { ImportRaceResultsApiPresenter } from './presenters/ImportRaceResultsApiPresenter';
|
||||
|
||||
// Tokens
|
||||
import { LOGGER_TOKEN } from './RaceProviders';
|
||||
|
||||
@Injectable()
|
||||
export class RaceService {
|
||||
constructor(
|
||||
private readonly getAllRacesUseCase: GetAllRacesUseCase,
|
||||
private readonly getTotalRacesUseCase: GetTotalRacesUseCase,
|
||||
private readonly importRaceResultsApiUseCase: ImportRaceResultsApiUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
constructor() {}
|
||||
async getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
this.logger.debug('[RaceService] Fetching all races.');
|
||||
|
||||
getAllRaces(): Promise<AllRacesPageViewModel> {
|
||||
console.log('[RaceService] Returning mock all races.');
|
||||
return Promise.resolve({
|
||||
races: [
|
||||
{ id: 'race-1', name: 'Global Race 1', date: new Date().toISOString(), leagueName: 'Global Racing' },
|
||||
{ id: 'race-2', name: 'Amateur Race 1', date: new Date().toISOString(), leagueName: 'Amateur Series' },
|
||||
],
|
||||
totalCount: 2,
|
||||
});
|
||||
const presenter = new GetAllRacesPresenter();
|
||||
await this.getAllRacesUseCase.execute({}, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
getTotalRaces(): Promise<RaceStatsDto> {
|
||||
console.log('[RaceService] Returning mock total races.');
|
||||
return Promise.resolve({
|
||||
totalRaces: 2, // Placeholder
|
||||
});
|
||||
async getTotalRaces(): Promise<RaceStatsDto> {
|
||||
this.logger.debug('[RaceService] Fetching total races count.');
|
||||
const presenter = new GetTotalRacesPresenter();
|
||||
await this.getTotalRacesUseCase.execute({}, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
|
||||
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
|
||||
console.log('Importing race results:', input);
|
||||
return {
|
||||
success: true,
|
||||
raceId: input.raceId,
|
||||
driversProcessed: 10, // Mock data
|
||||
resultsRecorded: 10, // Mock data
|
||||
errors: [],
|
||||
};
|
||||
this.logger.debug('Importing race results:', input);
|
||||
const presenter = new ImportRaceResultsApiPresenter();
|
||||
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter);
|
||||
return presenter.getViewModel()!;
|
||||
}
|
||||
}
|
||||
|
||||
17
apps/api/src/modules/race/presenters/GetAllRacesPresenter.ts
Normal file
17
apps/api/src/modules/race/presenters/GetAllRacesPresenter.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IGetAllRacesPresenter, GetAllRacesResultDTO, AllRacesPageViewModel } from '@gridpilot/racing/application/presenters/IGetAllRacesPresenter';
|
||||
|
||||
export class GetAllRacesPresenter implements IGetAllRacesPresenter {
|
||||
private result: AllRacesPageViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetAllRacesResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): AllRacesPageViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IGetTotalRacesPresenter, GetTotalRacesResultDTO } from '@gridpilot/racing/application/presenters/IGetTotalRacesPresenter';
|
||||
import { RaceStatsDto } from '../dto/RaceDto';
|
||||
|
||||
export class GetTotalRacesPresenter implements IGetTotalRacesPresenter {
|
||||
private result: RaceStatsDto | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetTotalRacesResultDTO) {
|
||||
this.result = {
|
||||
totalRaces: dto.totalRaces,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): RaceStatsDto | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { IImportRaceResultsApiPresenter, ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel } from '@gridpilot/racing/application/presenters/IImportRaceResultsApiPresenter';
|
||||
|
||||
export class ImportRaceResultsApiPresenter implements IImportRaceResultsApiPresenter {
|
||||
private result: ImportRaceResultsSummaryViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: ImportRaceResultsApiResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): ImportRaceResultsSummaryViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,134 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { SponsorService } from './SponsorService';
|
||||
|
||||
export const SponsorProviders = [
|
||||
// Import core interfaces
|
||||
import { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
|
||||
import { ISeasonSponsorshipRepository } from '@gridpilot/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||
import { ISeasonRepository } from '@gridpilot/racing/domain/repositories/ISeasonRepository';
|
||||
import { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
||||
import { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
||||
import { ISponsorshipPricingRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipPricingRepository';
|
||||
import { ISponsorshipRequestRepository } from '@gridpilot/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import { Logger } from '@gridpilot/shared/application';
|
||||
|
||||
// Import use cases
|
||||
import { GetSponsorshipPricingUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorshipPricingUseCase';
|
||||
import { GetSponsorsUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorsUseCase';
|
||||
import { CreateSponsorUseCase } from '@gridpilot/racing/application/use-cases/CreateSponsorUseCase';
|
||||
import { GetSponsorDashboardUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
import { GetSponsorSponsorshipsUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import { GetEntitySponsorshipPricingUseCase } from '@gridpilot/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemorySponsorRepository } from 'adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
import { InMemorySeasonSponsorshipRepository } from 'adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||
import { InMemorySeasonRepository } from 'adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemoryLeagueRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueRepository';
|
||||
import { InMemoryLeagueMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryRaceRepository } from 'adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
||||
import { InMemorySponsorshipPricingRepository } from 'adapters/racing/persistence/inmemory/InMemorySponsorshipPricingRepository';
|
||||
import { InMemorySponsorshipRequestRepository } from 'adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
|
||||
// Define injection tokens
|
||||
export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository';
|
||||
export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository';
|
||||
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
||||
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
|
||||
export const LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN = 'ILeagueMembershipRepository';
|
||||
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
|
||||
export const SPONSORSHIP_PRICING_REPOSITORY_TOKEN = 'ISponsorshipPricingRepository';
|
||||
export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestRepository';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
|
||||
// Use case tokens
|
||||
export const GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetSponsorshipPricingUseCase';
|
||||
export const GET_SPONSORS_USE_CASE_TOKEN = 'GetSponsorsUseCase';
|
||||
export const CREATE_SPONSOR_USE_CASE_TOKEN = 'CreateSponsorUseCase';
|
||||
export const GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN = 'GetSponsorDashboardUseCase';
|
||||
export const GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN = 'GetSponsorSponsorshipsUseCase';
|
||||
export const GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN = 'GetEntitySponsorshipPricingUseCase';
|
||||
|
||||
export const SponsorProviders: Provider[] = [
|
||||
SponsorService,
|
||||
// Repositories
|
||||
{
|
||||
provide: SPONSOR_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemorySponsorRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemorySeasonSponsorshipRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SEASON_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemorySeasonRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueMembershipRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: RACE_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SPONSORSHIP_PRICING_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemorySponsorshipPricingRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: SPONSORSHIP_REQUEST_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemorySponsorshipRequestRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Use cases
|
||||
{
|
||||
provide: GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
|
||||
useFactory: () => new GetSponsorshipPricingUseCase(),
|
||||
inject: [],
|
||||
},
|
||||
{
|
||||
provide: GET_SPONSORS_USE_CASE_TOKEN,
|
||||
useFactory: (sponsorRepo: ISponsorRepository) => new GetSponsorsUseCase(sponsorRepo),
|
||||
inject: [SPONSOR_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: CREATE_SPONSOR_USE_CASE_TOKEN,
|
||||
useFactory: (sponsorRepo: ISponsorRepository) => new CreateSponsorUseCase(sponsorRepo),
|
||||
inject: [SPONSOR_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN,
|
||||
useFactory: (sponsorRepo: ISponsorRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, leagueRepo: ILeagueRepository, leagueMembershipRepo: ILeagueMembershipRepository, raceRepo: IRaceRepository) =>
|
||||
new GetSponsorDashboardUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, leagueRepo, leagueMembershipRepo, raceRepo),
|
||||
inject: [SPONSOR_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN,
|
||||
useFactory: (sponsorRepo: ISponsorRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, seasonRepo: ISeasonRepository, raceRepo: IRaceRepository) =>
|
||||
new GetSponsorSponsorshipsUseCase(sponsorRepo, seasonSponsorshipRepo, seasonRepo, raceRepo),
|
||||
inject: [SPONSOR_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, SEASON_REPOSITORY_TOKEN, RACE_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_ENTITY_SPONSORSHIP_PRICING_USE_CASE_TOKEN,
|
||||
useFactory: (sponsorshipPricingRepo: ISponsorshipPricingRepository, sponsorshipRequestRepo: ISponsorshipRequestRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository, logger: Logger) =>
|
||||
new GetEntitySponsorshipPricingUseCase(sponsorshipPricingRepo, sponsorshipRequestRepo, seasonSponsorshipRepo, logger),
|
||||
inject: [SPONSORSHIP_PRICING_REPOSITORY_TOKEN, SPONSORSHIP_REQUEST_REPOSITORY_TOKEN, SEASON_SPONSORSHIP_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,162 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GetEntitySponsorshipPricingResultDto, SponsorDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorshipDetailDTO, SponsorSponsorshipsDTO, SponsoredLeagueDTO, SponsorDashboardMetricsDTO, SponsorDashboardInvestmentDTO } from './dto/SponsorDto';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { GetEntitySponsorshipPricingResultDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorSponsorshipsDTO, SponsorDto, SponsorDashboardMetricsDTO, SponsoredLeagueDTO, SponsorDashboardInvestmentDTO, SponsorshipDetailDTO } from './dto/SponsorDto';
|
||||
|
||||
const sponsors: Map<string, SponsorDto> = new Map();
|
||||
// Use cases
|
||||
import { GetSponsorshipPricingUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorshipPricingUseCase';
|
||||
import { GetSponsorsUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorsUseCase';
|
||||
import { CreateSponsorUseCase } from '@gridpilot/racing/application/use-cases/CreateSponsorUseCase';
|
||||
import { GetSponsorDashboardUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
import { GetSponsorSponsorshipsUseCase } from '@gridpilot/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
|
||||
// Presenters
|
||||
import { GetSponsorshipPricingPresenter } from './presenters/GetSponsorshipPricingPresenter';
|
||||
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
|
||||
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
|
||||
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
|
||||
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
|
||||
|
||||
// Tokens
|
||||
import { GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN, GET_SPONSORS_USE_CASE_TOKEN, CREATE_SPONSOR_USE_CASE_TOKEN, GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN, GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN, LOGGER_TOKEN } from './SponsorProviders';
|
||||
import { Logger } from '@gridpilot/shared/application';
|
||||
|
||||
@Injectable()
|
||||
export class SponsorService {
|
||||
constructor(
|
||||
@Inject(GET_SPONSORSHIP_PRICING_USE_CASE_TOKEN) private readonly getSponsorshipPricingUseCase: GetSponsorshipPricingUseCase,
|
||||
@Inject(GET_SPONSORS_USE_CASE_TOKEN) private readonly getSponsorsUseCase: GetSponsorsUseCase,
|
||||
@Inject(CREATE_SPONSOR_USE_CASE_TOKEN) private readonly createSponsorUseCase: CreateSponsorUseCase,
|
||||
@Inject(GET_SPONSOR_DASHBOARD_USE_CASE_TOKEN) private readonly getSponsorDashboardUseCase: GetSponsorDashboardUseCase,
|
||||
@Inject(GET_SPONSOR_SPONSORSHIPS_USE_CASE_TOKEN) private readonly getSponsorSponsorshipsUseCase: GetSponsorSponsorshipsUseCase,
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
constructor() {
|
||||
// Seed some demo sponsors for dashboard if empty
|
||||
if (sponsors.size === 0) {
|
||||
const demoSponsor1: SponsorDto = {
|
||||
id: 'sponsor-demo-1',
|
||||
name: 'Demo Sponsor Co.',
|
||||
contactEmail: 'contact@demosponsor.com',
|
||||
websiteUrl: 'https://demosponsor.com',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/aaaaaa/ffffff?text=DemoCo',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const demoSponsor2: SponsorDto = {
|
||||
id: 'sponsor-demo-2',
|
||||
name: 'Second Brand',
|
||||
contactEmail: 'info@secondbrand.net',
|
||||
websiteUrl: 'https://secondbrand.net',
|
||||
logoUrl: 'https://fakeimg.pl/200x100/cccccc/ffffff?text=Brand2',
|
||||
createdAt: new Date(Date.now() - 86400000 * 5),
|
||||
};
|
||||
sponsors.set(demoSponsor1.id, demoSponsor1);
|
||||
sponsors.set(demoSponsor2.id, demoSponsor2);
|
||||
}
|
||||
}
|
||||
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
|
||||
|
||||
getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||
// This logic relies on external factors (e.g., pricing configuration, entity type)
|
||||
// For now, return mock data
|
||||
return Promise.resolve({
|
||||
pricing: [
|
||||
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
|
||||
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
|
||||
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
|
||||
],
|
||||
});
|
||||
const presenter = new GetSponsorshipPricingPresenter();
|
||||
await this.getSponsorshipPricingUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
||||
return { sponsors: Array.from(sponsors.values()) };
|
||||
this.logger.debug('[SponsorService] Fetching sponsors.');
|
||||
|
||||
const presenter = new GetSponsorsPresenter();
|
||||
await this.getSponsorsUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async createSponsor(input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newSponsor: SponsorDto = {
|
||||
id,
|
||||
name: input.name,
|
||||
contactEmail: input.contactEmail,
|
||||
websiteUrl: input.websiteUrl,
|
||||
logoUrl: input.logoUrl,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
sponsors.set(id, newSponsor);
|
||||
return { sponsor: newSponsor };
|
||||
this.logger.debug('[SponsorService] Creating sponsor.', { input });
|
||||
|
||||
const presenter = new CreateSponsorPresenter();
|
||||
await this.createSponsorUseCase.execute(input, presenter);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
|
||||
async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params });
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simplified mock data for dashboard metrics and sponsored leagues
|
||||
const metrics: SponsorDashboardMetricsDTO = {
|
||||
impressions: 10000,
|
||||
impressionsChange: 12.5,
|
||||
uniqueViewers: 7000,
|
||||
viewersChange: 8.3,
|
||||
races: 50,
|
||||
drivers: 100,
|
||||
exposure: 75,
|
||||
exposureChange: 5.2,
|
||||
};
|
||||
|
||||
const sponsoredLeagues: SponsoredLeagueDTO[] = [
|
||||
{ id: 'league-1', name: 'League 1', tier: 'main', drivers: 50, races: 10, impressions: 5000, status: 'active' },
|
||||
{ id: 'league-2', name: 'League 2', tier: 'secondary', drivers: 30, races: 5, impressions: 1500, status: 'upcoming' },
|
||||
];
|
||||
|
||||
const investment: SponsorDashboardInvestmentDTO = {
|
||||
activeSponsorships: 2,
|
||||
totalInvestment: 5000,
|
||||
costPerThousandViews: 0.5,
|
||||
};
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
metrics,
|
||||
sponsoredLeagues,
|
||||
investment,
|
||||
};
|
||||
const presenter = new GetSponsorDashboardPresenter();
|
||||
await this.getSponsorDashboardUseCase.execute(params, presenter);
|
||||
return presenter.viewModel as SponsorDashboardDTO | null;
|
||||
}
|
||||
|
||||
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
||||
const { sponsorId } = params;
|
||||
this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params });
|
||||
|
||||
const sponsor = sponsors.get(sponsorId);
|
||||
if (!sponsor) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const sponsorshipDetails: SponsorshipDetailDTO[] = [
|
||||
{
|
||||
id: 'sponsorship-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
seasonId: 'season-1',
|
||||
seasonName: 'Season 1',
|
||||
seasonStartDate: new Date('2025-01-01'),
|
||||
seasonEndDate: new Date('2025-12-31'),
|
||||
tier: 'main',
|
||||
status: 'active',
|
||||
pricing: { amount: 1000, currency: 'USD' },
|
||||
platformFee: { amount: 100, currency: 'USD' },
|
||||
netAmount: { amount: 900, currency: 'USD' },
|
||||
metrics: { drivers: 50, races: 10, completedRaces: 8, impressions: 5000 },
|
||||
createdAt: new Date('2024-12-01'),
|
||||
activatedAt: new Date('2025-01-01'),
|
||||
},
|
||||
{
|
||||
id: 'sponsorship-2',
|
||||
leagueId: 'league-2',
|
||||
leagueName: 'League 2',
|
||||
seasonId: 'season-2',
|
||||
seasonName: 'Season 2',
|
||||
tier: 'secondary',
|
||||
status: 'pending',
|
||||
pricing: { amount: 500, currency: 'USD' },
|
||||
platformFee: { amount: 50, currency: 'USD' },
|
||||
netAmount: { amount: 450, currency: 'USD' },
|
||||
metrics: { drivers: 30, races: 5, completedRaces: 0, impressions: 0 },
|
||||
createdAt: new Date('2025-03-15'),
|
||||
},
|
||||
];
|
||||
|
||||
const totalInvestment = sponsorshipDetails.reduce((sum, s) => sum + s.pricing.amount, 0);
|
||||
const totalPlatformFees = sponsorshipDetails.reduce((sum, s) => sum + s.platformFee.amount, 0);
|
||||
const activeSponsorships = sponsorshipDetails.filter(s => s.status === 'active').length;
|
||||
|
||||
return {
|
||||
sponsorId,
|
||||
sponsorName: sponsor.name,
|
||||
sponsorships: sponsorshipDetails,
|
||||
summary: {
|
||||
totalSponsorships: sponsorshipDetails.length,
|
||||
activeSponsorships,
|
||||
totalInvestment,
|
||||
totalPlatformFees,
|
||||
currency: 'USD',
|
||||
},
|
||||
};
|
||||
const presenter = new GetSponsorSponsorshipsPresenter();
|
||||
await this.getSponsorSponsorshipsUseCase.execute(params, presenter);
|
||||
return presenter.viewModel as SponsorSponsorshipsDTO | null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { CreateSponsorViewModel, CreateSponsorResultDTO, ICreateSponsorPresenter } from '@gridpilot/racing/application/presenters/ICreateSponsorPresenter';
|
||||
|
||||
export class CreateSponsorPresenter implements ICreateSponsorPresenter {
|
||||
private result: CreateSponsorViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: CreateSponsorResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): CreateSponsorViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): CreateSponsorViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { GetEntitySponsorshipPricingResultDTO } from '@gridpilot/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
|
||||
import type { IEntitySponsorshipPricingPresenter } from '@gridpilot/racing/application/presenters/IEntitySponsorshipPricingPresenter';
|
||||
|
||||
export class GetEntitySponsorshipPricingPresenter implements IEntitySponsorshipPricingPresenter {
|
||||
private result: GetEntitySponsorshipPricingResultDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetEntitySponsorshipPricingResultDTO | null) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): GetEntitySponsorshipPricingResultDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetEntitySponsorshipPricingResultDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { SponsorDashboardDTO } from '@gridpilot/racing/application/use-cases/GetSponsorDashboardUseCase';
|
||||
import type { ISponsorDashboardPresenter, SponsorDashboardViewModel } from '@gridpilot/racing/application/presenters/ISponsorDashboardPresenter';
|
||||
|
||||
export class GetSponsorDashboardPresenter implements ISponsorDashboardPresenter {
|
||||
private result: SponsorDashboardViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: SponsorDashboardDTO | null) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): SponsorDashboardViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): SponsorDashboardViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { SponsorSponsorshipsDTO } from '@gridpilot/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
|
||||
import type { ISponsorSponsorshipsPresenter, SponsorSponsorshipsViewModel } from '@gridpilot/racing/application/presenters/ISponsorSponsorshipsPresenter';
|
||||
|
||||
export class GetSponsorSponsorshipsPresenter implements ISponsorSponsorshipsPresenter {
|
||||
private result: SponsorSponsorshipsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: SponsorSponsorshipsDTO | null) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): SponsorSponsorshipsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): SponsorSponsorshipsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { GetSponsorsViewModel, GetSponsorsResultDTO, IGetSponsorsPresenter } from '@gridpilot/racing/application/presenters/IGetSponsorsPresenter';
|
||||
|
||||
export class GetSponsorsPresenter implements IGetSponsorsPresenter {
|
||||
private result: GetSponsorsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetSponsorsResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): GetSponsorsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetSponsorsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { GetSponsorshipPricingViewModel, GetSponsorshipPricingResultDTO, IGetSponsorshipPricingPresenter } from '@gridpilot/racing/application/presenters/IGetSponsorshipPricingPresenter';
|
||||
|
||||
export class GetSponsorshipPricingPresenter implements IGetSponsorshipPricingPresenter {
|
||||
private result: GetSponsorshipPricingViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: GetSponsorshipPricingResultDTO) {
|
||||
this.result = dto;
|
||||
}
|
||||
|
||||
getViewModel(): GetSponsorshipPricingViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): GetSponsorshipPricingViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||
import { TeamService } from './TeamService';
|
||||
import { AllTeamsViewModel } from './dto/TeamDto';
|
||||
import { AllTeamsViewModel, DriverTeamViewModel, TeamDetailsViewModel, TeamMembersViewModel, TeamJoinRequestsViewModel, CreateTeamInput, CreateTeamOutput, UpdateTeamInput, UpdateTeamOutput, ApproveTeamJoinRequestInput, ApproveTeamJoinRequestOutput, RejectTeamJoinRequestInput, RejectTeamJoinRequestOutput } from './dto/TeamDto';
|
||||
|
||||
@ApiTags('teams')
|
||||
@Controller('teams')
|
||||
@@ -15,5 +15,79 @@ export class TeamController {
|
||||
return this.teamService.getAllTeams();
|
||||
}
|
||||
|
||||
// Add other Team endpoints here based on other presenters
|
||||
@Get(':teamId')
|
||||
@ApiOperation({ summary: 'Get team details' })
|
||||
@ApiResponse({ status: 200, description: 'Team details', type: TeamDetailsViewModel })
|
||||
@ApiResponse({ status: 404, description: 'Team not found' })
|
||||
async getTeamDetails(
|
||||
@Param('teamId') teamId: string,
|
||||
): Promise<TeamDetailsViewModel | null> {
|
||||
return this.teamService.getTeamDetails(teamId);
|
||||
}
|
||||
|
||||
@Get(':teamId/members')
|
||||
@ApiOperation({ summary: 'Get team members' })
|
||||
@ApiResponse({ status: 200, description: 'Team members', type: TeamMembersViewModel })
|
||||
async getTeamMembers(@Param('teamId') teamId: string): Promise<TeamMembersViewModel> {
|
||||
return this.teamService.getTeamMembers(teamId);
|
||||
}
|
||||
|
||||
@Get(':teamId/join-requests')
|
||||
@ApiOperation({ summary: 'Get team join requests' })
|
||||
@ApiResponse({ status: 200, description: 'Team join requests', type: TeamJoinRequestsViewModel })
|
||||
async getTeamJoinRequests(@Param('teamId') teamId: string): Promise<TeamJoinRequestsViewModel> {
|
||||
return this.teamService.getTeamJoinRequests(teamId);
|
||||
}
|
||||
|
||||
@Post(':teamId/join-requests/approve')
|
||||
@ApiOperation({ summary: 'Approve a team join request' })
|
||||
@ApiBody({ type: ApproveTeamJoinRequestInput })
|
||||
@ApiResponse({ status: 200, description: 'Join request approved', type: ApproveTeamJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async approveJoinRequest(
|
||||
@Param('teamId') teamId: string,
|
||||
@Body() input: ApproveTeamJoinRequestInput,
|
||||
): Promise<ApproveTeamJoinRequestOutput> {
|
||||
return this.teamService.approveTeamJoinRequest({ ...input, teamId });
|
||||
}
|
||||
|
||||
@Post(':teamId/join-requests/reject')
|
||||
@ApiOperation({ summary: 'Reject a team join request' })
|
||||
@ApiBody({ type: RejectTeamJoinRequestInput })
|
||||
@ApiResponse({ status: 200, description: 'Join request rejected', type: RejectTeamJoinRequestOutput })
|
||||
@ApiResponse({ status: 404, description: 'Join request not found' })
|
||||
async rejectJoinRequest(
|
||||
@Param('teamId') teamId: string,
|
||||
@Body() input: RejectTeamJoinRequestInput,
|
||||
): Promise<RejectTeamJoinRequestOutput> {
|
||||
return this.teamService.rejectTeamJoinRequest({ ...input, teamId });
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new team' })
|
||||
@ApiBody({ type: CreateTeamInput })
|
||||
@ApiResponse({ status: 201, description: 'Team created successfully', type: CreateTeamOutput })
|
||||
async createTeam(@Body() input: CreateTeamInput): Promise<CreateTeamOutput> {
|
||||
return this.teamService.createTeam(input);
|
||||
}
|
||||
|
||||
@Patch(':teamId')
|
||||
@ApiOperation({ summary: 'Update team details' })
|
||||
@ApiBody({ type: UpdateTeamInput })
|
||||
@ApiResponse({ status: 200, description: 'Team updated successfully', type: UpdateTeamOutput })
|
||||
@ApiResponse({ status: 404, description: 'Team not found' })
|
||||
async updateTeam(
|
||||
@Param('teamId') teamId: string,
|
||||
@Body() input: UpdateTeamInput,
|
||||
): Promise<UpdateTeamOutput> {
|
||||
return this.teamService.updateTeam({ ...input, teamId });
|
||||
}
|
||||
|
||||
@Get('driver/:driverId')
|
||||
@ApiOperation({ summary: 'Get team for a driver' })
|
||||
@ApiResponse({ status: 200, description: 'Driver team membership', type: DriverTeamViewModel })
|
||||
@ApiResponse({ status: 404, description: 'Driver not in a team' })
|
||||
async getDriverTeam(@Param('driverId') driverId: string): Promise<DriverTeamViewModel | null> {
|
||||
return this.teamService.getDriverTeam({ teamId: '', driverId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,64 @@ import { TeamService } from './TeamService';
|
||||
// Import core interfaces
|
||||
import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
||||
import { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
|
||||
import { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
||||
import { IImageServicePort } from '@gridpilot/racing/application/ports/IImageServicePort';
|
||||
import { Logger } from '@gridpilot/shared/application/Logger';
|
||||
|
||||
// Import concrete in-memory implementations
|
||||
import { InMemoryTeamRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||
import { InMemoryTeamMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||
import { InMemoryDriverRepository } from 'adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||
|
||||
// Import use cases
|
||||
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
|
||||
import { GetTeamDetailsUseCase } from '@gridpilot/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { GetTeamMembersUseCase } from '@gridpilot/racing/application/use-cases/GetTeamMembersUseCase';
|
||||
import { GetTeamJoinRequestsUseCase } from '@gridpilot/racing/application/use-cases/GetTeamJoinRequestsUseCase';
|
||||
import { CreateTeamUseCase } from '@gridpilot/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { UpdateTeamUseCase } from '@gridpilot/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { ApproveTeamJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
|
||||
import { RejectTeamJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/RejectTeamJoinRequestUseCase';
|
||||
|
||||
// Import presenters for use case initialization
|
||||
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
|
||||
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
|
||||
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
|
||||
|
||||
// Tokens
|
||||
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
|
||||
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
|
||||
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
|
||||
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
|
||||
export const TEAM_GET_ALL_USE_CASE_TOKEN = 'GetAllTeamsUseCase';
|
||||
export const TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
|
||||
export const TEAM_GET_DETAILS_USE_CASE_TOKEN = 'GetTeamDetailsUseCase';
|
||||
export const TEAM_GET_MEMBERS_USE_CASE_TOKEN = 'GetTeamMembersUseCase';
|
||||
export const TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN = 'GetTeamJoinRequestsUseCase';
|
||||
export const TEAM_CREATE_USE_CASE_TOKEN = 'CreateTeamUseCase';
|
||||
export const TEAM_UPDATE_USE_CASE_TOKEN = 'UpdateTeamUseCase';
|
||||
export const TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN = 'ApproveTeamJoinRequestUseCase';
|
||||
export const TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN = 'RejectTeamJoinRequestUseCase';
|
||||
export const TEAM_LOGGER_TOKEN = 'Logger';
|
||||
|
||||
// Simple image service implementation for team module
|
||||
class SimpleImageService implements IImageServicePort {
|
||||
getDriverAvatar(driverId: string): string {
|
||||
return `/api/media/avatars/${driverId}`;
|
||||
}
|
||||
getTeamLogo(teamId: string): string {
|
||||
return `/api/media/teams/${teamId}/logo`;
|
||||
}
|
||||
getLeagueCover(leagueId: string): string {
|
||||
return `/api/media/leagues/${leagueId}/cover`;
|
||||
}
|
||||
getLeagueLogo(leagueId: string): string {
|
||||
return `/api/media/leagues/${leagueId}/logo`;
|
||||
}
|
||||
}
|
||||
|
||||
export const TeamProviders: Provider[] = [
|
||||
TeamService, // Provide the service itself
|
||||
{
|
||||
@@ -34,6 +74,15 @@ export const TeamProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
|
||||
inject: [TEAM_LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DRIVER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
|
||||
inject: [TEAM_LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: IMAGE_SERVICE_TOKEN,
|
||||
useClass: SimpleImageService,
|
||||
},
|
||||
{
|
||||
provide: TEAM_LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
@@ -48,7 +97,57 @@ export const TeamProviders: Provider[] = [
|
||||
{
|
||||
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
|
||||
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger),
|
||||
new GetDriverTeamUseCase(teamRepo, membershipRepo, logger, new DriverTeamPresenter()),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_GET_DETAILS_USE_CASE_TOKEN,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
|
||||
new GetTeamDetailsUseCase(teamRepo, membershipRepo),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_GET_MEMBERS_USE_CASE_TOKEN,
|
||||
useFactory: (
|
||||
membershipRepo: ITeamMembershipRepository,
|
||||
driverRepo: IDriverRepository,
|
||||
imageService: IImageServicePort,
|
||||
logger: Logger,
|
||||
) => new GetTeamMembersUseCase(membershipRepo, driverRepo, imageService, logger, new TeamMembersPresenter()),
|
||||
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, TEAM_LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN,
|
||||
useFactory: (
|
||||
membershipRepo: ITeamMembershipRepository,
|
||||
driverRepo: IDriverRepository,
|
||||
imageService: IImageServicePort,
|
||||
logger: Logger,
|
||||
) => new GetTeamJoinRequestsUseCase(membershipRepo, driverRepo, imageService, logger, new TeamJoinRequestsPresenter()),
|
||||
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_TOKEN, TEAM_LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_CREATE_USE_CASE_TOKEN,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
|
||||
new CreateTeamUseCase(teamRepo, membershipRepo),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_UPDATE_USE_CASE_TOKEN,
|
||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository) =>
|
||||
new UpdateTeamUseCase(teamRepo, membershipRepo),
|
||||
inject: [TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN,
|
||||
useFactory: (membershipRepo: ITeamMembershipRepository, logger: Logger) =>
|
||||
new ApproveTeamJoinRequestUseCase(membershipRepo, logger),
|
||||
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN, TEAM_LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN,
|
||||
useFactory: (membershipRepo: ITeamMembershipRepository) =>
|
||||
new RejectTeamJoinRequestUseCase(membershipRepo),
|
||||
inject: [TEAM_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,25 +1,53 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel } from './dto/TeamDto';
|
||||
import { AllTeamsViewModel, GetDriverTeamQuery, DriverTeamViewModel, TeamDetailsViewModel, TeamMembersViewModel, TeamJoinRequestsViewModel, CreateTeamInput, CreateTeamOutput, UpdateTeamInput, UpdateTeamOutput, ApproveTeamJoinRequestInput, ApproveTeamJoinRequestOutput, RejectTeamJoinRequestInput, RejectTeamJoinRequestOutput } from './dto/TeamDto';
|
||||
|
||||
// Use cases
|
||||
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
|
||||
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
|
||||
import { GetTeamDetailsUseCase } from '@gridpilot/racing/application/use-cases/GetTeamDetailsUseCase';
|
||||
import { GetTeamMembersUseCase } from '@gridpilot/racing/application/use-cases/GetTeamMembersUseCase';
|
||||
import { GetTeamJoinRequestsUseCase } from '@gridpilot/racing/application/use-cases/GetTeamJoinRequestsUseCase';
|
||||
import { CreateTeamUseCase } from '@gridpilot/racing/application/use-cases/CreateTeamUseCase';
|
||||
import { UpdateTeamUseCase } from '@gridpilot/racing/application/use-cases/UpdateTeamUseCase';
|
||||
import { ApproveTeamJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/ApproveTeamJoinRequestUseCase';
|
||||
import { RejectTeamJoinRequestUseCase } from '@gridpilot/racing/application/use-cases/RejectTeamJoinRequestUseCase';
|
||||
|
||||
// Presenters
|
||||
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
|
||||
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
|
||||
import { TeamDetailsPresenter } from './presenters/TeamDetailsPresenter';
|
||||
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
|
||||
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
|
||||
|
||||
// Logger
|
||||
import { Logger } from '@gridpilot/shared/application/Logger';
|
||||
|
||||
// Tokens
|
||||
import { TEAM_GET_ALL_USE_CASE_TOKEN, TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN, TEAM_LOGGER_TOKEN } from './TeamProviders';
|
||||
import {
|
||||
TEAM_GET_ALL_USE_CASE_TOKEN,
|
||||
TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
|
||||
TEAM_GET_DETAILS_USE_CASE_TOKEN,
|
||||
TEAM_GET_MEMBERS_USE_CASE_TOKEN,
|
||||
TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN,
|
||||
TEAM_CREATE_USE_CASE_TOKEN,
|
||||
TEAM_UPDATE_USE_CASE_TOKEN,
|
||||
TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN,
|
||||
TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN,
|
||||
TEAM_LOGGER_TOKEN
|
||||
} from './TeamProviders';
|
||||
|
||||
@Injectable()
|
||||
export class TeamService {
|
||||
constructor(
|
||||
@Inject(TEAM_GET_ALL_USE_CASE_TOKEN) private readonly getAllTeamsUseCase: GetAllTeamsUseCase,
|
||||
@Inject(TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN) private readonly getDriverTeamUseCase: GetDriverTeamUseCase,
|
||||
@Inject(TEAM_GET_DETAILS_USE_CASE_TOKEN) private readonly getTeamDetailsUseCase: GetTeamDetailsUseCase,
|
||||
@Inject(TEAM_GET_MEMBERS_USE_CASE_TOKEN) private readonly getTeamMembersUseCase: GetTeamMembersUseCase,
|
||||
@Inject(TEAM_GET_JOIN_REQUESTS_USE_CASE_TOKEN) private readonly getTeamJoinRequestsUseCase: GetTeamJoinRequestsUseCase,
|
||||
@Inject(TEAM_CREATE_USE_CASE_TOKEN) private readonly createTeamUseCase: CreateTeamUseCase,
|
||||
@Inject(TEAM_UPDATE_USE_CASE_TOKEN) private readonly updateTeamUseCase: UpdateTeamUseCase,
|
||||
@Inject(TEAM_APPROVE_JOIN_REQUEST_USE_CASE_TOKEN) private readonly approveTeamJoinRequestUseCase: ApproveTeamJoinRequestUseCase,
|
||||
@Inject(TEAM_REJECT_JOIN_REQUEST_USE_CASE_TOKEN) private readonly rejectTeamJoinRequestUseCase: RejectTeamJoinRequestUseCase,
|
||||
@Inject(TEAM_LOGGER_TOKEN) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -28,7 +56,7 @@ export class TeamService {
|
||||
|
||||
const presenter = new AllTeamsPresenter();
|
||||
await this.getAllTeamsUseCase.execute(undefined, presenter);
|
||||
return presenter.viewModel;
|
||||
return presenter.viewModel as unknown as AllTeamsViewModel;
|
||||
}
|
||||
|
||||
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
|
||||
@@ -37,10 +65,104 @@ export class TeamService {
|
||||
const presenter = new DriverTeamPresenter();
|
||||
try {
|
||||
await this.getDriverTeamUseCase.execute({ driverId: query.driverId }, presenter);
|
||||
return presenter.viewModel;
|
||||
return presenter.viewModel as unknown as DriverTeamViewModel;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching driver team: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getTeamDetails(teamId: string): Promise<TeamDetailsViewModel | null> {
|
||||
this.logger.debug(`[TeamService] Fetching team details for teamId: ${teamId}`);
|
||||
|
||||
const presenter = new TeamDetailsPresenter();
|
||||
try {
|
||||
await this.getTeamDetailsUseCase.execute({ teamId, driverId: '' }, presenter);
|
||||
return presenter.viewModel as unknown as TeamDetailsViewModel;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error fetching team details: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getTeamMembers(teamId: string): Promise<TeamMembersViewModel> {
|
||||
this.logger.debug(`[TeamService] Fetching team members for teamId: ${teamId}`);
|
||||
|
||||
const presenter = new TeamMembersPresenter();
|
||||
await this.getTeamMembersUseCase.execute({ teamId }, presenter);
|
||||
return presenter.viewModel as unknown as TeamMembersViewModel;
|
||||
}
|
||||
|
||||
async getTeamJoinRequests(teamId: string): Promise<TeamJoinRequestsViewModel> {
|
||||
this.logger.debug(`[TeamService] Fetching join requests for teamId: ${teamId}`);
|
||||
|
||||
const presenter = new TeamJoinRequestsPresenter();
|
||||
await this.getTeamJoinRequestsUseCase.execute({ teamId }, presenter);
|
||||
return presenter.viewModel as unknown as TeamJoinRequestsViewModel;
|
||||
}
|
||||
|
||||
async createTeam(input: CreateTeamInput): Promise<CreateTeamOutput> {
|
||||
this.logger.debug('[TeamService] Creating team', input);
|
||||
|
||||
try {
|
||||
const result = await this.createTeamUseCase.execute({
|
||||
name: input.name,
|
||||
tag: input.tag,
|
||||
description: input.description,
|
||||
ownerId: input.ownerId,
|
||||
leagues: [],
|
||||
});
|
||||
return {
|
||||
teamId: result.team.id,
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating team: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateTeam(input: UpdateTeamInput & { teamId: string }): Promise<UpdateTeamOutput> {
|
||||
this.logger.debug('[TeamService] Updating team', input);
|
||||
|
||||
try {
|
||||
await this.updateTeamUseCase.execute({
|
||||
teamId: input.teamId,
|
||||
updates: {
|
||||
name: input.name,
|
||||
tag: input.tag,
|
||||
description: input.description,
|
||||
},
|
||||
updatedBy: input.updatedBy,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating team: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async approveTeamJoinRequest(input: ApproveTeamJoinRequestInput & { teamId: string }): Promise<ApproveTeamJoinRequestOutput> {
|
||||
this.logger.debug('[TeamService] Approving team join request', input);
|
||||
|
||||
try {
|
||||
await this.approveTeamJoinRequestUseCase.execute({ requestId: input.requestId });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error approving join request: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async rejectTeamJoinRequest(input: RejectTeamJoinRequestInput & { teamId: string }): Promise<RejectTeamJoinRequestOutput> {
|
||||
this.logger.debug('[TeamService] Rejecting team join request', input);
|
||||
|
||||
try {
|
||||
await this.rejectTeamJoinRequestUseCase.execute({ requestId: input.requestId });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.logger.error(`Error rejecting join request: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate, IsOptional } from 'class-validator';
|
||||
|
||||
export class TeamLeagueDto {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
logoUrl?: string;
|
||||
}
|
||||
import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator';
|
||||
|
||||
export class TeamListItemViewModel {
|
||||
@ApiProperty()
|
||||
@@ -19,17 +8,26 @@ export class TeamListItemViewModel {
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
tag?: string;
|
||||
@ApiProperty()
|
||||
tag: string;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
description?: string;
|
||||
@ApiProperty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty()
|
||||
memberCount: number;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
@ApiProperty({ type: [String] })
|
||||
leagues: string[];
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
specialization?: 'endurance' | 'sprint' | 'mixed';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
region?: string;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
languages?: string[];
|
||||
}
|
||||
|
||||
export class AllTeamsViewModel {
|
||||
@@ -40,11 +38,169 @@ export class AllTeamsViewModel {
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class TeamDto {
|
||||
export class TeamViewModel {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@ApiProperty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
tag: string;
|
||||
|
||||
@ApiProperty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: [String] })
|
||||
leagues: string[];
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
createdAt?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
specialization?: 'endurance' | 'sprint' | 'mixed';
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
region?: string;
|
||||
|
||||
@ApiProperty({ type: [String], required: false })
|
||||
languages?: string[];
|
||||
}
|
||||
|
||||
export enum MembershipRole {
|
||||
OWNER = 'owner',
|
||||
MANAGER = 'manager',
|
||||
MEMBER = 'member',
|
||||
}
|
||||
|
||||
export enum MembershipStatus {
|
||||
ACTIVE = 'active',
|
||||
PENDING = 'pending',
|
||||
INVITED = 'invited',
|
||||
INACTIVE = 'inactive',
|
||||
}
|
||||
|
||||
export class MembershipViewModel {
|
||||
@ApiProperty()
|
||||
role: 'owner' | 'manager' | 'member';
|
||||
|
||||
@ApiProperty()
|
||||
joinedAt: string;
|
||||
|
||||
@ApiProperty()
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class DriverTeamViewModel {
|
||||
@ApiProperty({ type: TeamViewModel })
|
||||
team: TeamViewModel;
|
||||
|
||||
@ApiProperty({ type: MembershipViewModel })
|
||||
membership: MembershipViewModel;
|
||||
|
||||
@ApiProperty()
|
||||
isOwner: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
teamId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class TeamDetailsViewModel {
|
||||
@ApiProperty({ type: TeamViewModel })
|
||||
team: TeamViewModel;
|
||||
|
||||
@ApiProperty({ type: MembershipViewModel, nullable: true })
|
||||
membership: MembershipViewModel | null;
|
||||
|
||||
@ApiProperty()
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export class TeamMemberViewModel {
|
||||
@ApiProperty()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
driverName: string;
|
||||
|
||||
@ApiProperty()
|
||||
role: 'owner' | 'manager' | 'member';
|
||||
|
||||
@ApiProperty()
|
||||
joinedAt: string;
|
||||
|
||||
@ApiProperty()
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export class TeamMembersViewModel {
|
||||
@ApiProperty({ type: [TeamMemberViewModel] })
|
||||
members: TeamMemberViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
ownerCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
managerCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
export class TeamJoinRequestViewModel {
|
||||
@ApiProperty()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty()
|
||||
driverId: string;
|
||||
|
||||
@ApiProperty()
|
||||
driverName: string;
|
||||
|
||||
@ApiProperty()
|
||||
teamId: string;
|
||||
|
||||
@ApiProperty()
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
|
||||
@ApiProperty()
|
||||
requestedAt: string;
|
||||
|
||||
@ApiProperty()
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export class TeamJoinRequestsViewModel {
|
||||
@ApiProperty({ type: [TeamJoinRequestViewModel] })
|
||||
requests: TeamJoinRequestViewModel[];
|
||||
|
||||
@ApiProperty()
|
||||
pendingCount: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export class CreateTeamInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@@ -63,60 +219,80 @@ export class TeamDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId: string;
|
||||
|
||||
@ApiProperty({ type: [TeamLeagueDto] })
|
||||
leagues: TeamLeagueDto[];
|
||||
}
|
||||
|
||||
export enum MembershipRole {
|
||||
OWNER = 'owner',
|
||||
MANAGER = 'manager',
|
||||
MEMBER = 'member',
|
||||
}
|
||||
|
||||
export enum MembershipStatus {
|
||||
ACTIVE = 'active',
|
||||
PENDING = 'pending',
|
||||
INVITED = 'invited',
|
||||
INACTIVE = 'inactive',
|
||||
}
|
||||
|
||||
export class MembershipDto {
|
||||
@ApiProperty({ enum: MembershipRole })
|
||||
@IsEnum(MembershipRole)
|
||||
role: MembershipRole;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDate()
|
||||
joinedAt: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class DriverTeamViewModel {
|
||||
@ApiProperty({ type: TeamDto })
|
||||
team: TeamDto;
|
||||
|
||||
@ApiProperty({ type: MembershipDto })
|
||||
membership: MembershipDto;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
isOwner: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export class GetDriverTeamQuery {
|
||||
export class CreateTeamOutput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
teamId: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
driverId: string;
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class UpdateTeamInput {
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
teamId?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
tag?: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
updatedBy: string;
|
||||
}
|
||||
|
||||
export class UpdateTeamOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class ApproveTeamJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export class ApproveTeamJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export class RejectTeamJoinRequestInput {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
requestId: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export class RejectTeamJoinRequestOutput {
|
||||
@ApiProperty()
|
||||
@IsBoolean()
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel } from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
|
||||
import { TeamListItemViewModel } from '../dto/TeamDto';
|
||||
import { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel, TeamListItemViewModel } from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
|
||||
|
||||
export class AllTeamsPresenter implements IAllTeamsPresenter {
|
||||
private result: AllTeamsViewModel | null = null;
|
||||
@@ -24,6 +23,10 @@ export class AllTeamsPresenter implements IAllTeamsPresenter {
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): AllTeamsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): AllTeamsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IDriverTeamPresenter, DriverTeamResultDTO, DriverTeamViewModel } from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
|
||||
import { TeamDto, MembershipDto, MembershipRole } from '../dto/TeamDto';
|
||||
|
||||
export class DriverTeamPresenter implements IDriverTeamPresenter {
|
||||
private result: DriverTeamViewModel | null = null;
|
||||
@@ -9,32 +8,32 @@ export class DriverTeamPresenter implements IDriverTeamPresenter {
|
||||
}
|
||||
|
||||
present(dto: DriverTeamResultDTO) {
|
||||
const team: TeamDto = {
|
||||
id: dto.team.id,
|
||||
name: dto.team.name,
|
||||
tag: dto.team.tag,
|
||||
description: dto.team.description,
|
||||
ownerId: dto.team.ownerId,
|
||||
leagues: dto.team.leagues || [],
|
||||
};
|
||||
|
||||
const membership: MembershipDto = {
|
||||
role: dto.membership.role as MembershipRole,
|
||||
joinedAt: dto.membership.joinedAt,
|
||||
isActive: dto.membership.status === 'active',
|
||||
};
|
||||
|
||||
const isOwner = dto.team.ownerId === dto.driverId;
|
||||
const canManage = isOwner || membership.role === MembershipRole.MANAGER;
|
||||
const canManage = isOwner || dto.membership.role === 'owner' || dto.membership.role === 'manager';
|
||||
|
||||
this.result = {
|
||||
team,
|
||||
membership,
|
||||
team: {
|
||||
id: dto.team.id,
|
||||
name: dto.team.name,
|
||||
tag: dto.team.tag,
|
||||
description: dto.team.description || '',
|
||||
ownerId: dto.team.ownerId,
|
||||
leagues: dto.team.leagues || [],
|
||||
},
|
||||
membership: {
|
||||
role: dto.membership.role as 'owner' | 'manager' | 'member',
|
||||
joinedAt: dto.membership.joinedAt.toISOString(),
|
||||
isActive: dto.membership.status === 'active',
|
||||
},
|
||||
isOwner,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): DriverTeamViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): DriverTeamViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
|
||||
50
apps/api/src/modules/team/presenters/TeamDetailsPresenter.ts
Normal file
50
apps/api/src/modules/team/presenters/TeamDetailsPresenter.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
ITeamDetailsPresenter,
|
||||
TeamDetailsResultDTO,
|
||||
TeamDetailsViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
|
||||
|
||||
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||
private result: TeamDetailsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TeamDetailsResultDTO) {
|
||||
const { team, membership } = dto;
|
||||
|
||||
const canManage =
|
||||
membership !== null &&
|
||||
(membership.role === 'owner' || membership.role === 'manager');
|
||||
|
||||
this.result = {
|
||||
team: {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
ownerId: team.ownerId,
|
||||
leagues: team.leagues || [],
|
||||
createdAt: team.createdAt?.toISOString() || new Date().toISOString(),
|
||||
},
|
||||
membership: membership
|
||||
? {
|
||||
role: membership.role as 'owner' | 'manager' | 'member',
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isActive: membership.status === 'active',
|
||||
}
|
||||
: null,
|
||||
canManage,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamDetailsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): TeamDetailsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
ITeamJoinRequestsPresenter,
|
||||
TeamJoinRequestsResultDTO,
|
||||
TeamJoinRequestsViewModel,
|
||||
TeamJoinRequestViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamJoinRequestsPresenter';
|
||||
|
||||
export class TeamJoinRequestsPresenter implements ITeamJoinRequestsPresenter {
|
||||
private result: TeamJoinRequestsViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TeamJoinRequestsResultDTO) {
|
||||
const { requests, driverNames, avatarUrls } = dto;
|
||||
|
||||
const requestViewModels: TeamJoinRequestViewModel[] = requests.map((request) => ({
|
||||
requestId: request.id,
|
||||
driverId: request.driverId,
|
||||
driverName: driverNames[request.driverId] || 'Unknown',
|
||||
teamId: request.teamId,
|
||||
status: 'pending' as const,
|
||||
requestedAt: request.requestedAt.toISOString(),
|
||||
avatarUrl: avatarUrls[request.driverId] || '',
|
||||
}));
|
||||
|
||||
this.result = {
|
||||
requests: requestViewModels,
|
||||
pendingCount: requestViewModels.length,
|
||||
totalCount: requestViewModels.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamJoinRequestsViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): TeamJoinRequestsViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
48
apps/api/src/modules/team/presenters/TeamMembersPresenter.ts
Normal file
48
apps/api/src/modules/team/presenters/TeamMembersPresenter.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ITeamMembersPresenter,
|
||||
TeamMembersResultDTO,
|
||||
TeamMembersViewModel,
|
||||
TeamMemberViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/ITeamMembersPresenter';
|
||||
|
||||
export class TeamMembersPresenter implements ITeamMembersPresenter {
|
||||
private result: TeamMembersViewModel | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(dto: TeamMembersResultDTO) {
|
||||
const { memberships, driverNames, avatarUrls } = dto;
|
||||
|
||||
const members: TeamMemberViewModel[] = memberships.map((membership) => ({
|
||||
driverId: membership.driverId,
|
||||
driverName: driverNames[membership.driverId] || 'Unknown',
|
||||
role: membership.role as 'owner' | 'manager' | 'member',
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
isActive: membership.status === 'active',
|
||||
avatarUrl: avatarUrls[membership.driverId] || '',
|
||||
}));
|
||||
|
||||
const ownerCount = members.filter((m) => m.role === 'owner').length;
|
||||
const managerCount = members.filter((m) => m.role === 'manager').length;
|
||||
const memberCount = members.filter((m) => m.role === 'member').length;
|
||||
|
||||
this.result = {
|
||||
members,
|
||||
totalCount: members.length,
|
||||
ownerCount,
|
||||
managerCount,
|
||||
memberCount,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): TeamMembersViewModel | null {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
get viewModel(): TeamMembersViewModel {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user