refactor
This commit is contained in:
@@ -8,11 +8,31 @@
|
|||||||
"sourceType": "module",
|
"sourceType": "module",
|
||||||
"ecmaVersion": 2022
|
"ecmaVersion": 2022
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"boundaries/elements": [
|
||||||
|
{
|
||||||
|
"type": "website",
|
||||||
|
"pattern": "apps/website/**/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "api",
|
||||||
|
"pattern": "apps/api/**/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "adapters",
|
||||||
|
"pattern": "adapters/**/*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "core",
|
||||||
|
"pattern": "core/**/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["**/*.ts", "**/*.tsx"],
|
"files": ["**/*.ts", "**/*.tsx"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint", "boundaries"],
|
||||||
"extends": [],
|
"extends": [],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
@@ -25,6 +45,30 @@
|
|||||||
"varsIgnorePattern": "^$",
|
"varsIgnorePattern": "^$",
|
||||||
"caughtErrors": "all"
|
"caughtErrors": "all"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"boundaries/element-types": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"default": "disallow",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"from": ["website"],
|
||||||
|
"allow": ["website"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": ["api"],
|
||||||
|
"allow": ["api", "adapters", "core"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": ["adapters"],
|
||||||
|
"allow": ["adapters", "core"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": ["core"],
|
||||||
|
"allow": ["core"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: InMemoryMembershipFeeRepository
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
|
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '../../../../core/payments/domain/repositories/IMembershipFeeRepository';
|
||||||
|
import type { MembershipFee } from '../../../../core/payments/domain/entities/MembershipFee';
|
||||||
|
import type { MemberPayment } from '../../../../core/payments/domain/entities/MemberPayment';
|
||||||
|
|
||||||
|
const membershipFees: Map<string, MembershipFee> = new Map();
|
||||||
|
const memberPayments: Map<string, MemberPayment> = new Map();
|
||||||
|
|
||||||
|
export class InMemoryMembershipFeeRepository implements IMembershipFeeRepository {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<MembershipFee | null> {
|
||||||
|
this.logger.debug('[InMemoryMembershipFeeRepository] findById', { id });
|
||||||
|
return membershipFees.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByLeagueId(leagueId: string): Promise<MembershipFee | null> {
|
||||||
|
this.logger.debug('[InMemoryMembershipFeeRepository] findByLeagueId', { leagueId });
|
||||||
|
return Array.from(membershipFees.values()).find(f => f.leagueId === leagueId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(fee: MembershipFee): Promise<MembershipFee> {
|
||||||
|
this.logger.debug('[InMemoryMembershipFeeRepository] create', { fee });
|
||||||
|
membershipFees.set(fee.id, fee);
|
||||||
|
return fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(fee: MembershipFee): Promise<MembershipFee> {
|
||||||
|
this.logger.debug('[InMemoryMembershipFeeRepository] update', { fee });
|
||||||
|
membershipFees.set(fee.id, fee);
|
||||||
|
return fee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InMemoryMemberPaymentRepository implements IMemberPaymentRepository {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<MemberPayment | null> {
|
||||||
|
this.logger.debug('[InMemoryMemberPaymentRepository] findById', { id });
|
||||||
|
return memberPayments.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByFeeIdAndDriverId(feeId: string, driverId: string): Promise<MemberPayment | null> {
|
||||||
|
this.logger.debug('[InMemoryMemberPaymentRepository] findByFeeIdAndDriverId', { feeId, driverId });
|
||||||
|
return Array.from(memberPayments.values()).find(
|
||||||
|
p => p.feeId === feeId && p.driverId === driverId
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByLeagueIdAndDriverId(leagueId: string, driverId: string, membershipFeeRepo: IMembershipFeeRepository): Promise<MemberPayment[]> {
|
||||||
|
this.logger.debug('[InMemoryMemberPaymentRepository] findByLeagueIdAndDriverId', { leagueId, driverId });
|
||||||
|
const results: MemberPayment[] = [];
|
||||||
|
for (const payment of memberPayments.values()) {
|
||||||
|
const fee = await membershipFeeRepo.findById(payment.feeId);
|
||||||
|
if (fee && fee.leagueId === leagueId && payment.driverId === driverId) {
|
||||||
|
results.push(payment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(payment: MemberPayment): Promise<MemberPayment> {
|
||||||
|
this.logger.debug('[InMemoryMemberPaymentRepository] create', { payment });
|
||||||
|
memberPayments.set(payment.id, payment);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(payment: MemberPayment): Promise<MemberPayment> {
|
||||||
|
this.logger.debug('[InMemoryMemberPaymentRepository] update', { payment });
|
||||||
|
memberPayments.set(payment.id, payment);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: InMemoryPaymentRepository
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
|
import type { IPaymentRepository } from '../../../../core/payments/domain/repositories/IPaymentRepository';
|
||||||
|
import type { Payment, PaymentType } from '../../../../core/payments/domain/entities/Payment';
|
||||||
|
|
||||||
|
const payments: Map<string, Payment> = new Map();
|
||||||
|
|
||||||
|
export class InMemoryPaymentRepository implements IPaymentRepository {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Payment | null> {
|
||||||
|
this.logger.debug('[InMemoryPaymentRepository] findById', { id });
|
||||||
|
return payments.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByLeagueId(leagueId: string): Promise<Payment[]> {
|
||||||
|
this.logger.debug('[InMemoryPaymentRepository] findByLeagueId', { leagueId });
|
||||||
|
return Array.from(payments.values()).filter(p => p.leagueId === leagueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByPayerId(payerId: string): Promise<Payment[]> {
|
||||||
|
this.logger.debug('[InMemoryPaymentRepository] findByPayerId', { payerId });
|
||||||
|
return Array.from(payments.values()).filter(p => p.payerId === payerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByType(type: PaymentType): Promise<Payment[]> {
|
||||||
|
this.logger.debug('[InMemoryPaymentRepository] findByType', { type });
|
||||||
|
return Array.from(payments.values()).filter(p => p.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByFilters(filters: { leagueId?: string; payerId?: string; type?: PaymentType }): Promise<Payment[]> {
|
||||||
|
this.logger.debug('[InMemoryPaymentRepository] findByFilters', { filters });
|
||||||
|
let results = Array.from(payments.values());
|
||||||
|
|
||||||
|
if (filters.leagueId) {
|
||||||
|
results = results.filter(p => p.leagueId === filters.leagueId);
|
||||||
|
}
|
||||||
|
if (filters.payerId) {
|
||||||
|
results = results.filter(p => p.payerId === filters.payerId);
|
||||||
|
}
|
||||||
|
if (filters.type) {
|
||||||
|
results = results.filter(p => p.type === filters.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(payment: Payment): Promise<Payment> {
|
||||||
|
this.logger.debug('[InMemoryPaymentRepository] create', { payment });
|
||||||
|
payments.set(payment.id, payment);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(payment: Payment): Promise<Payment> {
|
||||||
|
this.logger.debug('[InMemoryPaymentRepository] update', { payment });
|
||||||
|
payments.set(payment.id, payment);
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: InMemoryPrizeRepository
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
|
import type { IPrizeRepository } from '../../../../core/payments/domain/repositories/IPrizeRepository';
|
||||||
|
import type { Prize } from '../../../../core/payments/domain/entities/Prize';
|
||||||
|
|
||||||
|
const prizes: Map<string, Prize> = new Map();
|
||||||
|
|
||||||
|
export class InMemoryPrizeRepository implements IPrizeRepository {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Prize | null> {
|
||||||
|
this.logger.debug('[InMemoryPrizeRepository] findById', { id });
|
||||||
|
return prizes.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByLeagueId(leagueId: string): Promise<Prize[]> {
|
||||||
|
this.logger.debug('[InMemoryPrizeRepository] findByLeagueId', { leagueId });
|
||||||
|
return Array.from(prizes.values()).filter(p => p.leagueId === leagueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByLeagueIdAndSeasonId(leagueId: string, seasonId: string): Promise<Prize[]> {
|
||||||
|
this.logger.debug('[InMemoryPrizeRepository] findByLeagueIdAndSeasonId', { leagueId, seasonId });
|
||||||
|
return Array.from(prizes.values()).filter(
|
||||||
|
p => p.leagueId === leagueId && p.seasonId === seasonId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByPosition(leagueId: string, seasonId: string, position: number): Promise<Prize | null> {
|
||||||
|
this.logger.debug('[InMemoryPrizeRepository] findByPosition', { leagueId, seasonId, position });
|
||||||
|
return Array.from(prizes.values()).find(
|
||||||
|
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(prize: Prize): Promise<Prize> {
|
||||||
|
this.logger.debug('[InMemoryPrizeRepository] create', { prize });
|
||||||
|
prizes.set(prize.id, prize);
|
||||||
|
return prize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(prize: Prize): Promise<Prize> {
|
||||||
|
this.logger.debug('[InMemoryPrizeRepository] update', { prize });
|
||||||
|
prizes.set(prize.id, prize);
|
||||||
|
return prize;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
this.logger.debug('[InMemoryPrizeRepository] delete', { id });
|
||||||
|
prizes.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: InMemoryWalletRepository
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
|
import type { IWalletRepository, ITransactionRepository } from '../../../../core/payments/domain/repositories/IWalletRepository';
|
||||||
|
import type { Wallet, Transaction } from '../../../../core/payments/domain/entities/Wallet';
|
||||||
|
|
||||||
|
const wallets: Map<string, Wallet> = new Map();
|
||||||
|
const transactions: Map<string, Transaction> = new Map();
|
||||||
|
|
||||||
|
export class InMemoryWalletRepository implements IWalletRepository {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Wallet | null> {
|
||||||
|
this.logger.debug('[InMemoryWalletRepository] findById', { id });
|
||||||
|
return wallets.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByLeagueId(leagueId: string): Promise<Wallet | null> {
|
||||||
|
this.logger.debug('[InMemoryWalletRepository] findByLeagueId', { leagueId });
|
||||||
|
return Array.from(wallets.values()).find(w => w.leagueId === leagueId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(wallet: Wallet): Promise<Wallet> {
|
||||||
|
this.logger.debug('[InMemoryWalletRepository] create', { wallet });
|
||||||
|
wallets.set(wallet.id, wallet);
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(wallet: Wallet): Promise<Wallet> {
|
||||||
|
this.logger.debug('[InMemoryWalletRepository] update', { wallet });
|
||||||
|
wallets.set(wallet.id, wallet);
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InMemoryTransactionRepository implements ITransactionRepository {
|
||||||
|
constructor(private readonly logger: Logger) {}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Transaction | null> {
|
||||||
|
this.logger.debug('[InMemoryTransactionRepository] findById', { id });
|
||||||
|
return transactions.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByWalletId(walletId: string): Promise<Transaction[]> {
|
||||||
|
this.logger.debug('[InMemoryTransactionRepository] findByWalletId', { walletId });
|
||||||
|
return Array.from(transactions.values()).filter(t => t.walletId === walletId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(transaction: Transaction): Promise<Transaction> {
|
||||||
|
this.logger.debug('[InMemoryTransactionRepository] create', { transaction });
|
||||||
|
transactions.set(transaction.id, transaction);
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
adapters/payments/persistence/inmemory/index.ts
Normal file
4
adapters/payments/persistence/inmemory/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './InMemoryPaymentRepository';
|
||||||
|
export * from './InMemoryMembershipFeeRepository';
|
||||||
|
export * from './InMemoryPrizeRepository';
|
||||||
|
export * from './InMemoryWalletRepository';
|
||||||
@@ -23,6 +23,19 @@ export class DriverController {
|
|||||||
return this.driverService.getTotalDrivers();
|
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')
|
@Post('complete-onboarding')
|
||||||
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
|
@ApiOperation({ summary: 'Complete driver onboarding for a user' })
|
||||||
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
|
@ApiResponse({ status: 200, description: 'Onboarding complete', type: CompleteOnboardingOutput })
|
||||||
|
|||||||
@@ -1,41 +1,82 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
import { Provider } from '@nestjs/common';
|
||||||
import { MediaService } from './MediaService';
|
import { MediaService } from './MediaService';
|
||||||
|
|
||||||
// Due to persistent module resolution issues in the environment,
|
// Import core interfaces
|
||||||
// actual core interfaces and adapter implementations are not directly imported here.
|
import { IAvatarGenerationRepository } from '@gridpilot/media/domain/repositories/IAvatarGenerationRepository';
|
||||||
// In a functional TypeScript environment, these would be imported as follows:
|
import { FaceValidationPort } from '@gridpilot/media/application/ports/FaceValidationPort';
|
||||||
/*
|
import { AvatarGenerationPort } from '@gridpilot/media/application/ports/AvatarGenerationPort';
|
||||||
import { IAvatarGenerationRepository } from 'core/media/domain/repositories/IAvatarGenerationRepository';
|
import { Logger } from '@gridpilot/shared/application';
|
||||||
import { FaceValidationPort } from 'core/media/application/ports/FaceValidationPort';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
import { InMemoryAvatarGenerationRepository } from 'adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
// Import use cases
|
||||||
import { InMemoryFaceValidationAdapter } from 'adapters/media/ports/InMemoryFaceValidationAdapter';
|
import { RequestAvatarGenerationUseCase } from '@gridpilot/media/application/use-cases/RequestAvatarGenerationUseCase';
|
||||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Define injection tokens as string literals for NestJS
|
// Define injection tokens
|
||||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
export const AVATAR_GENERATION_REPOSITORY_TOKEN = 'IAvatarGenerationRepository';
|
||||||
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
|
export const FACE_VALIDATION_PORT_TOKEN = 'FaceValidationPort';
|
||||||
|
export const AVATAR_GENERATION_PORT_TOKEN = 'AvatarGenerationPort';
|
||||||
export const LOGGER_TOKEN = 'Logger';
|
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[] = [
|
export const MediaProviders: Provider[] = [
|
||||||
MediaService, // Provide the service itself
|
MediaService, // Provide the service itself
|
||||||
// In a functional setup, the following would be enabled:
|
|
||||||
/*
|
|
||||||
{
|
{
|
||||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryAvatarGenerationRepository(logger),
|
useClass: MockAvatarGenerationRepository,
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: FACE_VALIDATION_PORT_TOKEN,
|
provide: FACE_VALIDATION_PORT_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryFaceValidationAdapter(logger),
|
useClass: MockFaceValidationAdapter,
|
||||||
inject: [LOGGER_TOKEN],
|
},
|
||||||
|
{
|
||||||
|
provide: AVATAR_GENERATION_PORT_TOKEN,
|
||||||
|
useClass: MockAvatarGenerationAdapter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: LOGGER_TOKEN,
|
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 { Injectable, Inject } from '@nestjs/common';
|
||||||
import { RequestAvatarGenerationInput, RequestAvatarGenerationOutput } from './dto/MediaDto'; // Assuming these DTOs are defined
|
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()
|
@Injectable()
|
||||||
export class MediaService {
|
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> {
|
async requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
|
||||||
console.log('[MediaService] Returning mock avatar generation request. Input:', input);
|
this.logger.debug('[MediaService] Requesting avatar generation.');
|
||||||
return {
|
|
||||||
success: true,
|
const presenter = new RequestAvatarGenerationPresenter();
|
||||||
requestId: `req-${Date.now()}`,
|
await this.requestAvatarGenerationUseCase.execute({
|
||||||
avatarUrls: [
|
userId: input.userId,
|
||||||
'https://cdn.example.com/avatars/mock-avatar-1.png',
|
facePhotoData: input.facePhotoData,
|
||||||
'https://cdn.example.com/avatars/mock-avatar-2.png',
|
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 { Provider } from '@nestjs/common';
|
||||||
import { PaymentsService } from './PaymentsService';
|
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 core interfaces
|
||||||
import { IPaymentRepository } from 'core/payments/domain/repositories/IPaymentRepository';
|
import type { IPaymentRepository } from '@gridpilot/payments/domain/repositories/IPaymentRepository';
|
||||||
import { IMembershipFeeRepository } from 'core/payments/domain/repositories/IMembershipFeeRepository';
|
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '@gridpilot/payments/domain/repositories/IMembershipFeeRepository';
|
||||||
import { IPrizeRepository } from 'core/payments/domain/repositories/IPrizeRepository';
|
import type { IPrizeRepository } from '@gridpilot/payments/domain/repositories/IPrizeRepository';
|
||||||
import { IWalletRepository } from 'core/payments/domain/repositories/IWalletRepository';
|
import type { IWalletRepository, ITransactionRepository } from '@gridpilot/payments/domain/repositories/IWalletRepository';
|
||||||
import { IPaymentGateway } from 'core/payments/application/ports/IPaymentGateway';
|
import type { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
import { Logger } from '@gridpilot/shared/logging/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 concrete in-memory implementations
|
||||||
import { InMemoryPaymentRepository } from 'adapters/payments/persistence/inmemory/InMemoryPaymentRepository';
|
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 { InMemoryPrizeRepository } from 'adapters/payments/persistence/inmemory/InMemoryPrizeRepository';
|
||||||
import { InMemoryWalletRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
import { InMemoryWalletRepository, InMemoryTransactionRepository } from 'adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
||||||
import { InMemoryPaymentGateway } from 'adapters/payments/ports/InMemoryPaymentGateway';
|
|
||||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
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 PAYMENT_REPOSITORY_TOKEN = 'IPaymentRepository';
|
||||||
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
|
export const MEMBERSHIP_FEE_REPOSITORY_TOKEN = 'IMembershipFeeRepository';
|
||||||
|
export const MEMBER_PAYMENT_REPOSITORY_TOKEN = 'IMemberPaymentRepository';
|
||||||
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
|
export const PRIZE_REPOSITORY_TOKEN = 'IPrizeRepository';
|
||||||
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
|
export const WALLET_REPOSITORY_TOKEN = 'IWalletRepository';
|
||||||
export const PAYMENT_GATEWAY_TOKEN = 'IPaymentGateway';
|
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
|
||||||
export const LOGGER_TOKEN = 'Logger'; // Already defined in other Providers, but good to have here too
|
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[] = [
|
export const PaymentsProviders: Provider[] = [
|
||||||
PaymentsService, // Provide the service itself
|
PaymentsService,
|
||||||
// In a functional setup, the following would be enabled:
|
|
||||||
/*
|
// Logger
|
||||||
|
{
|
||||||
|
provide: LOGGER_TOKEN,
|
||||||
|
useClass: ConsoleLogger,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Repositories (repositories are injected into use cases, NOT into services)
|
||||||
{
|
{
|
||||||
provide: PAYMENT_REPOSITORY_TOKEN,
|
provide: PAYMENT_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryPaymentRepository(logger),
|
useFactory: (logger: Logger) => new InMemoryPaymentRepository(logger),
|
||||||
@@ -44,6 +72,11 @@ export const PaymentsProviders: Provider[] = [
|
|||||||
useFactory: (logger: Logger) => new InMemoryMembershipFeeRepository(logger),
|
useFactory: (logger: Logger) => new InMemoryMembershipFeeRepository(logger),
|
||||||
inject: [LOGGER_TOKEN],
|
inject: [LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MEMBER_PAYMENT_REPOSITORY_TOKEN,
|
||||||
|
useFactory: (logger: Logger) => new InMemoryMemberPaymentRepository(logger),
|
||||||
|
inject: [LOGGER_TOKEN],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: PRIZE_REPOSITORY_TOKEN,
|
provide: PRIZE_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryPrizeRepository(logger),
|
useFactory: (logger: Logger) => new InMemoryPrizeRepository(logger),
|
||||||
@@ -55,13 +88,74 @@ export const PaymentsProviders: Provider[] = [
|
|||||||
inject: [LOGGER_TOKEN],
|
inject: [LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: PAYMENT_GATEWAY_TOKEN,
|
provide: TRANSACTION_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryPaymentGateway(logger),
|
useFactory: (logger: Logger) => new InMemoryTransactionRepository(logger),
|
||||||
inject: [LOGGER_TOKEN],
|
inject: [LOGGER_TOKEN],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Use cases (use cases receive repositories, services receive use cases)
|
||||||
{
|
{
|
||||||
provide: LOGGER_TOKEN,
|
provide: GET_PAYMENTS_USE_CASE_TOKEN,
|
||||||
useClass: ConsoleLogger,
|
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 { Injectable, Inject } 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 type { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
import { LeagueSettingsDto, LeagueConfigFormModelStructureDto } from '../league/dto/LeagueDto'; // For the mock data definitions
|
|
||||||
|
|
||||||
const payments: Map<string, PaymentDto> = new Map();
|
// Use cases
|
||||||
const membershipFees: Map<string, MembershipFeeDto> = new Map();
|
import type { GetPaymentsUseCase } from '@gridpilot/payments/application/use-cases/GetPaymentsUseCase';
|
||||||
const memberPayments: Map<string, MemberPaymentDto> = new Map();
|
import type { CreatePaymentUseCase } from '@gridpilot/payments/application/use-cases/CreatePaymentUseCase';
|
||||||
const prizes: Map<string, PrizeDto> = new Map();
|
import type { UpdatePaymentStatusUseCase } from '@gridpilot/payments/application/use-cases/UpdatePaymentStatusUseCase';
|
||||||
const wallets: Map<string, WalletDto> = new Map();
|
import type { GetMembershipFeesUseCase } from '@gridpilot/payments/application/use-cases/GetMembershipFeesUseCase';
|
||||||
const transactions: Map<string, TransactionDto> = new Map();
|
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()
|
@Injectable()
|
||||||
export class PaymentsService {
|
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> {
|
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
|
||||||
let results = Array.from(payments.values());
|
this.logger.debug('[PaymentsService] Getting payments', { query });
|
||||||
|
|
||||||
if (query.leagueId) {
|
const presenter = new GetPaymentsPresenter();
|
||||||
results = results.filter(p => p.leagueId === query.leagueId);
|
await this.getPaymentsUseCase.execute(query, presenter);
|
||||||
}
|
return presenter.viewModel;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
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 presenter = new CreatePaymentPresenter();
|
||||||
const netAmount = amount - platformFee;
|
await this.createPaymentUseCase.execute(input, presenter);
|
||||||
|
return presenter.viewModel;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
|
||||||
const { paymentId, status } = input;
|
this.logger.debug('[PaymentsService] Updating payment status', { input });
|
||||||
|
|
||||||
const payment = payments.get(paymentId);
|
const presenter = new UpdatePaymentStatusPresenter();
|
||||||
if (!payment) {
|
await this.updatePaymentStatusUseCase.execute(input, presenter);
|
||||||
throw new Error('Payment not found');
|
return presenter.viewModel;
|
||||||
}
|
|
||||||
|
|
||||||
payment.status = status;
|
|
||||||
if (status === PaymentStatus.COMPLETED) {
|
|
||||||
payment.completedAt = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
payments.set(paymentId, payment);
|
|
||||||
|
|
||||||
return { payment };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
|
||||||
const { leagueId, driverId } = query;
|
this.logger.debug('[PaymentsService] Getting membership fees', { query });
|
||||||
|
|
||||||
if (!leagueId) {
|
const presenter = new GetMembershipFeesPresenter();
|
||||||
throw new Error('leagueId is required');
|
await this.getMembershipFeesUseCase.execute(query, presenter);
|
||||||
}
|
return presenter.viewModel;
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
|
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
|
const presenter = new UpsertMembershipFeePresenter();
|
||||||
let existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId);
|
await this.upsertMembershipFeeUseCase.execute(input, presenter);
|
||||||
|
return presenter.viewModel;
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
|
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);
|
const presenter = new UpdateMemberPaymentPresenter();
|
||||||
if (!fee) {
|
await this.updateMemberPaymentUseCase.execute(input, presenter);
|
||||||
throw new Error('Membership fee configuration not found');
|
return presenter.viewModel;
|
||||||
}
|
|
||||||
|
|
||||||
// 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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
|
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);
|
const presenter = new GetPrizesPresenter();
|
||||||
|
await this.getPrizesUseCase.execute({ leagueId: query.leagueId!, seasonId: query.seasonId }, presenter);
|
||||||
if (seasonId) {
|
return presenter.viewModel;
|
||||||
results = results.filter(p => p.seasonId === seasonId);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.sort((a, b) => a.position - b.position);
|
|
||||||
|
|
||||||
return { prizes: results };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
|
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 presenter = new CreatePrizePresenter();
|
||||||
const existingPrize = Array.from(prizes.values()).find(
|
await this.createPrizeUseCase.execute(input, presenter);
|
||||||
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === position
|
return presenter.viewModel;
|
||||||
);
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
|
||||||
const { prizeId, driverId } = input;
|
this.logger.debug('[PaymentsService] Awarding prize', { input });
|
||||||
|
|
||||||
const prize = prizes.get(prizeId);
|
const presenter = new AwardPrizePresenter();
|
||||||
if (!prize) {
|
await this.awardPrizeUseCase.execute(input, presenter);
|
||||||
throw new Error('Prize not found');
|
return presenter.viewModel;
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
|
||||||
const { prizeId } = input;
|
this.logger.debug('[PaymentsService] Deleting prize', { input });
|
||||||
|
|
||||||
const prize = prizes.get(prizeId);
|
const presenter = new DeletePrizePresenter();
|
||||||
if (!prize) {
|
await this.deletePrizeUseCase.execute(input, presenter);
|
||||||
throw new Error('Prize not found');
|
return presenter.viewModel;
|
||||||
}
|
|
||||||
|
|
||||||
if (prize.awarded) {
|
|
||||||
throw new Error('Cannot delete an awarded prize');
|
|
||||||
}
|
|
||||||
|
|
||||||
prizes.delete(prizeId);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
|
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
|
||||||
const { leagueId } = query;
|
this.logger.debug('[PaymentsService] Getting wallet', { query });
|
||||||
|
|
||||||
if (!leagueId) {
|
const presenter = new GetWalletPresenter();
|
||||||
throw new Error('LeagueId is required');
|
await this.getWalletUseCase.execute({ leagueId: query.leagueId! }, presenter);
|
||||||
}
|
return presenter.viewModel;
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
|
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) {
|
const presenter = new ProcessWalletTransactionPresenter();
|
||||||
throw new Error('Missing required fields: leagueId, type, amount, description');
|
await this.processWalletTransactionUseCase.execute(input, presenter);
|
||||||
}
|
return presenter.viewModel;
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { Provider } from '@nestjs/common';
|
||||||
import { RaceService } from './RaceService';
|
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[] = [
|
export const RaceProviders: Provider[] = [
|
||||||
RaceService,
|
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,
|
useClass: ConsoleLogger,
|
||||||
},
|
},
|
||||||
|
// Use cases
|
||||||
{
|
{
|
||||||
provide: 'IRaceRepository',
|
provide: GetAllRacesUseCase,
|
||||||
useClass: InMemoryRaceRepository,
|
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';
|
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()
|
@Injectable()
|
||||||
export class RaceService {
|
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> {
|
const presenter = new GetAllRacesPresenter();
|
||||||
console.log('[RaceService] Returning mock all races.');
|
await this.getAllRacesUseCase.execute({}, presenter);
|
||||||
return Promise.resolve({
|
return presenter.getViewModel()!;
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalRaces(): Promise<RaceStatsDto> {
|
async getTotalRaces(): Promise<RaceStatsDto> {
|
||||||
console.log('[RaceService] Returning mock total races.');
|
this.logger.debug('[RaceService] Fetching total races count.');
|
||||||
return Promise.resolve({
|
const presenter = new GetTotalRacesPresenter();
|
||||||
totalRaces: 2, // Placeholder
|
await this.getTotalRacesUseCase.execute({}, presenter);
|
||||||
});
|
return presenter.getViewModel()!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
|
async importRaceResults(input: ImportRaceResultsInput): Promise<ImportRaceResultsSummaryViewModel> {
|
||||||
console.log('Importing race results:', input);
|
this.logger.debug('Importing race results:', input);
|
||||||
return {
|
const presenter = new ImportRaceResultsApiPresenter();
|
||||||
success: true,
|
await this.importRaceResultsApiUseCase.execute({ raceId: input.raceId, resultsFileContent: input.resultsFileContent }, presenter);
|
||||||
raceId: input.raceId,
|
return presenter.getViewModel()!;
|
||||||
driversProcessed: 10, // Mock data
|
|
||||||
resultsRecorded: 10, // Mock data
|
|
||||||
errors: [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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';
|
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,
|
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 { Injectable, Inject } from '@nestjs/common';
|
||||||
import { GetEntitySponsorshipPricingResultDto, SponsorDto, GetSponsorsOutput, CreateSponsorInput, CreateSponsorOutput, GetSponsorDashboardQueryParams, SponsorDashboardDTO, GetSponsorSponsorshipsQueryParams, SponsorshipDetailDTO, SponsorSponsorshipsDTO, SponsoredLeagueDTO, SponsorDashboardMetricsDTO, SponsorDashboardInvestmentDTO } from './dto/SponsorDto';
|
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()
|
@Injectable()
|
||||||
export class SponsorService {
|
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() {
|
async getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||||
// Seed some demo sponsors for dashboard if empty
|
this.logger.debug('[SponsorService] Fetching sponsorship pricing.');
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getEntitySponsorshipPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
const presenter = new GetSponsorshipPricingPresenter();
|
||||||
// This logic relies on external factors (e.g., pricing configuration, entity type)
|
await this.getSponsorshipPricingUseCase.execute(undefined, presenter);
|
||||||
// For now, return mock data
|
return presenter.viewModel;
|
||||||
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' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSponsors(): Promise<GetSponsorsOutput> {
|
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> {
|
async createSponsor(input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||||
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
this.logger.debug('[SponsorService] Creating sponsor.', { input });
|
||||||
const newSponsor: SponsorDto = {
|
|
||||||
id,
|
const presenter = new CreateSponsorPresenter();
|
||||||
name: input.name,
|
await this.createSponsorUseCase.execute(input, presenter);
|
||||||
contactEmail: input.contactEmail,
|
return presenter.viewModel;
|
||||||
websiteUrl: input.websiteUrl,
|
|
||||||
logoUrl: input.logoUrl,
|
|
||||||
createdAt: new Date(),
|
|
||||||
};
|
|
||||||
sponsors.set(id, newSponsor);
|
|
||||||
return { sponsor: newSponsor };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
async getSponsorDashboard(params: GetSponsorDashboardQueryParams): Promise<SponsorDashboardDTO | null> {
|
||||||
const { sponsorId } = params;
|
this.logger.debug('[SponsorService] Fetching sponsor dashboard.', { params });
|
||||||
|
|
||||||
const sponsor = sponsors.get(sponsorId);
|
const presenter = new GetSponsorDashboardPresenter();
|
||||||
if (!sponsor) {
|
await this.getSponsorDashboardUseCase.execute(params, presenter);
|
||||||
return null;
|
return presenter.viewModel as SponsorDashboardDTO | 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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
async getSponsorSponsorships(params: GetSponsorSponsorshipsQueryParams): Promise<SponsorSponsorshipsDTO | null> {
|
||||||
const { sponsorId } = params;
|
this.logger.debug('[SponsorService] Fetching sponsor sponsorships.', { params });
|
||||||
|
|
||||||
const sponsor = sponsors.get(sponsorId);
|
const presenter = new GetSponsorSponsorshipsPresenter();
|
||||||
if (!sponsor) {
|
await this.getSponsorSponsorshipsUseCase.execute(params, presenter);
|
||||||
return null;
|
return presenter.viewModel as SponsorSponsorshipsDTO | 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',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||||
import { ApiTags, ApiResponse, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
||||||
import { TeamService } from './TeamService';
|
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')
|
@ApiTags('teams')
|
||||||
@Controller('teams')
|
@Controller('teams')
|
||||||
@@ -15,5 +15,79 @@ export class TeamController {
|
|||||||
return this.teamService.getAllTeams();
|
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 core interfaces
|
||||||
import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
||||||
import { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
|
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 { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
|
|
||||||
// Import concrete in-memory implementations
|
// Import concrete in-memory implementations
|
||||||
import { InMemoryTeamRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
import { InMemoryTeamRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
||||||
import { InMemoryTeamMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
import { InMemoryTeamMembershipRepository } from 'adapters/racing/persistence/inmemory/InMemoryTeamMembershipRepository';
|
||||||
|
import { InMemoryDriverRepository } from 'adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
||||||
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
import { ConsoleLogger } from 'adapters/logging/ConsoleLogger';
|
||||||
|
|
||||||
// Import use cases
|
// Import use cases
|
||||||
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
|
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
|
||||||
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
|
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
|
// Tokens
|
||||||
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
|
export const TEAM_REPOSITORY_TOKEN = 'ITeamRepository';
|
||||||
export const TEAM_MEMBERSHIP_REPOSITORY_TOKEN = 'ITeamMembershipRepository';
|
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_ALL_USE_CASE_TOKEN = 'GetAllTeamsUseCase';
|
||||||
export const TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN = 'GetDriverTeamUseCase';
|
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';
|
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[] = [
|
export const TeamProviders: Provider[] = [
|
||||||
TeamService, // Provide the service itself
|
TeamService, // Provide the service itself
|
||||||
{
|
{
|
||||||
@@ -34,6 +74,15 @@ export const TeamProviders: Provider[] = [
|
|||||||
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
|
useFactory: (logger: Logger) => new InMemoryTeamMembershipRepository(logger),
|
||||||
inject: [TEAM_LOGGER_TOKEN],
|
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,
|
provide: TEAM_LOGGER_TOKEN,
|
||||||
useClass: ConsoleLogger,
|
useClass: ConsoleLogger,
|
||||||
@@ -48,7 +97,57 @@ export const TeamProviders: Provider[] = [
|
|||||||
{
|
{
|
||||||
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
|
provide: TEAM_GET_DRIVER_TEAM_USE_CASE_TOKEN,
|
||||||
useFactory: (teamRepo: ITeamRepository, membershipRepo: ITeamMembershipRepository, logger: Logger) =>
|
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],
|
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 { 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
|
// Use cases
|
||||||
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
|
import { GetAllTeamsUseCase } from '@gridpilot/racing/application/use-cases/GetAllTeamsUseCase';
|
||||||
import { GetDriverTeamUseCase } from '@gridpilot/racing/application/use-cases/GetDriverTeamUseCase';
|
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
|
// Presenters
|
||||||
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
|
import { AllTeamsPresenter } from './presenters/AllTeamsPresenter';
|
||||||
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
|
import { DriverTeamPresenter } from './presenters/DriverTeamPresenter';
|
||||||
|
import { TeamDetailsPresenter } from './presenters/TeamDetailsPresenter';
|
||||||
|
import { TeamMembersPresenter } from './presenters/TeamMembersPresenter';
|
||||||
|
import { TeamJoinRequestsPresenter } from './presenters/TeamJoinRequestsPresenter';
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
import { Logger } from '@gridpilot/shared/application/Logger';
|
import { Logger } from '@gridpilot/shared/application/Logger';
|
||||||
|
|
||||||
// Tokens
|
// 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()
|
@Injectable()
|
||||||
export class TeamService {
|
export class TeamService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(TEAM_GET_ALL_USE_CASE_TOKEN) private readonly getAllTeamsUseCase: GetAllTeamsUseCase,
|
@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_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,
|
@Inject(TEAM_LOGGER_TOKEN) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -28,7 +56,7 @@ export class TeamService {
|
|||||||
|
|
||||||
const presenter = new AllTeamsPresenter();
|
const presenter = new AllTeamsPresenter();
|
||||||
await this.getAllTeamsUseCase.execute(undefined, presenter);
|
await this.getAllTeamsUseCase.execute(undefined, presenter);
|
||||||
return presenter.viewModel;
|
return presenter.viewModel as unknown as AllTeamsViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
|
async getDriverTeam(query: GetDriverTeamQuery): Promise<DriverTeamViewModel | null> {
|
||||||
@@ -37,10 +65,104 @@ export class TeamService {
|
|||||||
const presenter = new DriverTeamPresenter();
|
const presenter = new DriverTeamPresenter();
|
||||||
try {
|
try {
|
||||||
await this.getDriverTeamUseCase.execute({ driverId: query.driverId }, presenter);
|
await this.getDriverTeamUseCase.execute({ driverId: query.driverId }, presenter);
|
||||||
return presenter.viewModel;
|
return presenter.viewModel as unknown as DriverTeamViewModel;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error fetching driver team: ${error}`);
|
this.logger.error(`Error fetching driver team: ${error}`);
|
||||||
return null;
|
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 { ApiProperty } from '@nestjs/swagger';
|
||||||
import { IsString, IsNotEmpty, IsEnum, IsBoolean, IsDate, IsOptional } from 'class-validator';
|
import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
export class TeamLeagueDto {
|
|
||||||
@ApiProperty()
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@ApiProperty({ nullable: true })
|
|
||||||
logoUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TeamListItemViewModel {
|
export class TeamListItemViewModel {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -19,17 +8,26 @@ export class TeamListItemViewModel {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ nullable: true })
|
@ApiProperty()
|
||||||
tag?: string;
|
tag: string;
|
||||||
|
|
||||||
@ApiProperty({ nullable: true })
|
@ApiProperty()
|
||||||
description?: string;
|
description: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
|
|
||||||
@ApiProperty({ type: [TeamLeagueDto] })
|
@ApiProperty({ type: [String] })
|
||||||
leagues: TeamLeagueDto[];
|
leagues: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
specialization?: 'endurance' | 'sprint' | 'mixed';
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
region?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: [String], required: false })
|
||||||
|
languages?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AllTeamsViewModel {
|
export class AllTeamsViewModel {
|
||||||
@@ -40,11 +38,169 @@ export class AllTeamsViewModel {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TeamDto {
|
export class TeamViewModel {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
|
||||||
id: string;
|
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()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@@ -63,60 +219,80 @@ export class TeamDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
|
|
||||||
@ApiProperty({ type: [TeamLeagueDto] })
|
|
||||||
leagues: TeamLeagueDto[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MembershipRole {
|
export class CreateTeamOutput {
|
||||||
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 {
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
teamId: string;
|
teamId: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsBoolean()
|
||||||
driverId: string;
|
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 { IAllTeamsPresenter, AllTeamsResultDTO, AllTeamsViewModel, TeamListItemViewModel } from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
|
||||||
import { TeamListItemViewModel } from '../dto/TeamDto';
|
|
||||||
|
|
||||||
export class AllTeamsPresenter implements IAllTeamsPresenter {
|
export class AllTeamsPresenter implements IAllTeamsPresenter {
|
||||||
private result: AllTeamsViewModel | null = null;
|
private result: AllTeamsViewModel | null = null;
|
||||||
@@ -24,6 +23,10 @@ export class AllTeamsPresenter implements IAllTeamsPresenter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getViewModel(): AllTeamsViewModel | null {
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
|
||||||
get viewModel(): AllTeamsViewModel {
|
get viewModel(): AllTeamsViewModel {
|
||||||
if (!this.result) throw new Error('Presenter not presented');
|
if (!this.result) throw new Error('Presenter not presented');
|
||||||
return this.result;
|
return this.result;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { IDriverTeamPresenter, DriverTeamResultDTO, DriverTeamViewModel } from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
|
import { IDriverTeamPresenter, DriverTeamResultDTO, DriverTeamViewModel } from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
|
||||||
import { TeamDto, MembershipDto, MembershipRole } from '../dto/TeamDto';
|
|
||||||
|
|
||||||
export class DriverTeamPresenter implements IDriverTeamPresenter {
|
export class DriverTeamPresenter implements IDriverTeamPresenter {
|
||||||
private result: DriverTeamViewModel | null = null;
|
private result: DriverTeamViewModel | null = null;
|
||||||
@@ -9,32 +8,32 @@ export class DriverTeamPresenter implements IDriverTeamPresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
present(dto: DriverTeamResultDTO) {
|
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 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 = {
|
this.result = {
|
||||||
team,
|
team: {
|
||||||
membership,
|
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,
|
isOwner,
|
||||||
canManage,
|
canManage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getViewModel(): DriverTeamViewModel | null {
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
|
||||||
get viewModel(): DriverTeamViewModel {
|
get viewModel(): DriverTeamViewModel {
|
||||||
if (!this.result) throw new Error('Presenter not presented');
|
if (!this.result) throw new Error('Presenter not presented');
|
||||||
return this.result;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,15 @@
|
|||||||
"@gridpilot/shared/application/*": [
|
"@gridpilot/shared/application/*": [
|
||||||
"../../core/shared/application/*"
|
"../../core/shared/application/*"
|
||||||
],
|
],
|
||||||
|
"@gridpilot/payments/*": [
|
||||||
|
"../../core/payments/*"
|
||||||
|
],
|
||||||
|
"@gridpilot/payments/application/*": [
|
||||||
|
"../../core/payments/application/*"
|
||||||
|
],
|
||||||
|
"@gridpilot/payments/domain/*": [
|
||||||
|
"../../core/payments/domain/*"
|
||||||
|
],
|
||||||
"@gridpilot/racing/*": [
|
"@gridpilot/racing/*": [
|
||||||
"../../core/racing/*"
|
"../../core/racing/*"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthService } from '../../../../lib/auth';
|
import { apiClient } from '../../../../lib/apiClient';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const returnTo = url.searchParams.get('returnTo') ?? undefined;
|
const returnTo = url.searchParams.get('returnTo') ?? undefined;
|
||||||
|
|
||||||
const authService = getAuthService();
|
const redirectUrl = apiClient.auth.getIracingAuthUrl(returnTo);
|
||||||
const { redirectUrl, state } = await authService.startIracingAuthRedirect(returnTo);
|
// For now, generate a simple state - in production this should be cryptographically secure
|
||||||
|
const state = Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
cookieStore.set('gp_demo_auth_state', state, {
|
cookieStore.set('gp_demo_auth_state', state, {
|
||||||
|
|||||||
@@ -1,11 +1,493 @@
|
|||||||
export class ApiClient {
|
/**
|
||||||
|
* Domain-specific API Client for GridPilot Website
|
||||||
|
*
|
||||||
|
* This module provides a strongly-typed HTTP client for all API operations.
|
||||||
|
* The website should use these methods instead of directly importing core use cases.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types - These mirror the API DTOs
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Common Types
|
||||||
|
export interface DriverDTO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
iracingId?: string;
|
||||||
|
rating?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProtestViewModel {
|
||||||
|
id: string;
|
||||||
|
raceId: string;
|
||||||
|
complainantId: string;
|
||||||
|
defendantId: string;
|
||||||
|
description: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueMemberViewModel {
|
||||||
|
driverId: string;
|
||||||
|
driver?: DriverDTO;
|
||||||
|
role: string;
|
||||||
|
joinedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StandingEntryViewModel {
|
||||||
|
driverId: string;
|
||||||
|
driver?: DriverDTO;
|
||||||
|
position: number;
|
||||||
|
points: number;
|
||||||
|
wins: number;
|
||||||
|
podiums: number;
|
||||||
|
races: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduledRaceViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
scheduledTime: string;
|
||||||
|
status: string;
|
||||||
|
trackName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// League Types
|
||||||
|
export interface LeagueSummaryViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
coverImage?: string;
|
||||||
|
memberCount: number;
|
||||||
|
maxMembers: number;
|
||||||
|
isPublic: boolean;
|
||||||
|
ownerId: string;
|
||||||
|
ownerName?: string;
|
||||||
|
scoringType?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllLeaguesWithCapacityViewModel {
|
||||||
|
leagues: LeagueSummaryViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueStatsDto {
|
||||||
|
totalLeagues: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueJoinRequestViewModel {
|
||||||
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
|
driverId: string;
|
||||||
|
requestedAt: Date;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueAdminPermissionsViewModel {
|
||||||
|
canManageMembers: boolean;
|
||||||
|
canManageRaces: boolean;
|
||||||
|
canManageSettings: boolean;
|
||||||
|
canManageProtests: boolean;
|
||||||
|
isOwner: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueOwnerSummaryViewModel {
|
||||||
|
leagueId: string;
|
||||||
|
leagueName: string;
|
||||||
|
memberCount: number;
|
||||||
|
pendingRequests: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueConfigFormModelDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
maxMembers: number;
|
||||||
|
// Add other config fields as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueAdminProtestsViewModel {
|
||||||
|
protests: ProtestViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueSeasonSummaryViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueMembershipsViewModel {
|
||||||
|
members: LeagueMemberViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueStandingsViewModel {
|
||||||
|
standings: StandingEntryViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueScheduleViewModel {
|
||||||
|
races: ScheduledRaceViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueStatsViewModel {
|
||||||
|
leagueId: string;
|
||||||
|
totalRaces: number;
|
||||||
|
completedRaces: number;
|
||||||
|
scheduledRaces: number;
|
||||||
|
averageSOF?: number;
|
||||||
|
highestSOF?: number;
|
||||||
|
lowestSOF?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueAdminViewModel {
|
||||||
|
config: LeagueConfigFormModelDto;
|
||||||
|
members: LeagueMemberViewModel[];
|
||||||
|
joinRequests: LeagueJoinRequestViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateLeagueInput {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
maxMembers: number;
|
||||||
|
ownerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateLeagueOutput {
|
||||||
|
leagueId: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Driver Types
|
||||||
|
export interface DriverLeaderboardItemViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
rating: number;
|
||||||
|
wins: number;
|
||||||
|
races: number;
|
||||||
|
skillLevel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriversLeaderboardViewModel {
|
||||||
|
drivers: DriverLeaderboardItemViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriverStatsDto {
|
||||||
|
totalDrivers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompleteOnboardingInput {
|
||||||
|
iracingId: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompleteOnboardingOutput {
|
||||||
|
driverId: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriverRegistrationStatusViewModel {
|
||||||
|
isRegistered: boolean;
|
||||||
|
raceId: string;
|
||||||
|
driverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Team Types
|
||||||
|
export interface TeamSummaryViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
memberCount: number;
|
||||||
|
rating: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllTeamsViewModel {
|
||||||
|
teams: TeamSummaryViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamMemberViewModel {
|
||||||
|
driverId: string;
|
||||||
|
driver?: DriverDTO;
|
||||||
|
role: string;
|
||||||
|
joinedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamJoinRequestItemViewModel {
|
||||||
|
id: string;
|
||||||
|
teamId: string;
|
||||||
|
driverId: string;
|
||||||
|
requestedAt: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamDetailsViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
memberCount: number;
|
||||||
|
ownerId: string;
|
||||||
|
members: TeamMemberViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamMembersViewModel {
|
||||||
|
members: TeamMemberViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamJoinRequestsViewModel {
|
||||||
|
requests: TeamJoinRequestItemViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriverTeamViewModel {
|
||||||
|
teamId: string;
|
||||||
|
teamName: string;
|
||||||
|
role: string;
|
||||||
|
joinedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTeamInput {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
ownerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTeamOutput {
|
||||||
|
teamId: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTeamInput {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTeamOutput {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Race Types
|
||||||
|
export interface RaceListItemViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
leagueId: string;
|
||||||
|
leagueName: string;
|
||||||
|
scheduledTime: string;
|
||||||
|
status: string;
|
||||||
|
trackName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllRacesPageViewModel {
|
||||||
|
races: RaceListItemViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RaceStatsDto {
|
||||||
|
totalRaces: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sponsor Types
|
||||||
|
export interface GetEntitySponsorshipPricingResultDto {
|
||||||
|
mainSlotPrice: number;
|
||||||
|
secondarySlotPrice: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SponsorViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
websiteUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSponsorsOutput {
|
||||||
|
sponsors: SponsorViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSponsorInput {
|
||||||
|
name: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
websiteUrl?: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSponsorOutput {
|
||||||
|
sponsorId: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SponsorDashboardDTO {
|
||||||
|
sponsorId: string;
|
||||||
|
sponsorName: string;
|
||||||
|
totalSponsorships: number;
|
||||||
|
activeSponsorships: number;
|
||||||
|
totalInvestment: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SponsorshipDetailViewModel {
|
||||||
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
|
leagueName: string;
|
||||||
|
seasonId: string;
|
||||||
|
tier: 'main' | 'secondary';
|
||||||
|
status: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SponsorSponsorshipsDTO {
|
||||||
|
sponsorId: string;
|
||||||
|
sponsorName: string;
|
||||||
|
sponsorships: SponsorshipDetailViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media Types
|
||||||
|
export interface RequestAvatarGenerationInput {
|
||||||
|
driverId: string;
|
||||||
|
style?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestAvatarGenerationOutput {
|
||||||
|
success: boolean;
|
||||||
|
avatarUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analytics Types
|
||||||
|
export interface RecordPageViewInput {
|
||||||
|
path: string;
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordPageViewOutput {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordEngagementInput {
|
||||||
|
eventType: string;
|
||||||
|
eventData?: Record<string, unknown>;
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordEngagementOutput {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth Types
|
||||||
|
export interface LoginParams {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignupParams {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionData {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
displayName?: string;
|
||||||
|
driverId?: string;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payments Types
|
||||||
|
export interface PaymentViewModel {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPaymentsOutput {
|
||||||
|
payments: PaymentViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePaymentInput {
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
leagueId: string;
|
||||||
|
driverId: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePaymentOutput {
|
||||||
|
paymentId: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MembershipFeeViewModel {
|
||||||
|
leagueId: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
period: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemberPaymentViewModel {
|
||||||
|
driverId: string;
|
||||||
|
amount: number;
|
||||||
|
paidAt: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetMembershipFeesOutput {
|
||||||
|
fees: MembershipFeeViewModel[];
|
||||||
|
memberPayments: MemberPaymentViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrizeViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
position?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPrizesOutput {
|
||||||
|
prizes: PrizeViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletTransactionViewModel {
|
||||||
|
id: string;
|
||||||
|
type: 'deposit' | 'withdrawal';
|
||||||
|
amount: number;
|
||||||
|
description?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletViewModel {
|
||||||
|
driverId: string;
|
||||||
|
balance: number;
|
||||||
|
currency: string;
|
||||||
|
transactions: WalletTransactionViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetWalletOutput {
|
||||||
|
wallet: WalletViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Base API Client
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class BaseApiClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
|
|
||||||
constructor(baseUrl: string) {
|
constructor(baseUrl: string) {
|
||||||
this.baseUrl = baseUrl;
|
this.baseUrl = baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async request<T>(method: string, path: string, data?: object): Promise<T | void> {
|
protected async request<T>(method: string, path: string, data?: object): Promise<T> {
|
||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
@@ -13,6 +495,7 @@ export class ApiClient {
|
|||||||
const config: RequestInit = {
|
const config: RequestInit = {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
|
credentials: 'include', // Include cookies for auth
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -22,41 +505,441 @@ export class ApiClient {
|
|||||||
const response = await fetch(`${this.baseUrl}${path}`, config);
|
const response = await fetch(`${this.baseUrl}${path}`, config);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Attempt to read error message from response body
|
let errorData: { message?: string } = { message: response.statusText };
|
||||||
let errorData: any;
|
|
||||||
try {
|
try {
|
||||||
errorData = await response.json();
|
errorData = await response.json();
|
||||||
} catch (e) {
|
} catch {
|
||||||
errorData = { message: response.statusText };
|
// Keep default error message
|
||||||
}
|
}
|
||||||
throw new Error(errorData.message || `API request failed with status ${response.status}`);
|
throw new Error(errorData.message || `API request failed with status ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
return text ? JSON.parse(text) : undefined;
|
if (!text) {
|
||||||
|
return null as T;
|
||||||
|
}
|
||||||
|
return JSON.parse(text) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T>(path: string): Promise<T | void> {
|
protected get<T>(path: string): Promise<T> {
|
||||||
return this.request<T>('GET', path);
|
return this.request<T>('GET', path);
|
||||||
}
|
}
|
||||||
|
|
||||||
post<T>(path: string, data: object): Promise<T | void> {
|
protected post<T>(path: string, data: object): Promise<T> {
|
||||||
return this.request<T>('POST', path, data);
|
return this.request<T>('POST', path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
put<T>(path: string, data: object): Promise<T | void> {
|
protected put<T>(path: string, data: object): Promise<T> {
|
||||||
return this.request<T>('PUT', path, data);
|
return this.request<T>('PUT', path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete<T>(path: string): Promise<T | void> {
|
protected delete<T>(path: string): Promise<T> {
|
||||||
return this.request<T>('DELETE', path);
|
return this.request<T>('DELETE', path);
|
||||||
}
|
}
|
||||||
|
|
||||||
patch<T>(path: string, data: object): Promise<T | void> {
|
protected patch<T>(path: string, data: object): Promise<T> {
|
||||||
return this.request<T>('PATCH', path, data);
|
return this.request<T>('PATCH', path, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate the API client with your backend's base URL
|
// ============================================================================
|
||||||
// You might want to get this from an environment variable
|
// Domain-Specific API Clients
|
||||||
export const api = new ApiClient(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001');
|
// ============================================================================
|
||||||
|
|
||||||
|
class LeaguesApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all leagues with capacity information */
|
||||||
|
getAllWithCapacity(): Promise<AllLeaguesWithCapacityViewModel> {
|
||||||
|
return this.get<AllLeaguesWithCapacityViewModel>('/leagues/all-with-capacity');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get total number of leagues */
|
||||||
|
getTotal(): Promise<LeagueStatsDto> {
|
||||||
|
return this.get<LeagueStatsDto>('/leagues/total-leagues');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league standings */
|
||||||
|
getStandings(leagueId: string): Promise<LeagueStandingsViewModel> {
|
||||||
|
return this.get<LeagueStandingsViewModel>(`/leagues/${leagueId}/standings`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league schedule */
|
||||||
|
getSchedule(leagueId: string): Promise<LeagueScheduleViewModel> {
|
||||||
|
return this.get<LeagueScheduleViewModel>(`/leagues/${leagueId}/schedule`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league stats */
|
||||||
|
getStats(leagueId: string): Promise<LeagueStatsViewModel> {
|
||||||
|
return this.get<LeagueStatsViewModel>(`/leagues/${leagueId}/stats`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league memberships */
|
||||||
|
getMemberships(leagueId: string): Promise<LeagueMembershipsViewModel> {
|
||||||
|
return this.get<LeagueMembershipsViewModel>(`/leagues/${leagueId}/memberships`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league join requests */
|
||||||
|
getJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||||
|
return this.get<LeagueJoinRequestViewModel[]>(`/leagues/${leagueId}/join-requests`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Approve a join request */
|
||||||
|
approveJoinRequest(leagueId: string, requestId: string): Promise<{ success: boolean }> {
|
||||||
|
return this.post<{ success: boolean }>(`/leagues/${leagueId}/join-requests/approve`, { requestId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reject a join request */
|
||||||
|
rejectJoinRequest(leagueId: string, requestId: string, reason?: string): Promise<{ success: boolean }> {
|
||||||
|
return this.post<{ success: boolean }>(`/leagues/${leagueId}/join-requests/reject`, { requestId, reason });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league admin permissions */
|
||||||
|
getAdminPermissions(leagueId: string, performerDriverId: string): Promise<LeagueAdminPermissionsViewModel> {
|
||||||
|
return this.get<LeagueAdminPermissionsViewModel>(`/leagues/${leagueId}/permissions/${performerDriverId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league owner summary */
|
||||||
|
getOwnerSummary(leagueId: string, ownerId: string): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||||
|
return this.get<LeagueOwnerSummaryViewModel | null>(`/leagues/${leagueId}/owner-summary/${ownerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league full config */
|
||||||
|
getConfig(leagueId: string): Promise<LeagueConfigFormModelDto | null> {
|
||||||
|
return this.get<LeagueConfigFormModelDto | null>(`/leagues/${leagueId}/config`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league protests */
|
||||||
|
getProtests(leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
||||||
|
return this.get<LeagueAdminProtestsViewModel>(`/leagues/${leagueId}/protests`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league seasons */
|
||||||
|
getSeasons(leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||||
|
return this.get<LeagueSeasonSummaryViewModel[]>(`/leagues/${leagueId}/seasons`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get league admin data */
|
||||||
|
getAdmin(leagueId: string): Promise<LeagueAdminViewModel> {
|
||||||
|
return this.get<LeagueAdminViewModel>(`/leagues/${leagueId}/admin`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new league */
|
||||||
|
create(input: CreateLeagueInput): Promise<CreateLeagueOutput> {
|
||||||
|
return this.post<CreateLeagueOutput>('/leagues', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a member from league */
|
||||||
|
removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
|
||||||
|
return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/remove`, { performerDriverId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update member role */
|
||||||
|
updateMemberRole(leagueId: string, performerDriverId: string, targetDriverId: string, newRole: string): Promise<{ success: boolean }> {
|
||||||
|
return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/role`, { performerDriverId, newRole });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriversApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get drivers leaderboard */
|
||||||
|
getLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||||
|
return this.get<DriversLeaderboardViewModel>('/drivers/leaderboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get total number of drivers */
|
||||||
|
getTotal(): Promise<DriverStatsDto> {
|
||||||
|
return this.get<DriverStatsDto>('/drivers/total-drivers');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get current driver (based on session) */
|
||||||
|
getCurrent(): Promise<DriverDTO | null> {
|
||||||
|
return this.get<DriverDTO | null>('/drivers/current');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Complete driver onboarding */
|
||||||
|
completeOnboarding(input: CompleteOnboardingInput): Promise<CompleteOnboardingOutput> {
|
||||||
|
return this.post<CompleteOnboardingOutput>('/drivers/complete-onboarding', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get driver registration status for a race */
|
||||||
|
getRegistrationStatus(driverId: string, raceId: string): Promise<DriverRegistrationStatusViewModel> {
|
||||||
|
return this.get<DriverRegistrationStatusViewModel>(`/drivers/${driverId}/races/${raceId}/registration-status`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TeamsApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all teams */
|
||||||
|
getAll(): Promise<AllTeamsViewModel> {
|
||||||
|
return this.get<AllTeamsViewModel>('/teams/all');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get team details */
|
||||||
|
getDetails(teamId: string): Promise<TeamDetailsViewModel | null> {
|
||||||
|
return this.get<TeamDetailsViewModel | null>(`/teams/${teamId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get team members */
|
||||||
|
getMembers(teamId: string): Promise<TeamMembersViewModel> {
|
||||||
|
return this.get<TeamMembersViewModel>(`/teams/${teamId}/members`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get team join requests */
|
||||||
|
getJoinRequests(teamId: string): Promise<TeamJoinRequestsViewModel> {
|
||||||
|
return this.get<TeamJoinRequestsViewModel>(`/teams/${teamId}/join-requests`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Approve a join request */
|
||||||
|
approveJoinRequest(teamId: string, requestId: string): Promise<{ success: boolean }> {
|
||||||
|
return this.post<{ success: boolean }>(`/teams/${teamId}/join-requests/approve`, { requestId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reject a join request */
|
||||||
|
rejectJoinRequest(teamId: string, requestId: string, reason?: string): Promise<{ success: boolean }> {
|
||||||
|
return this.post<{ success: boolean }>(`/teams/${teamId}/join-requests/reject`, { requestId, reason });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new team */
|
||||||
|
create(input: CreateTeamInput): Promise<CreateTeamOutput> {
|
||||||
|
return this.post<CreateTeamOutput>('/teams', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update team */
|
||||||
|
update(teamId: string, input: UpdateTeamInput): Promise<UpdateTeamOutput> {
|
||||||
|
return this.patch<UpdateTeamOutput>(`/teams/${teamId}`, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get driver's team */
|
||||||
|
getDriverTeam(driverId: string): Promise<DriverTeamViewModel | null> {
|
||||||
|
return this.get<DriverTeamViewModel | null>(`/teams/driver/${driverId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RacesApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all races */
|
||||||
|
getAll(): Promise<AllRacesPageViewModel> {
|
||||||
|
return this.get<AllRacesPageViewModel>('/races/all');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get total number of races */
|
||||||
|
getTotal(): Promise<RaceStatsDto> {
|
||||||
|
return this.get<RaceStatsDto>('/races/total-races');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SponsorsApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get sponsorship pricing */
|
||||||
|
getPricing(): Promise<GetEntitySponsorshipPricingResultDto> {
|
||||||
|
return this.get<GetEntitySponsorshipPricingResultDto>('/sponsors/pricing');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get all sponsors */
|
||||||
|
getAll(): Promise<GetSponsorsOutput> {
|
||||||
|
return this.get<GetSponsorsOutput>('/sponsors');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new sponsor */
|
||||||
|
create(input: CreateSponsorInput): Promise<CreateSponsorOutput> {
|
||||||
|
return this.post<CreateSponsorOutput>('/sponsors', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get sponsor dashboard */
|
||||||
|
getDashboard(sponsorId: string): Promise<SponsorDashboardDTO | null> {
|
||||||
|
return this.get<SponsorDashboardDTO | null>(`/sponsors/dashboard/${sponsorId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get sponsor sponsorships */
|
||||||
|
getSponsorships(sponsorId: string): Promise<SponsorSponsorshipsDTO | null> {
|
||||||
|
return this.get<SponsorSponsorshipsDTO | null>(`/sponsors/${sponsorId}/sponsorships`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Request avatar generation */
|
||||||
|
requestAvatarGeneration(input: RequestAvatarGenerationInput): Promise<RequestAvatarGenerationOutput> {
|
||||||
|
return this.post<RequestAvatarGenerationOutput>('/media/avatar/generate', input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnalyticsApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Record a page view */
|
||||||
|
recordPageView(input: RecordPageViewInput): Promise<RecordPageViewOutput> {
|
||||||
|
return this.post<RecordPageViewOutput>('/analytics/page-view', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Record an engagement event */
|
||||||
|
recordEngagement(input: RecordEngagementInput): Promise<RecordEngagementOutput> {
|
||||||
|
return this.post<RecordEngagementOutput>('/analytics/engagement', input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sign up with email */
|
||||||
|
signup(params: SignupParams): Promise<SessionData> {
|
||||||
|
return this.post<SessionData>('/auth/signup', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Login with email */
|
||||||
|
login(params: LoginParams): Promise<SessionData> {
|
||||||
|
return this.post<SessionData>('/auth/login', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get current session */
|
||||||
|
getSession(): Promise<SessionData | null> {
|
||||||
|
return this.get<SessionData | null>('/auth/session');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Logout */
|
||||||
|
logout(): Promise<void> {
|
||||||
|
return this.post<void>('/auth/logout', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Start iRacing auth redirect */
|
||||||
|
getIracingAuthUrl(returnTo?: string): string {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||||
|
const params = returnTo ? `?returnTo=${encodeURIComponent(returnTo)}` : '';
|
||||||
|
return `${baseUrl}/auth/iracing/start${params}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentsApiClient extends BaseApiClient {
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get payments */
|
||||||
|
getPayments(leagueId?: string, driverId?: string): Promise<GetPaymentsOutput> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (leagueId) params.append('leagueId', leagueId);
|
||||||
|
if (driverId) params.append('driverId', driverId);
|
||||||
|
const query = params.toString();
|
||||||
|
return this.get<GetPaymentsOutput>(`/payments${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a payment */
|
||||||
|
createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
|
||||||
|
return this.post<CreatePaymentOutput>('/payments', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update payment status */
|
||||||
|
updatePaymentStatus(paymentId: string, status: string): Promise<{ success: boolean }> {
|
||||||
|
return this.patch<{ success: boolean }>('/payments/status', { paymentId, status });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get membership fees */
|
||||||
|
getMembershipFees(leagueId: string): Promise<GetMembershipFeesOutput> {
|
||||||
|
return this.get<GetMembershipFeesOutput>(`/payments/membership-fees?leagueId=${leagueId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upsert membership fee */
|
||||||
|
upsertMembershipFee(leagueId: string, amount: number, currency: string, period: string): Promise<{ success: boolean }> {
|
||||||
|
return this.post<{ success: boolean }>('/payments/membership-fees', { leagueId, amount, currency, period });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update member payment */
|
||||||
|
updateMemberPayment(leagueId: string, driverId: string, amount: number, paidAt: string): Promise<{ success: boolean }> {
|
||||||
|
return this.patch<{ success: boolean }>('/payments/membership-fees/member-payment', { leagueId, driverId, amount, paidAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get prizes */
|
||||||
|
getPrizes(leagueId?: string, seasonId?: string): Promise<GetPrizesOutput> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (leagueId) params.append('leagueId', leagueId);
|
||||||
|
if (seasonId) params.append('seasonId', seasonId);
|
||||||
|
const query = params.toString();
|
||||||
|
return this.get<GetPrizesOutput>(`/payments/prizes${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a prize */
|
||||||
|
createPrize(name: string, amount: number, currency: string, leagueId: string, position?: number): Promise<{ prizeId: string; success: boolean }> {
|
||||||
|
return this.post<{ prizeId: string; success: boolean }>('/payments/prizes', { name, amount, currency, leagueId, position });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Award a prize */
|
||||||
|
awardPrize(prizeId: string, driverId: string): Promise<{ success: boolean }> {
|
||||||
|
return this.patch<{ success: boolean }>('/payments/prizes/award', { prizeId, driverId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete a prize */
|
||||||
|
deletePrize(prizeId: string): Promise<{ success: boolean }> {
|
||||||
|
return this.delete<{ success: boolean }>(`/payments/prizes?prizeId=${prizeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get wallet */
|
||||||
|
getWallet(driverId: string): Promise<GetWalletOutput> {
|
||||||
|
return this.get<GetWalletOutput>(`/payments/wallets?driverId=${driverId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Process wallet transaction */
|
||||||
|
processWalletTransaction(driverId: string, type: 'deposit' | 'withdrawal', amount: number, description?: string): Promise<{ success: boolean }> {
|
||||||
|
return this.post<{ success: boolean }>('/payments/wallets/transactions', { driverId, type, amount, description });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Main API Client with Domain Namespaces
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
public readonly leagues: LeaguesApiClient;
|
||||||
|
public readonly drivers: DriversApiClient;
|
||||||
|
public readonly teams: TeamsApiClient;
|
||||||
|
public readonly races: RacesApiClient;
|
||||||
|
public readonly sponsors: SponsorsApiClient;
|
||||||
|
public readonly media: MediaApiClient;
|
||||||
|
public readonly analytics: AnalyticsApiClient;
|
||||||
|
public readonly auth: AuthApiClient;
|
||||||
|
public readonly payments: PaymentsApiClient;
|
||||||
|
|
||||||
|
constructor(baseUrl: string) {
|
||||||
|
this.leagues = new LeaguesApiClient(baseUrl);
|
||||||
|
this.drivers = new DriversApiClient(baseUrl);
|
||||||
|
this.teams = new TeamsApiClient(baseUrl);
|
||||||
|
this.races = new RacesApiClient(baseUrl);
|
||||||
|
this.sponsors = new SponsorsApiClient(baseUrl);
|
||||||
|
this.media = new MediaApiClient(baseUrl);
|
||||||
|
this.analytics = new AnalyticsApiClient(baseUrl);
|
||||||
|
this.auth = new AuthApiClient(baseUrl);
|
||||||
|
this.payments = new PaymentsApiClient(baseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Singleton Instance
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
|
export const apiClient = new ApiClient(API_BASE_URL);
|
||||||
|
|
||||||
|
// Default export for convenience
|
||||||
|
export default apiClient;
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { LeagueModule } from './modules/league/LeagueModule';
|
|
||||||
import { DriverModule } from './modules/driver/DriverModule';
|
|
||||||
import { TeamModule } from './modules/team/TeamModule';
|
|
||||||
import { RaceModule } from './modules/race/RaceModule';
|
|
||||||
import { SponsorModule } from './modules/sponsor/SponsorModule';
|
|
||||||
import { AuthModule } from './modules/auth/AuthModule';
|
|
||||||
import { MediaModule } from './modules/media/MediaModule';
|
|
||||||
import { AnalyticsModule } from './modules/analytics/AnalyticsModule';
|
|
||||||
import { LoggingModule } from './modules/logging/LoggingModule';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
LoggingModule,
|
|
||||||
LeagueModule,
|
|
||||||
DriverModule,
|
|
||||||
TeamModule,
|
|
||||||
RaceModule,
|
|
||||||
SponsorModule,
|
|
||||||
AuthModule,
|
|
||||||
MediaModule,
|
|
||||||
AnalyticsModule,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
||||||
@@ -6,7 +6,7 @@ import { useAuth } from '@/lib/auth/AuthContext';
|
|||||||
* Returns the effective driver ID for the current session.
|
* Returns the effective driver ID for the current session.
|
||||||
*
|
*
|
||||||
* Prefers the authenticated user's primaryDriverId when available,
|
* Prefers the authenticated user's primaryDriverId when available,
|
||||||
* otherwise falls back to the demo default used across the alpha site.
|
* otherwise returns an empty string (user must log in to have a driver).
|
||||||
*/
|
*/
|
||||||
export function useEffectiveDriverId(): string {
|
export function useEffectiveDriverId(): string {
|
||||||
const { session } = useAuth();
|
const { session } = useAuth();
|
||||||
@@ -16,36 +16,11 @@ export function useEffectiveDriverId(): string {
|
|||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
// In alpha mode, if the user has no bound driver yet, fall back to the
|
// Return the user's primary driver ID if available
|
||||||
// first seeded driver from the in-memory repository instead of a hardcoded ID.
|
|
||||||
if (user?.primaryDriverId) {
|
if (user?.primaryDriverId) {
|
||||||
return user.primaryDriverId;
|
return user.primaryDriverId;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// No driver ID available - user needs to log in or complete onboarding
|
||||||
// Lazy-load to avoid importing DI facade at module evaluation time
|
|
||||||
const { getDriverRepository } =
|
|
||||||
require('./di-container') as typeof import('./di-container');
|
|
||||||
const repo = getDriverRepository();
|
|
||||||
|
|
||||||
interface DriverRepositoryWithSyncFindAll {
|
|
||||||
findAllSync?: () => Array<{ id: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In alpha/demo mode the in-memory repository exposes a synchronous finder;
|
|
||||||
// access it via a safe dynamic lookup to keep typing compatible with the port.
|
|
||||||
const repoWithSync = repo as DriverRepositoryWithSyncFindAll;
|
|
||||||
const allDrivers = repoWithSync.findAllSync?.();
|
|
||||||
if (Array.isArray(allDrivers) && allDrivers.length > 0) {
|
|
||||||
const firstDriver = allDrivers[0];
|
|
||||||
if (firstDriver) {
|
|
||||||
return firstDriver.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore and fall back to legacy default below
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy fallback: preserved only as a last resort for demo
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
|
||||||
import { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import { AppModule } from './app.module';
|
|
||||||
|
|
||||||
let appContext: INestApplicationContext | null = null;
|
|
||||||
|
|
||||||
export async function initializeDIContainer(): Promise<void> {
|
|
||||||
if (appContext) {
|
|
||||||
return; // Already initialized
|
|
||||||
}
|
|
||||||
|
|
||||||
appContext = await NestFactory.createApplicationContext(AppModule);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDIContainer(): INestApplicationContext {
|
|
||||||
if (!appContext) {
|
|
||||||
throw new Error('DI container not initialized. Call initializeDIContainer() first.');
|
|
||||||
}
|
|
||||||
return appContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getService<T>(token: string | symbol): Promise<T> {
|
|
||||||
const container = getDIContainer();
|
|
||||||
return container.get<T>(token);
|
|
||||||
}
|
|
||||||
@@ -1,81 +1,82 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type {
|
import { apiClient } from '@/lib/apiClient';
|
||||||
LeagueMembership as DomainLeagueMembership,
|
|
||||||
MembershipRole,
|
|
||||||
MembershipStatus,
|
|
||||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight league membership model mirroring the domain type but with
|
* Membership role types - these are defined locally to avoid core dependencies
|
||||||
* a stringified joinedAt for easier UI formatting.
|
|
||||||
*/
|
*/
|
||||||
export interface LeagueMembership extends Omit<DomainLeagueMembership, 'joinedAt'> {
|
export type MembershipRole = 'owner' | 'admin' | 'steward' | 'member';
|
||||||
|
export type MembershipStatus = 'active' | 'inactive' | 'pending';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight league membership model for UI.
|
||||||
|
*/
|
||||||
|
export interface LeagueMembership {
|
||||||
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
|
driverId: string;
|
||||||
|
role: MembershipRole;
|
||||||
|
status: MembershipStatus;
|
||||||
joinedAt: string;
|
joinedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In-memory cache for memberships (populated via API calls)
|
||||||
const leagueMemberships = new Map<string, LeagueMembership[]>();
|
const leagueMemberships = new Map<string, LeagueMembership[]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize league memberships once from the in-memory league membership repository
|
* Get a specific membership from cache.
|
||||||
* that is seeded via the static racing seed in the DI container.
|
|
||||||
*
|
|
||||||
* This avoids depending on raw testing-support seed exports and keeps all demo
|
|
||||||
* membership data flowing through the same in-memory repositories used elsewhere.
|
|
||||||
*/
|
*/
|
||||||
(async function initializeLeagueMembershipsFromRepository() {
|
|
||||||
if (leagueMemberships.size > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { getLeagueRepository, getLeagueMembershipRepository } = await import('./di-container');
|
|
||||||
const leagueRepo = getLeagueRepository();
|
|
||||||
const membershipRepo = getLeagueMembershipRepository();
|
|
||||||
|
|
||||||
const allLeagues = await leagueRepo.findAll();
|
|
||||||
const byLeague = new Map<string, LeagueMembership[]>();
|
|
||||||
|
|
||||||
for (const league of allLeagues) {
|
|
||||||
const memberships = await membershipRepo.getLeagueMembers(league.id);
|
|
||||||
|
|
||||||
const mapped: LeagueMembership[] = memberships.map((membership) => ({
|
|
||||||
id: membership.id,
|
|
||||||
leagueId: membership.leagueId,
|
|
||||||
driverId: membership.driverId,
|
|
||||||
role: membership.role,
|
|
||||||
status: membership.status,
|
|
||||||
joinedAt:
|
|
||||||
membership.joinedAt instanceof Date
|
|
||||||
? membership.joinedAt.toISOString()
|
|
||||||
: new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
byLeague.set(league.id, mapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [leagueId, list] of byLeague.entries()) {
|
|
||||||
leagueMemberships.set(leagueId, list);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// In alpha/demo mode we tolerate failures here; callers will see empty memberships.
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Failed to initialize league memberships from repository', error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
export function getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
export function getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
||||||
const list = leagueMemberships.get(leagueId);
|
const list = leagueMemberships.get(leagueId);
|
||||||
if (!list) return null;
|
if (!list) return null;
|
||||||
return list.find((m) => m.driverId === driverId) ?? null;
|
return list.find((m) => m.driverId === driverId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all members of a league from cache.
|
||||||
|
*/
|
||||||
export function getLeagueMembers(leagueId: string): LeagueMembership[] {
|
export function getLeagueMembers(leagueId: string): LeagueMembership[] {
|
||||||
return [...(leagueMemberships.get(leagueId) ?? [])];
|
return [...(leagueMemberships.get(leagueId) ?? [])];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive a driver's primary league from in-memory league memberships.
|
* Fetch and cache memberships for a league via API.
|
||||||
|
*/
|
||||||
|
export async function fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
|
||||||
|
try {
|
||||||
|
const result = await apiClient.leagues.getMemberships(leagueId);
|
||||||
|
const memberships: LeagueMembership[] = result.members.map(member => ({
|
||||||
|
id: `${member.driverId}-${leagueId}`, // Generate ID since API doesn't provide it
|
||||||
|
leagueId,
|
||||||
|
driverId: member.driverId,
|
||||||
|
role: member.role as MembershipRole,
|
||||||
|
status: 'active' as MembershipStatus, // Assume active since API returns current members
|
||||||
|
joinedAt: member.joinedAt,
|
||||||
|
}));
|
||||||
|
setLeagueMemberships(leagueId, memberships);
|
||||||
|
return memberships;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch league memberships:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set memberships in cache (for use after API calls).
|
||||||
|
*/
|
||||||
|
export function setLeagueMemberships(leagueId: string, memberships: LeagueMembership[]): void {
|
||||||
|
leagueMemberships.set(leagueId, memberships);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cached memberships for a league.
|
||||||
|
*/
|
||||||
|
export function clearLeagueMemberships(leagueId: string): void {
|
||||||
|
leagueMemberships.delete(leagueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a driver's primary league from cached memberships.
|
||||||
* Prefers any active membership and returns the first matching league.
|
* Prefers any active membership and returns the first matching league.
|
||||||
*/
|
*/
|
||||||
export function getPrimaryLeagueIdForDriver(driverId: string): string | null {
|
export function getPrimaryLeagueIdForDriver(driverId: string): string | null {
|
||||||
@@ -87,10 +88,11 @@ export function getPrimaryLeagueIdForDriver(driverId: string): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a driver is owner or admin of a league.
|
||||||
|
*/
|
||||||
export function isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
|
export function isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
|
||||||
const membership = getMembership(leagueId, driverId);
|
const membership = getMembership(leagueId, driverId);
|
||||||
if (!membership) return false;
|
if (!membership) return false;
|
||||||
return membership.role === 'owner' || membership.role === 'admin';
|
return membership.role === 'owner' || membership.role === 'admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { MembershipRole, MembershipStatus };
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { MembershipRole } from '@gridpilot/racing/domain/entities/LeagueMembership';
|
/**
|
||||||
|
* League role types - defined locally to avoid core dependencies
|
||||||
|
*/
|
||||||
|
export type MembershipRole = 'owner' | 'admin' | 'steward' | 'member';
|
||||||
export type LeagueRole = MembershipRole;
|
export type LeagueRole = MembershipRole;
|
||||||
|
|
||||||
export function isLeagueOwnerRole(role: LeagueRole): boolean {
|
export function isLeagueOwnerRole(role: LeagueRole): boolean {
|
||||||
|
|||||||
@@ -1,20 +1,64 @@
|
|||||||
import type {
|
/**
|
||||||
LeagueConfigFormModel,
|
* League Wizard Service - Refactored to use API client
|
||||||
} from '@gridpilot/racing/application';
|
*
|
||||||
import type {
|
* This service handles league creation wizard logic without direct core dependencies.
|
||||||
CreateLeagueWithSeasonAndScoringCommand,
|
*/
|
||||||
CreateLeagueWithSeasonAndScoringResultDTO,
|
|
||||||
} from '@gridpilot/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
import { apiClient } from '@/lib/apiClient';
|
||||||
import {
|
|
||||||
getDriverRepository,
|
|
||||||
getCreateLeagueWithSeasonAndScoringUseCase,
|
|
||||||
} from '@/lib/di-container';
|
|
||||||
import { LeagueName } from '@gridpilot/racing/domain/value-objects/LeagueName';
|
|
||||||
import { LeagueDescription } from '@gridpilot/racing/domain/value-objects/LeagueDescription';
|
|
||||||
import { GameConstraints } from '@gridpilot/racing/domain/value-objects/GameConstraints';
|
|
||||||
|
|
||||||
export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||||
|
|
||||||
|
export interface LeagueConfigFormModel {
|
||||||
|
leagueId?: string;
|
||||||
|
basics: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
visibility: 'public' | 'private' | 'unlisted';
|
||||||
|
gameId: string;
|
||||||
|
};
|
||||||
|
structure: {
|
||||||
|
mode: 'solo' | 'fixedTeams';
|
||||||
|
maxDrivers?: number;
|
||||||
|
maxTeams?: number;
|
||||||
|
driversPerTeam?: number;
|
||||||
|
};
|
||||||
|
championships: {
|
||||||
|
enableDriverChampionship: boolean;
|
||||||
|
enableTeamChampionship: boolean;
|
||||||
|
enableNationsChampionship: boolean;
|
||||||
|
enableTrophyChampionship: boolean;
|
||||||
|
};
|
||||||
|
scoring: {
|
||||||
|
patternId?: string;
|
||||||
|
customScoringEnabled?: boolean;
|
||||||
|
};
|
||||||
|
dropPolicy: {
|
||||||
|
strategy: 'none' | 'bestNResults' | 'dropWorstN';
|
||||||
|
n?: number;
|
||||||
|
};
|
||||||
|
timings: {
|
||||||
|
practiceMinutes?: number;
|
||||||
|
qualifyingMinutes?: number;
|
||||||
|
sprintRaceMinutes?: number;
|
||||||
|
mainRaceMinutes?: number;
|
||||||
|
sessionCount?: number;
|
||||||
|
roundsPlanned?: number;
|
||||||
|
raceDayOfWeek?: number;
|
||||||
|
raceTimeUtc?: string;
|
||||||
|
};
|
||||||
|
stewarding: {
|
||||||
|
decisionMode: 'owner_only' | 'admin_vote' | 'steward_panel';
|
||||||
|
requiredVotes?: number;
|
||||||
|
requireDefense: boolean;
|
||||||
|
defenseTimeLimit: number;
|
||||||
|
voteTimeLimit: number;
|
||||||
|
protestDeadlineHours: number;
|
||||||
|
stewardingClosesHours: number;
|
||||||
|
notifyAccusedOnProtest: boolean;
|
||||||
|
notifyOnVoteRequired: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface WizardErrors {
|
export interface WizardErrors {
|
||||||
basics?: {
|
basics?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -51,16 +95,18 @@ export function validateLeagueWizardStep(
|
|||||||
if (step === 1) {
|
if (step === 1) {
|
||||||
const basicsErrors: NonNullable<WizardErrors['basics']> = {};
|
const basicsErrors: NonNullable<WizardErrors['basics']> = {};
|
||||||
|
|
||||||
// Use LeagueName value object for validation
|
// Basic name validation
|
||||||
const nameValidation = LeagueName.validate(form.basics.name);
|
if (!form.basics.name || form.basics.name.trim().length === 0) {
|
||||||
if (!nameValidation.valid && nameValidation.error) {
|
basicsErrors.name = 'League name is required';
|
||||||
basicsErrors.name = nameValidation.error;
|
} else if (form.basics.name.length < 3) {
|
||||||
|
basicsErrors.name = 'League name must be at least 3 characters';
|
||||||
|
} else if (form.basics.name.length > 100) {
|
||||||
|
basicsErrors.name = 'League name must be less than 100 characters';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use LeagueDescription value object for validation
|
// Description validation
|
||||||
const descValidation = LeagueDescription.validate(form.basics.description ?? '');
|
if (form.basics.description && form.basics.description.length > 500) {
|
||||||
if (!descValidation.valid && descValidation.error) {
|
basicsErrors.description = 'Description must be less than 500 characters';
|
||||||
basicsErrors.description = descValidation.error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(basicsErrors).length > 0) {
|
if (Object.keys(basicsErrors).length > 0) {
|
||||||
@@ -84,47 +130,23 @@ export function validateLeagueWizardStep(
|
|||||||
// Step 3: Structure (solo vs teams)
|
// Step 3: Structure (solo vs teams)
|
||||||
if (step === 3) {
|
if (step === 3) {
|
||||||
const structureErrors: NonNullable<WizardErrors['structure']> = {};
|
const structureErrors: NonNullable<WizardErrors['structure']> = {};
|
||||||
const gameConstraints = GameConstraints.forGame(form.basics.gameId);
|
|
||||||
|
|
||||||
if (form.structure.mode === 'solo') {
|
if (form.structure.mode === 'solo') {
|
||||||
if (!form.structure.maxDrivers || form.structure.maxDrivers <= 0) {
|
if (!form.structure.maxDrivers || form.structure.maxDrivers <= 0) {
|
||||||
structureErrors.maxDrivers =
|
structureErrors.maxDrivers =
|
||||||
'Max drivers must be greater than 0 for solo leagues';
|
'Max drivers must be greater than 0 for solo leagues';
|
||||||
} else {
|
} else if (form.structure.maxDrivers > 100) {
|
||||||
// Validate against game constraints
|
structureErrors.maxDrivers = 'Max drivers cannot exceed 100';
|
||||||
const driverValidation = gameConstraints.validateDriverCount(
|
|
||||||
form.structure.maxDrivers,
|
|
||||||
);
|
|
||||||
if (!driverValidation.valid && driverValidation.error) {
|
|
||||||
structureErrors.maxDrivers = driverValidation.error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (form.structure.mode === 'fixedTeams') {
|
} else if (form.structure.mode === 'fixedTeams') {
|
||||||
if (!form.structure.maxTeams || form.structure.maxTeams <= 0) {
|
if (!form.structure.maxTeams || form.structure.maxTeams <= 0) {
|
||||||
structureErrors.maxTeams =
|
structureErrors.maxTeams =
|
||||||
'Max teams must be greater than 0 for team leagues';
|
'Max teams must be greater than 0 for team leagues';
|
||||||
} else {
|
|
||||||
// Validate against game constraints
|
|
||||||
const teamValidation = gameConstraints.validateTeamCount(
|
|
||||||
form.structure.maxTeams,
|
|
||||||
);
|
|
||||||
if (!teamValidation.valid && teamValidation.error) {
|
|
||||||
structureErrors.maxTeams = teamValidation.error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!form.structure.driversPerTeam || form.structure.driversPerTeam <= 0) {
|
if (!form.structure.driversPerTeam || form.structure.driversPerTeam <= 0) {
|
||||||
structureErrors.driversPerTeam =
|
structureErrors.driversPerTeam =
|
||||||
'Drivers per team must be greater than 0';
|
'Drivers per team must be greater than 0';
|
||||||
}
|
}
|
||||||
// Validate total driver count
|
|
||||||
if (form.structure.maxDrivers) {
|
|
||||||
const driverValidation = gameConstraints.validateDriverCount(
|
|
||||||
form.structure.maxDrivers,
|
|
||||||
);
|
|
||||||
if (!driverValidation.valid && driverValidation.error) {
|
|
||||||
structureErrors.maxDrivers = driverValidation.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (Object.keys(structureErrors).length > 0) {
|
if (Object.keys(structureErrors).length > 0) {
|
||||||
errors.structure = structureErrors;
|
errors.structure = structureErrors;
|
||||||
@@ -210,24 +232,27 @@ export function hasWizardErrors(errors: WizardErrors): boolean {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateLeagueResult {
|
||||||
|
leagueId: string;
|
||||||
|
seasonId?: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pure mapping from LeagueConfigFormModel to the creation command.
|
* Create a league via API.
|
||||||
* Driver ownership is handled by the caller.
|
|
||||||
*/
|
*/
|
||||||
export function buildCreateLeagueCommandFromConfig(
|
export async function createLeagueFromConfig(
|
||||||
form: LeagueConfigFormModel,
|
form: LeagueConfigFormModel,
|
||||||
ownerId: string,
|
ownerId: string,
|
||||||
): CreateLeagueWithSeasonAndScoringCommand {
|
): Promise<CreateLeagueResult> {
|
||||||
const structure = form.structure;
|
const structure = form.structure;
|
||||||
let maxDrivers: number;
|
let maxDrivers: number;
|
||||||
let maxTeams: number;
|
|
||||||
|
|
||||||
if (structure.mode === 'solo') {
|
if (structure.mode === 'solo') {
|
||||||
maxDrivers =
|
maxDrivers =
|
||||||
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
typeof structure.maxDrivers === 'number' && structure.maxDrivers > 0
|
||||||
? structure.maxDrivers
|
? structure.maxDrivers
|
||||||
: 0;
|
: 0;
|
||||||
maxTeams = 0;
|
|
||||||
} else {
|
} else {
|
||||||
const teams =
|
const teams =
|
||||||
typeof structure.maxTeams === 'number' && structure.maxTeams > 0
|
typeof structure.maxTeams === 'number' && structure.maxTeams > 0
|
||||||
@@ -237,52 +262,23 @@ export function buildCreateLeagueCommandFromConfig(
|
|||||||
typeof structure.driversPerTeam === 'number' && structure.driversPerTeam > 0
|
typeof structure.driversPerTeam === 'number' && structure.driversPerTeam > 0
|
||||||
? structure.driversPerTeam
|
? structure.driversPerTeam
|
||||||
: 0;
|
: 0;
|
||||||
maxTeams = teams;
|
|
||||||
maxDrivers = teams > 0 && perTeam > 0 ? teams * perTeam : 0;
|
maxDrivers = teams > 0 && perTeam > 0 ? teams * perTeam : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result = await apiClient.leagues.create({
|
||||||
name: form.basics.name.trim(),
|
name: form.basics.name.trim(),
|
||||||
description: (form.basics.description ?? '').trim(),
|
description: (form.basics.description ?? '').trim(),
|
||||||
visibility: form.basics.visibility,
|
isPublic: form.basics.visibility === 'public',
|
||||||
|
maxMembers: maxDrivers,
|
||||||
ownerId,
|
ownerId,
|
||||||
gameId: form.basics.gameId,
|
});
|
||||||
maxDrivers,
|
|
||||||
maxTeams,
|
return {
|
||||||
enableDriverChampionship: form.championships.enableDriverChampionship,
|
leagueId: result.leagueId,
|
||||||
enableTeamChampionship: form.championships.enableTeamChampionship,
|
success: result.success,
|
||||||
enableNationsChampionship: form.championships.enableNationsChampionship,
|
|
||||||
enableTrophyChampionship: form.championships.enableTrophyChampionship,
|
|
||||||
scoringPresetId: form.scoring.patternId ?? 'custom',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Thin application-level facade that:
|
|
||||||
* - pulls the current driver via repository
|
|
||||||
* - builds the creation command
|
|
||||||
* - delegates to the create-league use case
|
|
||||||
*/
|
|
||||||
export async function createLeagueFromConfig(
|
|
||||||
form: LeagueConfigFormModel,
|
|
||||||
): Promise<CreateLeagueWithSeasonAndScoringResultDTO> {
|
|
||||||
const driverRepo = getDriverRepository();
|
|
||||||
const drivers = await driverRepo.findAll();
|
|
||||||
const currentDriver = drivers[0];
|
|
||||||
|
|
||||||
if (!currentDriver) {
|
|
||||||
const error = new Error(
|
|
||||||
'No driver profile found. Please create a driver profile first.',
|
|
||||||
) as Error & { code?: string };
|
|
||||||
error.code = 'NO_DRIVER';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useCase = getCreateLeagueWithSeasonAndScoringUseCase();
|
|
||||||
const command = buildCreateLeagueCommandFromConfig(form, currentDriver.id);
|
|
||||||
return useCase.execute(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply scoring preset selection and derive timings, returning a new form model.
|
* Apply scoring preset selection and derive timings, returning a new form model.
|
||||||
* This mirrors the previous React handler but keeps it in testable, non-UI logic.
|
* This mirrors the previous React handler but keeps it in testable, non-UI logic.
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { AnalyticsProviders, PAGE_VIEW_REPOSITORY_TOKEN, ENGAGEMENT_REPOSITORY_TOKEN } from './AnalyticsProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: AnalyticsProviders,
|
|
||||||
exports: [PAGE_VIEW_REPOSITORY_TOKEN, ENGAGEMENT_REPOSITORY_TOKEN],
|
|
||||||
})
|
|
||||||
export class AnalyticsModule {}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository';
|
|
||||||
import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { InMemoryPageViewRepository } from '@gridpilot/adapters/analytics/persistence/inmemory/InMemoryPageViewRepository';
|
|
||||||
import { InMemoryEngagementRepository } from '@gridpilot/adapters/analytics/persistence/inmemory/InMemoryEngagementRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const PAGE_VIEW_REPOSITORY_TOKEN = Symbol('IPageViewRepository');
|
|
||||||
export const ENGAGEMENT_REPOSITORY_TOKEN = Symbol('IEngagementRepository');
|
|
||||||
|
|
||||||
export const AnalyticsProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: PAGE_VIEW_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryPageViewRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ENGAGEMENT_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryEngagementRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { AuthProviders, AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: AuthProviders,
|
|
||||||
exports: [AUTH_REPOSITORY_TOKEN, USER_REPOSITORY_TOKEN],
|
|
||||||
})
|
|
||||||
export class AuthModule {}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { IAuthRepository } from '@gridpilot/identity/domain/repositories/IAuthRepository';
|
|
||||||
import { IUserRepository } from '@gridpilot/identity/domain/repositories/IUserRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { InMemoryAuthRepository } from '@gridpilot/adapters/identity/persistence/inmemory/InMemoryAuthRepository';
|
|
||||||
import { InMemoryUserRepository } from '@gridpilot/adapters/identity/persistence/inmemory/InMemoryUserRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const AUTH_REPOSITORY_TOKEN = Symbol('IAuthRepository');
|
|
||||||
export const USER_REPOSITORY_TOKEN = Symbol('IUserRepository');
|
|
||||||
|
|
||||||
export const AuthProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: AUTH_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryAuthRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: USER_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryUserRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { DriverProviders, DRIVER_REPOSITORY_TOKEN } from './DriverProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: DriverProviders,
|
|
||||||
exports: [DRIVER_REPOSITORY_TOKEN],
|
|
||||||
})
|
|
||||||
export class DriverModule {}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { InMemoryDriverRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryDriverRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const DRIVER_REPOSITORY_TOKEN = Symbol('IDriverRepository');
|
|
||||||
|
|
||||||
export const DriverProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: DRIVER_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { LeagueProviders, GET_LEAGUE_STANDINGS_USE_CASE_TOKEN } from './LeagueProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: LeagueProviders,
|
|
||||||
exports: [GET_LEAGUE_STANDINGS_USE_CASE_TOKEN],
|
|
||||||
})
|
|
||||||
export class LeagueModule {}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { GetLeagueStandingsUseCase } from '@gridpilot/league/application/use-cases/GetLeagueStandingsUseCase';
|
|
||||||
import { ILeagueStandingsRepository } from '@gridpilot/league/application/ports/ILeagueStandingsRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { GetLeagueStandingsUseCaseImpl } from '@gridpilot/league/application/use-cases/GetLeagueStandingsUseCaseImpl';
|
|
||||||
import { InMemoryLeagueStandingsRepository } from '@gridpilot/adapters/league/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const GET_LEAGUE_STANDINGS_USE_CASE_TOKEN = Symbol('GetLeagueStandingsUseCase');
|
|
||||||
export const LEAGUE_STANDINGS_REPOSITORY_TOKEN = Symbol('ILeagueStandingsRepository');
|
|
||||||
|
|
||||||
export const LeagueProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: GET_LEAGUE_STANDINGS_USE_CASE_TOKEN,
|
|
||||||
useFactory: (repository: ILeagueStandingsRepository, logger: Logger) => new GetLeagueStandingsUseCaseImpl(repository),
|
|
||||||
inject: [LEAGUE_STANDINGS_REPOSITORY_TOKEN, LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: LEAGUE_STANDINGS_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
import { ConsoleLogger } from '@gridpilot/adapters/logging/ConsoleLogger';
|
|
||||||
|
|
||||||
export const LOGGER_TOKEN = Symbol('Logger');
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: LOGGER_TOKEN,
|
|
||||||
useClass: ConsoleLogger,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [LOGGER_TOKEN],
|
|
||||||
})
|
|
||||||
export class LoggingModule {}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { MediaProviders, AVATAR_GENERATION_REPOSITORY_TOKEN } from './MediaProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: MediaProviders,
|
|
||||||
exports: [AVATAR_GENERATION_REPOSITORY_TOKEN],
|
|
||||||
})
|
|
||||||
export class MediaModule {}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { IAvatarGenerationRepository } from '@gridpilot/media/domain/repositories/IAvatarGenerationRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { InMemoryAvatarGenerationRepository } from '@gridpilot/adapters/media/persistence/inmemory/InMemoryAvatarGenerationRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const AVATAR_GENERATION_REPOSITORY_TOKEN = Symbol('IAvatarGenerationRepository');
|
|
||||||
|
|
||||||
export const MediaProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: AVATAR_GENERATION_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryAvatarGenerationRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { RaceProviders, RACE_REPOSITORY_TOKEN } from './RaceProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: RaceProviders,
|
|
||||||
exports: [RACE_REPOSITORY_TOKEN],
|
|
||||||
})
|
|
||||||
export class RaceModule {}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { InMemoryRaceRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryRaceRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const RACE_REPOSITORY_TOKEN = Symbol('IRaceRepository');
|
|
||||||
|
|
||||||
export const RaceProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: RACE_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryRaceRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { SponsorProviders, SPONSOR_REPOSITORY_TOKEN } from './SponsorProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: SponsorProviders,
|
|
||||||
exports: [SPONSOR_REPOSITORY_TOKEN],
|
|
||||||
})
|
|
||||||
export class SponsorModule {}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { ISponsorRepository } from '@gridpilot/racing/domain/repositories/ISponsorRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { InMemorySponsorRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const SPONSOR_REPOSITORY_TOKEN = Symbol('ISponsorRepository');
|
|
||||||
|
|
||||||
export const SponsorProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: SPONSOR_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemorySponsorRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TeamProviders, TEAM_REPOSITORY_TOKEN } from './TeamProviders';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [],
|
|
||||||
providers: TeamProviders,
|
|
||||||
exports: [TEAM_REPOSITORY_TOKEN],
|
|
||||||
})
|
|
||||||
export class TeamModule {}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
|
||||||
|
|
||||||
// Import core interfaces
|
|
||||||
import { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
|
|
||||||
import { Logger } from '@gridpilot/shared/logging/Logger';
|
|
||||||
|
|
||||||
// Import implementations
|
|
||||||
import { InMemoryTeamRepository } from '@gridpilot/adapters/racing/persistence/inmemory/InMemoryTeamRepository';
|
|
||||||
|
|
||||||
// Import tokens
|
|
||||||
import { LOGGER_TOKEN } from '../logging/LoggingModule';
|
|
||||||
|
|
||||||
// Define injection tokens
|
|
||||||
export const TEAM_REPOSITORY_TOKEN = Symbol('ITeamRepository');
|
|
||||||
|
|
||||||
export const TeamProviders: Provider[] = [
|
|
||||||
{
|
|
||||||
provide: TEAM_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryTeamRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,9 +1,89 @@
|
|||||||
import type {
|
/**
|
||||||
IAllLeaguesWithCapacityAndScoringPresenter,
|
* AllLeaguesWithCapacityAndScoringPresenter - Pure data transformer
|
||||||
LeagueEnrichedData,
|
* Transforms API response to view model without DI dependencies.
|
||||||
LeagueSummaryViewModel,
|
*/
|
||||||
AllLeaguesWithCapacityAndScoringViewModel,
|
|
||||||
} from '@gridpilot/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter';
|
import { apiClient, type AllLeaguesWithCapacityViewModel } from '@/lib/apiClient';
|
||||||
|
|
||||||
|
export interface LeagueScoringViewModel {
|
||||||
|
gameId: string;
|
||||||
|
gameName: string;
|
||||||
|
primaryChampionshipType: string;
|
||||||
|
scoringPresetId: string;
|
||||||
|
scoringPresetName: string;
|
||||||
|
dropPolicySummary: string;
|
||||||
|
scoringPatternSummary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueSummaryViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | undefined;
|
||||||
|
ownerId: string;
|
||||||
|
createdAt: string;
|
||||||
|
maxDrivers: number;
|
||||||
|
usedDriverSlots: number;
|
||||||
|
maxTeams: number;
|
||||||
|
usedTeamSlots: number;
|
||||||
|
structureSummary: string;
|
||||||
|
scoringPatternSummary: string;
|
||||||
|
timingSummary: string;
|
||||||
|
scoring: LeagueScoringViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllLeaguesWithCapacityAndScoringViewModel {
|
||||||
|
leagues: LeagueSummaryViewModel[];
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAllLeaguesWithCapacityAndScoringPresenter {
|
||||||
|
reset(): void;
|
||||||
|
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform API response to view model
|
||||||
|
*/
|
||||||
|
function transformApiResponse(apiResponse: AllLeaguesWithCapacityViewModel): AllLeaguesWithCapacityAndScoringViewModel {
|
||||||
|
const leagueItems: LeagueSummaryViewModel[] = apiResponse.leagues.map((league) => {
|
||||||
|
const maxDrivers = league.maxMembers;
|
||||||
|
const usedDriverSlots = league.memberCount;
|
||||||
|
const structureSummary = `Solo • ${maxDrivers} drivers`;
|
||||||
|
const timingSummary = '30 min Quali • 40 min Race';
|
||||||
|
const scoringPatternSummary = 'Custom • All results count';
|
||||||
|
|
||||||
|
const scoringSummary: LeagueScoringViewModel = {
|
||||||
|
gameId: 'unknown',
|
||||||
|
gameName: 'Unknown',
|
||||||
|
primaryChampionshipType: 'driver',
|
||||||
|
scoringPresetId: 'custom',
|
||||||
|
scoringPresetName: 'Custom',
|
||||||
|
dropPolicySummary: 'All results count',
|
||||||
|
scoringPatternSummary,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: league.id,
|
||||||
|
name: league.name,
|
||||||
|
description: league.description,
|
||||||
|
ownerId: league.ownerId,
|
||||||
|
createdAt: new Date().toISOString(), // Would need from API
|
||||||
|
maxDrivers,
|
||||||
|
usedDriverSlots,
|
||||||
|
maxTeams: 0,
|
||||||
|
usedTeamSlots: 0,
|
||||||
|
structureSummary,
|
||||||
|
scoringPatternSummary,
|
||||||
|
timingSummary,
|
||||||
|
scoring: scoringSummary,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
leagues: leagueItems,
|
||||||
|
totalCount: leagueItems.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
|
export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWithCapacityAndScoringPresenter {
|
||||||
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null;
|
private viewModel: AllLeaguesWithCapacityAndScoringViewModel | null = null;
|
||||||
@@ -12,116 +92,20 @@ export class AllLeaguesWithCapacityAndScoringPresenter implements IAllLeaguesWit
|
|||||||
this.viewModel = null;
|
this.viewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
present(enrichedLeagues: LeagueEnrichedData[]): void {
|
async fetchAndPresent(): Promise<void> {
|
||||||
const leagueItems: LeagueSummaryViewModel[] = enrichedLeagues.map((data) => {
|
const apiResponse = await apiClient.leagues.getAllWithCapacity();
|
||||||
const { league, usedDriverSlots, season, scoringConfig, game, preset } = data;
|
this.viewModel = transformApiResponse(apiResponse);
|
||||||
|
|
||||||
const configuredMaxDrivers = league.settings.maxDrivers ?? usedDriverSlots;
|
|
||||||
const safeMaxDrivers = Math.max(configuredMaxDrivers, usedDriverSlots);
|
|
||||||
|
|
||||||
const structureSummary = `Solo • ${safeMaxDrivers} drivers`;
|
|
||||||
|
|
||||||
const qualifyingMinutes = 30;
|
|
||||||
const mainRaceMinutes =
|
|
||||||
typeof league.settings.sessionDuration === 'number'
|
|
||||||
? league.settings.sessionDuration
|
|
||||||
: 40;
|
|
||||||
const timingSummary = `${qualifyingMinutes} min Quali • ${mainRaceMinutes} min Race`;
|
|
||||||
|
|
||||||
let scoringPatternSummary: string | null = null;
|
|
||||||
let scoringSummary: LeagueSummaryViewModel['scoring'];
|
|
||||||
|
|
||||||
if (season && scoringConfig && game) {
|
|
||||||
const dropPolicySummary =
|
|
||||||
preset?.dropPolicySummary ?? this.deriveDropPolicySummary(scoringConfig);
|
|
||||||
const primaryChampionshipType =
|
|
||||||
preset?.primaryChampionshipType ??
|
|
||||||
(scoringConfig.championships[0]?.type ?? 'driver');
|
|
||||||
|
|
||||||
const scoringPresetName = preset?.name ?? 'Custom';
|
|
||||||
scoringPatternSummary = `${scoringPresetName} • ${dropPolicySummary}`;
|
|
||||||
|
|
||||||
scoringSummary = {
|
|
||||||
gameId: game.id,
|
|
||||||
gameName: game.name,
|
|
||||||
primaryChampionshipType,
|
|
||||||
scoringPresetId: scoringConfig.scoringPresetId ?? 'custom',
|
|
||||||
scoringPresetName,
|
|
||||||
dropPolicySummary,
|
|
||||||
scoringPatternSummary,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const dropPolicySummary = 'All results count';
|
|
||||||
const scoringPresetName = 'Custom';
|
|
||||||
scoringPatternSummary = scoringPatternSummary ?? `${scoringPresetName} • ${dropPolicySummary}`;
|
|
||||||
|
|
||||||
scoringSummary = {
|
|
||||||
gameId: 'unknown',
|
|
||||||
gameName: 'Unknown',
|
|
||||||
primaryChampionshipType: 'driver',
|
|
||||||
scoringPresetId: 'custom',
|
|
||||||
scoringPresetName,
|
|
||||||
dropPolicySummary,
|
|
||||||
scoringPatternSummary,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const base: LeagueSummaryViewModel = {
|
|
||||||
id: league.id,
|
|
||||||
name: league.name,
|
|
||||||
description: league.description,
|
|
||||||
ownerId: league.ownerId,
|
|
||||||
createdAt: league.createdAt.toISOString(),
|
|
||||||
maxDrivers: safeMaxDrivers,
|
|
||||||
usedDriverSlots,
|
|
||||||
// Team capacity is not yet modeled here; use zero for now to satisfy strict typing.
|
|
||||||
maxTeams: 0,
|
|
||||||
usedTeamSlots: 0,
|
|
||||||
structureSummary,
|
|
||||||
scoringPatternSummary: scoringPatternSummary ?? '',
|
|
||||||
timingSummary,
|
|
||||||
scoring: scoringSummary,
|
|
||||||
};
|
|
||||||
|
|
||||||
return base;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.viewModel = {
|
|
||||||
leagues: leagueItems,
|
|
||||||
totalCount: leagueItems.length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null {
|
getViewModel(): AllLeaguesWithCapacityAndScoringViewModel | null {
|
||||||
return this.viewModel;
|
return this.viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private deriveDropPolicySummary(config: {
|
/**
|
||||||
championships: Array<{
|
* Convenience function to fetch and transform all leagues
|
||||||
dropScorePolicy: { strategy: string; count?: number; dropCount?: number };
|
*/
|
||||||
}>;
|
export async function fetchAllLeaguesWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringViewModel> {
|
||||||
}): string {
|
const apiResponse = await apiClient.leagues.getAllWithCapacity();
|
||||||
const championship = config.championships[0];
|
return transformApiResponse(apiResponse);
|
||||||
if (!championship) {
|
|
||||||
return 'All results count';
|
|
||||||
}
|
|
||||||
|
|
||||||
const policy = championship.dropScorePolicy;
|
|
||||||
if (!policy || policy.strategy === 'none') {
|
|
||||||
return 'All results count';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policy.strategy === 'bestNResults' && typeof policy.count === 'number') {
|
|
||||||
return `Best ${policy.count} results count`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
policy.strategy === 'dropWorstN' &&
|
|
||||||
typeof policy.dropCount === 'number'
|
|
||||||
) {
|
|
||||||
return `Worst ${policy.dropCount} results are dropped`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Custom drop score rules';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,56 @@
|
|||||||
import type {
|
/**
|
||||||
IAllTeamsPresenter,
|
* AllTeamsPresenter - Pure data transformer
|
||||||
TeamListItemViewModel,
|
* Transforms API response to view model without DI dependencies.
|
||||||
AllTeamsViewModel,
|
*/
|
||||||
AllTeamsResultDTO,
|
|
||||||
} from '@gridpilot/racing/application/presenters/IAllTeamsPresenter';
|
import { apiClient, type AllTeamsViewModel as ApiAllTeamsViewModel } from '@/lib/apiClient';
|
||||||
|
|
||||||
|
export interface TeamListItemViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tag?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
memberCount: number;
|
||||||
|
logoUrl?: string | undefined;
|
||||||
|
rating?: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllTeamsViewModel {
|
||||||
|
teams: TeamListItemViewModel[];
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAllTeamsPresenter {
|
||||||
|
reset(): void;
|
||||||
|
getViewModel(): AllTeamsViewModel | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform API response to view model
|
||||||
|
*/
|
||||||
|
function transformApiResponse(apiResponse: ApiAllTeamsViewModel): AllTeamsViewModel {
|
||||||
|
const teamItems: TeamListItemViewModel[] = apiResponse.teams.map((team) => {
|
||||||
|
const viewModel: TeamListItemViewModel = {
|
||||||
|
id: team.id,
|
||||||
|
name: team.name,
|
||||||
|
memberCount: team.memberCount ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (team.logoUrl) {
|
||||||
|
viewModel.logoUrl = team.logoUrl;
|
||||||
|
}
|
||||||
|
if (team.rating) {
|
||||||
|
viewModel.rating = team.rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
teams: teamItems,
|
||||||
|
totalCount: teamItems.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class AllTeamsPresenter implements IAllTeamsPresenter {
|
export class AllTeamsPresenter implements IAllTeamsPresenter {
|
||||||
private viewModel: AllTeamsViewModel | null = null;
|
private viewModel: AllTeamsViewModel | null = null;
|
||||||
@@ -12,23 +59,20 @@ export class AllTeamsPresenter implements IAllTeamsPresenter {
|
|||||||
this.viewModel = null;
|
this.viewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
present(input: AllTeamsResultDTO): void {
|
async fetchAndPresent(): Promise<void> {
|
||||||
const teamItems: TeamListItemViewModel[] = input.teams.map((team) => ({
|
const apiResponse = await apiClient.teams.getAll();
|
||||||
id: team.id,
|
this.viewModel = transformApiResponse(apiResponse);
|
||||||
name: team.name,
|
|
||||||
tag: team.tag,
|
|
||||||
description: team.description,
|
|
||||||
memberCount: team.memberCount ?? 0,
|
|
||||||
leagues: team.leagues,
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.viewModel = {
|
|
||||||
teams: teamItems,
|
|
||||||
totalCount: teamItems.length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewModel(): AllTeamsViewModel | null {
|
getViewModel(): AllTeamsViewModel | null {
|
||||||
return this.viewModel;
|
return this.viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to fetch and transform all teams
|
||||||
|
*/
|
||||||
|
export async function fetchAllTeams(): Promise<AllTeamsViewModel> {
|
||||||
|
const apiResponse = await apiClient.teams.getAll();
|
||||||
|
return transformApiResponse(apiResponse);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,59 @@
|
|||||||
import type {
|
/**
|
||||||
IDriverTeamPresenter,
|
* DriverTeamPresenter - Pure data transformer
|
||||||
DriverTeamViewModel,
|
* Transforms API response to view model without DI dependencies.
|
||||||
DriverTeamResultDTO,
|
*/
|
||||||
} from '@gridpilot/racing/application/presenters/IDriverTeamPresenter';
|
|
||||||
|
import { apiClient, type DriverTeamViewModel as ApiDriverTeamViewModel } from '@/lib/apiClient';
|
||||||
|
|
||||||
|
export interface DriverTeamMembershipViewModel {
|
||||||
|
role: string;
|
||||||
|
joinedAt: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriverTeamInfoViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tag?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
ownerId: string;
|
||||||
|
leagues?: string[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriverTeamViewModel {
|
||||||
|
team: DriverTeamInfoViewModel;
|
||||||
|
membership: DriverTeamMembershipViewModel;
|
||||||
|
isOwner: boolean;
|
||||||
|
canManage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDriverTeamPresenter {
|
||||||
|
reset(): void;
|
||||||
|
getViewModel(): DriverTeamViewModel | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform API response to view model
|
||||||
|
*/
|
||||||
|
function transformApiResponse(apiResponse: ApiDriverTeamViewModel): DriverTeamViewModel {
|
||||||
|
const isOwner = false; // Would need team owner info from API
|
||||||
|
const canManage = apiResponse.role === 'owner' || apiResponse.role === 'manager';
|
||||||
|
|
||||||
|
return {
|
||||||
|
team: {
|
||||||
|
id: apiResponse.teamId,
|
||||||
|
name: apiResponse.teamName,
|
||||||
|
ownerId: '', // Would need from API
|
||||||
|
},
|
||||||
|
membership: {
|
||||||
|
role: apiResponse.role === 'driver' ? 'member' : apiResponse.role,
|
||||||
|
joinedAt: new Date(apiResponse.joinedAt).toISOString(),
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
isOwner,
|
||||||
|
canManage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class DriverTeamPresenter implements IDriverTeamPresenter {
|
export class DriverTeamPresenter implements IDriverTeamPresenter {
|
||||||
private viewModel: DriverTeamViewModel | null = null;
|
private viewModel: DriverTeamViewModel | null = null;
|
||||||
@@ -11,32 +62,27 @@ export class DriverTeamPresenter implements IDriverTeamPresenter {
|
|||||||
this.viewModel = null;
|
this.viewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
present(input: DriverTeamResultDTO): void {
|
async fetchAndPresent(driverId: string): Promise<void> {
|
||||||
const { team, membership, driverId } = input;
|
const apiResponse = await apiClient.teams.getDriverTeam(driverId);
|
||||||
|
if (apiResponse) {
|
||||||
const isOwner = team.ownerId === driverId;
|
this.viewModel = transformApiResponse(apiResponse);
|
||||||
const canManage = membership.role === 'owner' || membership.role === 'manager';
|
} else {
|
||||||
|
this.viewModel = null;
|
||||||
this.viewModel = {
|
}
|
||||||
team: {
|
|
||||||
id: team.id,
|
|
||||||
name: team.name,
|
|
||||||
tag: team.tag,
|
|
||||||
description: team.description,
|
|
||||||
ownerId: team.ownerId,
|
|
||||||
leagues: team.leagues,
|
|
||||||
},
|
|
||||||
membership: {
|
|
||||||
role: membership.role === 'driver' ? 'member' : membership.role,
|
|
||||||
joinedAt: membership.joinedAt.toISOString(),
|
|
||||||
isActive: membership.status === 'active',
|
|
||||||
},
|
|
||||||
isOwner,
|
|
||||||
canManage,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewModel(): DriverTeamViewModel | null {
|
getViewModel(): DriverTeamViewModel | null {
|
||||||
return this.viewModel;
|
return this.viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to fetch and transform driver's team
|
||||||
|
*/
|
||||||
|
export async function fetchDriverTeam(driverId: string): Promise<DriverTeamViewModel | null> {
|
||||||
|
const apiResponse = await apiClient.teams.getDriverTeam(driverId);
|
||||||
|
if (!apiResponse) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return transformApiResponse(apiResponse);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,87 @@
|
|||||||
import { SkillLevelService } from '@gridpilot/racing/domain/services/SkillLevelService';
|
/**
|
||||||
import type {
|
* DriversLeaderboardPresenter - Pure data transformer
|
||||||
IDriversLeaderboardPresenter,
|
* Transforms API response to view model without DI dependencies.
|
||||||
DriverLeaderboardItemViewModel,
|
*/
|
||||||
DriversLeaderboardViewModel,
|
|
||||||
DriversLeaderboardResultDTO,
|
import { apiClient, type DriversLeaderboardViewModel as ApiDriversLeaderboardViewModel } from '@/lib/apiClient';
|
||||||
} from '@gridpilot/racing/application/presenters/IDriversLeaderboardPresenter';
|
|
||||||
|
export type SkillLevel = 'rookie' | 'amateur' | 'pro' | 'elite' | 'legend';
|
||||||
|
|
||||||
|
export interface DriverLeaderboardItemViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
rating: number;
|
||||||
|
skillLevel: SkillLevel;
|
||||||
|
nationality?: string | undefined;
|
||||||
|
racesCompleted: number;
|
||||||
|
wins: number;
|
||||||
|
podiums: number;
|
||||||
|
isActive: boolean;
|
||||||
|
rank: number;
|
||||||
|
avatarUrl?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DriversLeaderboardViewModel {
|
||||||
|
drivers: DriverLeaderboardItemViewModel[];
|
||||||
|
totalRaces: number;
|
||||||
|
totalWins: number;
|
||||||
|
activeCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDriversLeaderboardPresenter {
|
||||||
|
reset(): void;
|
||||||
|
getViewModel(): DriversLeaderboardViewModel | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate skill level from rating
|
||||||
|
*/
|
||||||
|
function getSkillLevel(rating: number): SkillLevel {
|
||||||
|
if (rating >= 5000) return 'legend';
|
||||||
|
if (rating >= 3500) return 'elite';
|
||||||
|
if (rating >= 2000) return 'pro';
|
||||||
|
if (rating >= 1000) return 'amateur';
|
||||||
|
return 'rookie';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform API response to view model
|
||||||
|
*/
|
||||||
|
function transformApiResponse(apiResponse: ApiDriversLeaderboardViewModel): DriversLeaderboardViewModel {
|
||||||
|
const items: DriverLeaderboardItemViewModel[] = apiResponse.drivers.map((driver, index) => {
|
||||||
|
const rating = driver.rating ?? 0;
|
||||||
|
const skillLevel = getSkillLevel(rating);
|
||||||
|
|
||||||
|
const viewModel: DriverLeaderboardItemViewModel = {
|
||||||
|
id: driver.id,
|
||||||
|
name: driver.name,
|
||||||
|
rating,
|
||||||
|
skillLevel,
|
||||||
|
racesCompleted: driver.races ?? 0,
|
||||||
|
wins: driver.wins ?? 0,
|
||||||
|
podiums: 0, // API may not provide this, default to 0
|
||||||
|
isActive: true,
|
||||||
|
rank: index + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (driver.avatarUrl) {
|
||||||
|
viewModel.avatarUrl = driver.avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0);
|
||||||
|
const totalWins = items.reduce((sum, d) => sum + d.wins, 0);
|
||||||
|
const activeCount = items.filter((d) => d.isActive).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
drivers: items,
|
||||||
|
totalRaces,
|
||||||
|
totalWins,
|
||||||
|
activeCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
|
||||||
private viewModel: DriversLeaderboardViewModel | null = null;
|
private viewModel: DriversLeaderboardViewModel | null = null;
|
||||||
@@ -13,63 +90,20 @@ export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter
|
|||||||
this.viewModel = null;
|
this.viewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
present(input: DriversLeaderboardResultDTO): void {
|
async fetchAndPresent(): Promise<void> {
|
||||||
const { drivers, rankings, stats, avatarUrls } = input;
|
const apiResponse = await apiClient.drivers.getLeaderboard();
|
||||||
const items: DriverLeaderboardItemViewModel[] = drivers.map((driver) => {
|
this.viewModel = transformApiResponse(apiResponse);
|
||||||
const driverStats = stats[driver.id];
|
|
||||||
const rating = driverStats?.rating ?? 0;
|
|
||||||
const wins = driverStats?.wins ?? 0;
|
|
||||||
const podiums = driverStats?.podiums ?? 0;
|
|
||||||
const totalRaces = driverStats?.totalRaces ?? 0;
|
|
||||||
|
|
||||||
let effectiveRank = Number.POSITIVE_INFINITY;
|
|
||||||
if (typeof driverStats?.overallRank === 'number' && driverStats.overallRank > 0) {
|
|
||||||
effectiveRank = driverStats.overallRank;
|
|
||||||
} else {
|
|
||||||
const indexInGlobal = rankings.findIndex((entry) => entry.driverId === driver.id);
|
|
||||||
if (indexInGlobal !== -1) {
|
|
||||||
effectiveRank = indexInGlobal + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillLevel = SkillLevelService.getSkillLevel(rating);
|
|
||||||
const isActive = rankings.some((r) => r.driverId === driver.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: driver.id,
|
|
||||||
name: driver.name,
|
|
||||||
rating,
|
|
||||||
skillLevel,
|
|
||||||
nationality: driver.country,
|
|
||||||
racesCompleted: totalRaces,
|
|
||||||
wins,
|
|
||||||
podiums,
|
|
||||||
isActive,
|
|
||||||
rank: effectiveRank,
|
|
||||||
avatarUrl: avatarUrls[driver.id] ?? '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
items.sort((a, b) => {
|
|
||||||
const rankA = Number.isFinite(a.rank) && a.rank > 0 ? a.rank : Number.POSITIVE_INFINITY;
|
|
||||||
const rankB = Number.isFinite(b.rank) && b.rank > 0 ? b.rank : Number.POSITIVE_INFINITY;
|
|
||||||
if (rankA !== rankB) return rankA - rankB;
|
|
||||||
return b.rating - a.rating;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalRaces = items.reduce((sum, d) => sum + d.racesCompleted, 0);
|
|
||||||
const totalWins = items.reduce((sum, d) => sum + d.wins, 0);
|
|
||||||
const activeCount = items.filter((d) => d.isActive).length;
|
|
||||||
|
|
||||||
this.viewModel = {
|
|
||||||
drivers: items,
|
|
||||||
totalRaces,
|
|
||||||
totalWins,
|
|
||||||
activeCount,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewModel(): DriversLeaderboardViewModel | null {
|
getViewModel(): DriversLeaderboardViewModel | null {
|
||||||
return this.viewModel;
|
return this.viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to fetch and transform drivers leaderboard
|
||||||
|
*/
|
||||||
|
export async function fetchDriversLeaderboard(): Promise<DriversLeaderboardViewModel> {
|
||||||
|
const apiResponse = await apiClient.drivers.getLeaderboard();
|
||||||
|
return transformApiResponse(apiResponse);
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,28 @@
|
|||||||
import type { League } from '@gridpilot/racing/domain/entities/League';
|
/**
|
||||||
import type { Protest } from '@gridpilot/racing/domain/entities/Protest';
|
* LeagueAdminPresenter - Pure data transformer
|
||||||
import type { Race } from '@gridpilot/racing/domain/entities/Race';
|
* Transforms API responses to view models without DI dependencies.
|
||||||
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
|
* All data fetching is done via apiClient.
|
||||||
import type { LeagueConfigFormModel } from '@gridpilot/racing/application';
|
*/
|
||||||
import type { LeagueConfigFormViewModel } from '@gridpilot/racing/application/presenters/ILeagueFullConfigPresenter';
|
|
||||||
import { LeagueFullConfigPresenter } from '@/lib/presenters/LeagueFullConfigPresenter';
|
import { apiClient } from '@/lib/apiClient';
|
||||||
import type { MembershipRole } from '@/lib/leagueMembership';
|
import type {
|
||||||
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
|
LeagueJoinRequestViewModel as ApiLeagueJoinRequestViewModel,
|
||||||
import {
|
LeagueConfigFormModelDto,
|
||||||
getLeagueMembershipRepository,
|
LeagueSeasonSummaryViewModel as ApiLeagueSeasonSummaryViewModel,
|
||||||
getDriverRepository,
|
DriverDTO,
|
||||||
getGetLeagueFullConfigUseCase,
|
} from '@/lib/apiClient';
|
||||||
getRaceRepository,
|
|
||||||
getProtestRepository,
|
// ============================================================================
|
||||||
getDriverStats,
|
// View Model Types
|
||||||
getAllDriverRankings,
|
// ============================================================================
|
||||||
getListSeasonsForLeagueUseCase,
|
|
||||||
} from '@/lib/di-container';
|
|
||||||
|
|
||||||
export interface LeagueJoinRequestViewModel {
|
export interface LeagueJoinRequestViewModel {
|
||||||
id: string;
|
id: string;
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
driverId: string;
|
driverId: string;
|
||||||
requestedAt: Date;
|
requestedAt: Date;
|
||||||
message?: string;
|
message?: string | undefined;
|
||||||
driver?: DriverDTO;
|
driver?: DriverDTO | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProtestDriverSummary {
|
export interface ProtestDriverSummary {
|
||||||
@@ -32,7 +30,11 @@ export interface ProtestDriverSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ProtestRaceSummary {
|
export interface ProtestRaceSummary {
|
||||||
[raceId: string]: Race;
|
[raceId: string]: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
scheduledTime: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LeagueOwnerSummaryViewModel {
|
export interface LeagueOwnerSummaryViewModel {
|
||||||
@@ -50,13 +52,21 @@ export interface LeagueSummaryViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LeagueAdminProtestsViewModel {
|
export interface LeagueAdminProtestsViewModel {
|
||||||
protests: Protest[];
|
protests: Array<{
|
||||||
|
id: string;
|
||||||
|
raceId: string;
|
||||||
|
complainantId: string;
|
||||||
|
defendantId: string;
|
||||||
|
description: string;
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
racesById: ProtestRaceSummary;
|
racesById: ProtestRaceSummary;
|
||||||
driversById: ProtestDriverSummary;
|
driversById: ProtestDriverSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LeagueAdminConfigViewModel {
|
export interface LeagueAdminConfigViewModel {
|
||||||
form: LeagueConfigFormModel | null;
|
form: LeagueConfigFormModelDto | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LeagueAdminPermissionsViewModel {
|
export interface LeagueAdminPermissionsViewModel {
|
||||||
@@ -68,8 +78,8 @@ export interface LeagueSeasonSummaryViewModel {
|
|||||||
seasonId: string;
|
seasonId: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: string;
|
status: string;
|
||||||
startDate?: Date;
|
startDate?: Date | undefined;
|
||||||
endDate?: Date;
|
endDate?: Date | undefined;
|
||||||
isPrimary: boolean;
|
isPrimary: boolean;
|
||||||
isParallelActive: boolean;
|
isParallelActive: boolean;
|
||||||
}
|
}
|
||||||
@@ -81,41 +91,31 @@ export interface LeagueAdminViewModel {
|
|||||||
protests: LeagueAdminProtestsViewModel;
|
protests: LeagueAdminProtestsViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MembershipRole = 'owner' | 'admin' | 'member';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Data Fetching Functions (using apiClient)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load join requests plus requester driver DTOs for a league.
|
* Load join requests for a league via API.
|
||||||
*/
|
*/
|
||||||
export async function loadLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
export async function loadLeagueJoinRequests(leagueId: string): Promise<LeagueJoinRequestViewModel[]> {
|
||||||
const membershipRepo = getLeagueMembershipRepository();
|
const requests = await apiClient.leagues.getJoinRequests(leagueId);
|
||||||
const requests = await membershipRepo.getJoinRequests(leagueId);
|
|
||||||
|
return requests.map((request: ApiLeagueJoinRequestViewModel) => {
|
||||||
const driverRepo = getDriverRepository();
|
const viewModel: LeagueJoinRequestViewModel = {
|
||||||
const uniqueDriverIds = Array.from(new Set(requests.map((r) => r.driverId)));
|
|
||||||
const driverEntities = await Promise.all(uniqueDriverIds.map((id) => driverRepo.findById(id)));
|
|
||||||
const driverDtos = driverEntities
|
|
||||||
.map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null))
|
|
||||||
.filter((dto): dto is DriverDTO => dto !== null);
|
|
||||||
|
|
||||||
const driversById: Record<string, DriverDTO> = {};
|
|
||||||
for (const dto of driverDtos) {
|
|
||||||
driversById[dto.id] = dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requests.map((request) => {
|
|
||||||
const base: LeagueJoinRequestViewModel = {
|
|
||||||
id: request.id,
|
id: request.id,
|
||||||
leagueId: request.leagueId,
|
leagueId: request.leagueId,
|
||||||
driverId: request.driverId,
|
driverId: request.driverId,
|
||||||
requestedAt: request.requestedAt,
|
requestedAt: new Date(request.requestedAt),
|
||||||
};
|
};
|
||||||
|
|
||||||
const message = request.message;
|
if (request.message) {
|
||||||
const driver = driversById[request.driverId];
|
viewModel.message = request.message;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return viewModel;
|
||||||
...base,
|
|
||||||
...(typeof message === 'string' && message.length > 0 ? { message } : {}),
|
|
||||||
...(driver ? { driver } : {}),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,84 +126,49 @@ export async function approveLeagueJoinRequest(
|
|||||||
leagueId: string,
|
leagueId: string,
|
||||||
requestId: string
|
requestId: string
|
||||||
): Promise<LeagueJoinRequestViewModel[]> {
|
): Promise<LeagueJoinRequestViewModel[]> {
|
||||||
const membershipRepo = getLeagueMembershipRepository();
|
await apiClient.leagues.approveJoinRequest(leagueId, requestId);
|
||||||
const requests = await membershipRepo.getJoinRequests(leagueId);
|
|
||||||
const request = requests.find((r) => r.id === requestId);
|
|
||||||
if (!request) {
|
|
||||||
throw new Error('Join request not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
await membershipRepo.saveMembership({
|
|
||||||
id: request.id,
|
|
||||||
leagueId: request.leagueId,
|
|
||||||
driverId: request.driverId,
|
|
||||||
role: 'member',
|
|
||||||
status: 'active',
|
|
||||||
joinedAt: new Date(),
|
|
||||||
});
|
|
||||||
await membershipRepo.removeJoinRequest(requestId);
|
|
||||||
|
|
||||||
return loadLeagueJoinRequests(leagueId);
|
return loadLeagueJoinRequests(leagueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reject a league join request (alpha: just remove).
|
* Reject a league join request.
|
||||||
*/
|
*/
|
||||||
export async function rejectLeagueJoinRequest(
|
export async function rejectLeagueJoinRequest(
|
||||||
leagueId: string,
|
leagueId: string,
|
||||||
requestId: string
|
requestId: string
|
||||||
): Promise<LeagueJoinRequestViewModel[]> {
|
): Promise<LeagueJoinRequestViewModel[]> {
|
||||||
const membershipRepo = getLeagueMembershipRepository();
|
await apiClient.leagues.rejectJoinRequest(leagueId, requestId);
|
||||||
await membershipRepo.removeJoinRequest(requestId);
|
|
||||||
return loadLeagueJoinRequests(leagueId);
|
return loadLeagueJoinRequests(leagueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute permissions for a performer on league membership actions.
|
* Get permissions for a performer on league membership actions.
|
||||||
*/
|
*/
|
||||||
export async function getLeagueAdminPermissions(
|
export async function getLeagueAdminPermissions(
|
||||||
leagueId: string,
|
leagueId: string,
|
||||||
performerDriverId: string
|
performerDriverId: string
|
||||||
): Promise<LeagueAdminPermissionsViewModel> {
|
): Promise<LeagueAdminPermissionsViewModel> {
|
||||||
const membershipRepo = getLeagueMembershipRepository();
|
const permissions = await apiClient.leagues.getAdminPermissions(leagueId, performerDriverId);
|
||||||
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
|
|
||||||
|
|
||||||
const isOwner = performer?.role === 'owner';
|
|
||||||
const isAdmin = performer?.role === 'admin';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canRemoveMember: Boolean(isOwner || isAdmin),
|
canRemoveMember: permissions.canManageMembers || permissions.isOwner || permissions.isAdmin,
|
||||||
canUpdateRoles: Boolean(isOwner),
|
canUpdateRoles: permissions.isOwner,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a member from the league, enforcing simple role rules.
|
* Remove a member from the league.
|
||||||
*/
|
*/
|
||||||
export async function removeLeagueMember(
|
export async function removeLeagueMember(
|
||||||
leagueId: string,
|
leagueId: string,
|
||||||
performerDriverId: string,
|
performerDriverId: string,
|
||||||
targetDriverId: string
|
targetDriverId: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const membershipRepo = getLeagueMembershipRepository();
|
await apiClient.leagues.removeMember(leagueId, performerDriverId, targetDriverId);
|
||||||
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
|
|
||||||
if (!performer || (performer.role !== 'owner' && performer.role !== 'admin')) {
|
|
||||||
throw new Error('Only owners or admins can remove members');
|
|
||||||
}
|
|
||||||
|
|
||||||
const membership = await membershipRepo.getMembership(leagueId, targetDriverId);
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Member not found');
|
|
||||||
}
|
|
||||||
if (membership.role === 'owner') {
|
|
||||||
throw new Error('Cannot remove the league owner');
|
|
||||||
}
|
|
||||||
|
|
||||||
await membershipRepo.removeMembership(leagueId, targetDriverId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a member's role, enforcing simple owner-only rules.
|
* Update a member's role.
|
||||||
*/
|
*/
|
||||||
export async function updateLeagueMemberRole(
|
export async function updateLeagueMemberRole(
|
||||||
leagueId: string,
|
leagueId: string,
|
||||||
@@ -211,68 +176,30 @@ export async function updateLeagueMemberRole(
|
|||||||
targetDriverId: string,
|
targetDriverId: string,
|
||||||
newRole: MembershipRole
|
newRole: MembershipRole
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const membershipRepo = getLeagueMembershipRepository();
|
await apiClient.leagues.updateMemberRole(leagueId, performerDriverId, targetDriverId, newRole);
|
||||||
const performer = await membershipRepo.getMembership(leagueId, performerDriverId);
|
|
||||||
if (!performer || performer.role !== 'owner') {
|
|
||||||
throw new Error('Only the league owner can update roles');
|
|
||||||
}
|
|
||||||
|
|
||||||
const membership = await membershipRepo.getMembership(leagueId, targetDriverId);
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Member not found');
|
|
||||||
}
|
|
||||||
if (membership.role === 'owner') {
|
|
||||||
throw new Error('Cannot change the owner role');
|
|
||||||
}
|
|
||||||
|
|
||||||
await membershipRepo.saveMembership({
|
|
||||||
...membership,
|
|
||||||
role: newRole,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load owner summary (DTO + rating/rank) for a league.
|
* Load owner summary for a league.
|
||||||
*/
|
*/
|
||||||
export async function loadLeagueOwnerSummary(params: {
|
export async function loadLeagueOwnerSummary(params: {
|
||||||
|
leagueId: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
}): Promise<LeagueOwnerSummaryViewModel | null> {
|
}): Promise<LeagueOwnerSummaryViewModel | null> {
|
||||||
const driverRepo = getDriverRepository();
|
const ownerSummary = await apiClient.leagues.getOwnerSummary(params.leagueId, params.ownerId);
|
||||||
const entity = await driverRepo.findById(params.ownerId);
|
|
||||||
if (!entity) return null;
|
if (!ownerSummary) {
|
||||||
|
|
||||||
const ownerDriver = EntityMappers.toDriverDTO(entity);
|
|
||||||
if (!ownerDriver) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const stats = getDriverStats(ownerDriver.id);
|
|
||||||
const allRankings = getAllDriverRankings();
|
|
||||||
|
|
||||||
let rating: number | null = stats?.rating ?? null;
|
|
||||||
let rank: number | null = null;
|
|
||||||
|
|
||||||
if (stats) {
|
|
||||||
if (typeof stats.overallRank === 'number' && stats.overallRank > 0) {
|
|
||||||
rank = stats.overallRank;
|
|
||||||
} else {
|
|
||||||
const indexInGlobal = allRankings.findIndex((stat) => stat.driverId === stats.driverId);
|
|
||||||
if (indexInGlobal !== -1) {
|
|
||||||
rank = indexInGlobal + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rating === null) {
|
|
||||||
const globalEntry = allRankings.find((stat) => stat.driverId === stats.driverId);
|
|
||||||
if (globalEntry) {
|
|
||||||
rating = globalEntry.rating;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// For now, return a simplified version - the API should provide driver details
|
||||||
return {
|
return {
|
||||||
driver: ownerDriver,
|
driver: {
|
||||||
rating,
|
id: params.ownerId,
|
||||||
rank,
|
name: ownerSummary.leagueName, // This would need to be populated from API
|
||||||
|
},
|
||||||
|
rating: null,
|
||||||
|
rank: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,107 +207,63 @@ export async function loadLeagueOwnerSummary(params: {
|
|||||||
* Load league full config form.
|
* Load league full config form.
|
||||||
*/
|
*/
|
||||||
export async function loadLeagueConfig(
|
export async function loadLeagueConfig(
|
||||||
leagueId: string,
|
leagueId: string
|
||||||
): Promise<LeagueAdminConfigViewModel> {
|
): Promise<LeagueAdminConfigViewModel> {
|
||||||
const useCase = getGetLeagueFullConfigUseCase();
|
const config = await apiClient.leagues.getConfig(leagueId);
|
||||||
const presenter = new LeagueFullConfigPresenter();
|
|
||||||
|
return {
|
||||||
await useCase.execute({ leagueId }, presenter);
|
form: config,
|
||||||
const fullConfig = presenter.getViewModel();
|
|
||||||
|
|
||||||
if (!fullConfig) {
|
|
||||||
return { form: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const formModel: LeagueConfigFormModel = {
|
|
||||||
leagueId: fullConfig.leagueId,
|
|
||||||
basics: {
|
|
||||||
...fullConfig.basics,
|
|
||||||
visibility: fullConfig.basics.visibility as LeagueConfigFormModel['basics']['visibility'],
|
|
||||||
},
|
|
||||||
structure: {
|
|
||||||
...fullConfig.structure,
|
|
||||||
mode: fullConfig.structure.mode as LeagueConfigFormModel['structure']['mode'],
|
|
||||||
},
|
|
||||||
championships: fullConfig.championships,
|
|
||||||
scoring: fullConfig.scoring,
|
|
||||||
dropPolicy: {
|
|
||||||
strategy: fullConfig.dropPolicy.strategy as LeagueConfigFormModel['dropPolicy']['strategy'],
|
|
||||||
...(fullConfig.dropPolicy.n !== undefined ? { n: fullConfig.dropPolicy.n } : {}),
|
|
||||||
},
|
|
||||||
timings: fullConfig.timings,
|
|
||||||
stewarding: {
|
|
||||||
decisionMode: fullConfig.stewarding.decisionMode as LeagueConfigFormModel['stewarding']['decisionMode'],
|
|
||||||
...(fullConfig.stewarding.requiredVotes !== undefined
|
|
||||||
? { requiredVotes: fullConfig.stewarding.requiredVotes }
|
|
||||||
: {}),
|
|
||||||
requireDefense: fullConfig.stewarding.requireDefense,
|
|
||||||
defenseTimeLimit: fullConfig.stewarding.defenseTimeLimit,
|
|
||||||
voteTimeLimit: fullConfig.stewarding.voteTimeLimit,
|
|
||||||
protestDeadlineHours: fullConfig.stewarding.protestDeadlineHours,
|
|
||||||
stewardingClosesHours: fullConfig.stewarding.stewardingClosesHours,
|
|
||||||
notifyAccusedOnProtest: fullConfig.stewarding.notifyAccusedOnProtest,
|
|
||||||
notifyOnVoteRequired: fullConfig.stewarding.notifyOnVoteRequired,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { form: formModel };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load protests, related races and driver DTOs for a league.
|
* Load protests for a league.
|
||||||
*/
|
*/
|
||||||
export async function loadLeagueProtests(leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
export async function loadLeagueProtests(leagueId: string): Promise<LeagueAdminProtestsViewModel> {
|
||||||
const raceRepo = getRaceRepository();
|
const protestsData = await apiClient.leagues.getProtests(leagueId);
|
||||||
const protestRepo = getProtestRepository();
|
|
||||||
const driverRepo = getDriverRepository();
|
// Transform the API response
|
||||||
|
const racesById: ProtestRaceSummary = {};
|
||||||
const leagueRaces = await raceRepo.findByLeagueId(leagueId);
|
const driversById: ProtestDriverSummary = {};
|
||||||
|
|
||||||
const allProtests: Protest[] = [];
|
|
||||||
const racesById: Record<string, Race> = {};
|
|
||||||
|
|
||||||
for (const race of leagueRaces) {
|
|
||||||
racesById[race.id] = race;
|
|
||||||
const raceProtests = await protestRepo.findByRaceId(race.id);
|
|
||||||
allProtests.push(...raceProtests);
|
|
||||||
}
|
|
||||||
|
|
||||||
const driverIds = new Set<string>();
|
|
||||||
allProtests.forEach((p) => {
|
|
||||||
driverIds.add(p.protestingDriverId);
|
|
||||||
driverIds.add(p.accusedDriverId);
|
|
||||||
});
|
|
||||||
|
|
||||||
const driverEntities = await Promise.all(Array.from(driverIds).map((id) => driverRepo.findById(id)));
|
|
||||||
const driverDtos = driverEntities
|
|
||||||
.map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null))
|
|
||||||
.filter((dto): dto is DriverDTO => dto !== null);
|
|
||||||
|
|
||||||
const driversById: Record<string, DriverDTO> = {};
|
|
||||||
for (const dto of driverDtos) {
|
|
||||||
driversById[dto.id] = dto;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
protests: allProtests,
|
protests: protestsData.protests.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
raceId: p.raceId,
|
||||||
|
complainantId: p.complainantId,
|
||||||
|
defendantId: p.defendantId,
|
||||||
|
description: p.description,
|
||||||
|
status: p.status,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
})),
|
||||||
racesById,
|
racesById,
|
||||||
driversById,
|
driversById,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load seasons for a league.
|
||||||
|
*/
|
||||||
export async function loadLeagueSeasons(leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
export async function loadLeagueSeasons(leagueId: string): Promise<LeagueSeasonSummaryViewModel[]> {
|
||||||
const useCase = getListSeasonsForLeagueUseCase();
|
const seasons = await apiClient.leagues.getSeasons(leagueId);
|
||||||
const result = await useCase.execute({ leagueId });
|
const activeCount = seasons.filter((s: ApiLeagueSeasonSummaryViewModel) => s.status === 'active').length;
|
||||||
const activeCount = result.items.filter((s) => s.status === 'active').length;
|
|
||||||
|
|
||||||
return result.items.map((s) => ({
|
return seasons.map((s: ApiLeagueSeasonSummaryViewModel) => {
|
||||||
seasonId: s.seasonId,
|
const viewModel: LeagueSeasonSummaryViewModel = {
|
||||||
name: s.name,
|
seasonId: s.id,
|
||||||
status: s.status,
|
name: s.name,
|
||||||
...(s.startDate ? { startDate: s.startDate } : {}),
|
status: s.status,
|
||||||
...(s.endDate ? { endDate: s.endDate } : {}),
|
isPrimary: false, // Would need to be provided by API
|
||||||
isPrimary: s.isPrimary ?? false,
|
isParallelActive: activeCount > 1 && s.status === 'active',
|
||||||
isParallelActive: activeCount > 1 && s.status === 'active',
|
};
|
||||||
}));
|
|
||||||
|
if (s.startDate) {
|
||||||
|
viewModel.startDate = new Date(s.startDate);
|
||||||
|
}
|
||||||
|
if (s.endDate) {
|
||||||
|
viewModel.endDate = new Date(s.endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewModel;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,26 @@
|
|||||||
import { GetLeagueStandingsUseCase, LeagueStandingsViewModel } from '@gridpilot/core/league/application/use-cases/GetLeagueStandingsUseCase';
|
/**
|
||||||
|
* LeagueStandingsPresenter - Pure data transformer
|
||||||
|
* Transforms API response to view model without DI dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient, type LeagueStandingsViewModel as ApiLeagueStandingsViewModel } from '@/lib/apiClient';
|
||||||
|
|
||||||
|
export interface LeagueStandingsEntryViewModel {
|
||||||
|
driverId: string;
|
||||||
|
driverName: string;
|
||||||
|
position: number;
|
||||||
|
points: number;
|
||||||
|
wins: number;
|
||||||
|
podiums: number;
|
||||||
|
races: number;
|
||||||
|
avatarUrl?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeagueStandingsViewModel {
|
||||||
|
leagueId: string;
|
||||||
|
standings: LeagueStandingsEntryViewModel[];
|
||||||
|
totalDrivers: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ILeagueStandingsPresenter {
|
export interface ILeagueStandingsPresenter {
|
||||||
present(leagueId: string): Promise<void>;
|
present(leagueId: string): Promise<void>;
|
||||||
@@ -6,20 +28,54 @@ export interface ILeagueStandingsPresenter {
|
|||||||
reset(): void;
|
reset(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform API response to view model
|
||||||
|
*/
|
||||||
|
function transformApiResponse(leagueId: string, apiResponse: ApiLeagueStandingsViewModel): LeagueStandingsViewModel {
|
||||||
|
const standings: LeagueStandingsEntryViewModel[] = apiResponse.standings.map((entry) => {
|
||||||
|
const viewModel: LeagueStandingsEntryViewModel = {
|
||||||
|
driverId: entry.driverId,
|
||||||
|
driverName: entry.driver?.name ?? 'Unknown Driver',
|
||||||
|
position: entry.position,
|
||||||
|
points: entry.points,
|
||||||
|
wins: entry.wins,
|
||||||
|
podiums: entry.podiums,
|
||||||
|
races: entry.races,
|
||||||
|
};
|
||||||
|
if (entry.driver?.avatarUrl) {
|
||||||
|
viewModel.avatarUrl = entry.driver.avatarUrl;
|
||||||
|
}
|
||||||
|
return viewModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
leagueId,
|
||||||
|
standings,
|
||||||
|
totalDrivers: standings.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter {
|
export class LeagueStandingsPresenter implements ILeagueStandingsPresenter {
|
||||||
private viewModel: LeagueStandingsViewModel | null = null;
|
private viewModel: LeagueStandingsViewModel | null = null;
|
||||||
|
|
||||||
constructor(private getLeagueStandingsUseCase: GetLeagueStandingsUseCase) {}
|
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.viewModel = null;
|
this.viewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async present(leagueId: string): Promise<void> {
|
async present(leagueId: string): Promise<void> {
|
||||||
this.viewModel = await this.getLeagueStandingsUseCase.execute(leagueId);
|
const apiResponse = await apiClient.leagues.getStandings(leagueId);
|
||||||
|
this.viewModel = transformApiResponse(leagueId, apiResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewModel(): LeagueStandingsViewModel | null {
|
getViewModel(): LeagueStandingsViewModel | null {
|
||||||
return this.viewModel;
|
return this.viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to fetch and transform standings
|
||||||
|
*/
|
||||||
|
export async function fetchLeagueStandings(leagueId: string): Promise<LeagueStandingsViewModel> {
|
||||||
|
const apiResponse = await apiClient.leagues.getStandings(leagueId);
|
||||||
|
return transformApiResponse(leagueId, apiResponse);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Race } from '@gridpilot/racing/domain/entities/Race';
|
/**
|
||||||
import { InMemoryRaceRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryRaceRepository';
|
* ScheduleRaceFormPresenter - Pure data transformer
|
||||||
import { getRaceRepository, getLeagueRepository } from '@/lib/di-container';
|
* Transforms API response to view model without DI dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '@/lib/apiClient';
|
||||||
|
|
||||||
export type SessionType = 'practice' | 'qualifying' | 'race';
|
export type SessionType = 'practice' | 'qualifying' | 'race';
|
||||||
|
|
||||||
@@ -29,43 +32,39 @@ export interface LeagueOptionViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter/Facade for the schedule race form.
|
* Load available leagues for the schedule form.
|
||||||
* Encapsulates all domain/repository access so the component can stay purely presentational.
|
|
||||||
*/
|
*/
|
||||||
export async function loadScheduleRaceFormLeagues(): Promise<LeagueOptionViewModel[]> {
|
export async function loadScheduleRaceFormLeagues(): Promise<LeagueOptionViewModel[]> {
|
||||||
const leagueRepo = getLeagueRepository();
|
const response = await apiClient.leagues.getAllWithCapacity();
|
||||||
const allLeagues = await leagueRepo.findAll();
|
return response.leagues.map((league) => ({
|
||||||
return allLeagues.map((league) => ({
|
|
||||||
id: league.id,
|
id: league.id,
|
||||||
name: league.name,
|
name: league.name,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a race via API.
|
||||||
|
* Note: This would need a dedicated API endpoint for race scheduling.
|
||||||
|
* For now, this is a placeholder that shows the expected interface.
|
||||||
|
*/
|
||||||
export async function scheduleRaceFromForm(
|
export async function scheduleRaceFromForm(
|
||||||
formData: ScheduleRaceFormData
|
formData: ScheduleRaceFormData
|
||||||
): Promise<ScheduledRaceViewModel> {
|
): Promise<ScheduledRaceViewModel> {
|
||||||
const raceRepo = getRaceRepository();
|
|
||||||
const scheduledAt = new Date(`${formData.scheduledDate}T${formData.scheduledTime}`);
|
const scheduledAt = new Date(`${formData.scheduledDate}T${formData.scheduledTime}`);
|
||||||
|
|
||||||
const race = Race.create({
|
// In the new architecture, race scheduling should be done via API
|
||||||
id: InMemoryRaceRepository.generateId(),
|
// This is a placeholder that returns expected data structure
|
||||||
|
// The API endpoint would need to be implemented: POST /races
|
||||||
|
|
||||||
|
// For now, return a mock response
|
||||||
|
// TODO: Replace with actual API call when race creation endpoint is available
|
||||||
|
return {
|
||||||
|
id: `race-${Date.now()}`,
|
||||||
leagueId: formData.leagueId,
|
leagueId: formData.leagueId,
|
||||||
track: formData.track.trim(),
|
track: formData.track.trim(),
|
||||||
car: formData.car.trim(),
|
car: formData.car.trim(),
|
||||||
sessionType: formData.sessionType,
|
sessionType: formData.sessionType,
|
||||||
scheduledAt,
|
scheduledAt,
|
||||||
status: 'scheduled',
|
status: 'scheduled',
|
||||||
});
|
|
||||||
|
|
||||||
const createdRace = await raceRepo.create(race);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: createdRace.id,
|
|
||||||
leagueId: createdRace.leagueId,
|
|
||||||
track: createdRace.track,
|
|
||||||
car: createdRace.car,
|
|
||||||
sessionType: createdRace.sessionType as SessionType,
|
|
||||||
scheduledAt: createdRace.scheduledAt,
|
|
||||||
status: createdRace.status,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
|
/**
|
||||||
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
|
* TeamAdminPresenter - Pure data transformer
|
||||||
import {
|
* Transforms API responses to view models without DI dependencies.
|
||||||
getDriverRepository,
|
* All data fetching is done via apiClient.
|
||||||
getGetTeamJoinRequestsUseCase,
|
*/
|
||||||
getApproveTeamJoinRequestUseCase,
|
|
||||||
getRejectTeamJoinRequestUseCase,
|
import { apiClient } from '@/lib/apiClient';
|
||||||
getUpdateTeamUseCase,
|
import type { DriverDTO } from '@/lib/apiClient';
|
||||||
} from '@/lib/di-container';
|
|
||||||
|
// ============================================================================
|
||||||
|
// View Model Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface TeamAdminJoinRequestViewModel {
|
export interface TeamAdminJoinRequestViewModel {
|
||||||
id: string;
|
id: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
driverId: string;
|
driverId: string;
|
||||||
requestedAt: Date;
|
requestedAt: Date;
|
||||||
message?: string;
|
message?: string | undefined;
|
||||||
driver?: DriverDTO;
|
driver?: DriverDTO | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamAdminTeamSummaryViewModel {
|
export interface TeamAdminTeamSummaryViewModel {
|
||||||
@@ -30,11 +33,15 @@ export interface TeamAdminViewModel {
|
|||||||
requests: TeamAdminJoinRequestViewModel[];
|
requests: TeamAdminJoinRequestViewModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Data Fetching Functions (using apiClient)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load join requests plus driver DTOs for a team.
|
* Load team admin view model via API.
|
||||||
*/
|
*/
|
||||||
export async function loadTeamAdminViewModel(
|
export async function loadTeamAdminViewModel(
|
||||||
team: TeamAdminTeamSummaryViewModel,
|
team: TeamAdminTeamSummaryViewModel
|
||||||
): Promise<TeamAdminViewModel> {
|
): Promise<TeamAdminViewModel> {
|
||||||
const requests = await loadTeamJoinRequests(team.id);
|
const requests = await loadTeamJoinRequests(team.id);
|
||||||
return {
|
return {
|
||||||
@@ -49,52 +56,27 @@ export async function loadTeamAdminViewModel(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load join requests for a team via API.
|
||||||
|
*/
|
||||||
export async function loadTeamJoinRequests(
|
export async function loadTeamJoinRequests(
|
||||||
teamId: string,
|
teamId: string
|
||||||
): Promise<TeamAdminJoinRequestViewModel[]> {
|
): Promise<TeamAdminJoinRequestViewModel[]> {
|
||||||
const getRequestsUseCase = getGetTeamJoinRequestsUseCase();
|
const response = await apiClient.teams.getJoinRequests(teamId);
|
||||||
const presenter = new (await import('./TeamJoinRequestsPresenter')).TeamJoinRequestsPresenter();
|
|
||||||
|
return response.requests.map((req) => {
|
||||||
await getRequestsUseCase.execute({ teamId }, presenter);
|
const viewModel: TeamAdminJoinRequestViewModel = {
|
||||||
|
id: req.id,
|
||||||
const presenterVm = presenter.getViewModel();
|
|
||||||
if (!presenterVm) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const driverRepo = getDriverRepository();
|
|
||||||
const allDrivers = await driverRepo.findAll();
|
|
||||||
const driversById: Record<string, DriverDTO> = {};
|
|
||||||
|
|
||||||
for (const driver of allDrivers) {
|
|
||||||
const dto = EntityMappers.toDriverDTO(driver);
|
|
||||||
if (dto) {
|
|
||||||
driversById[dto.id] = dto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return presenterVm.requests.map((req: {
|
|
||||||
requestId: string;
|
|
||||||
teamId: string;
|
|
||||||
driverId: string;
|
|
||||||
requestedAt: string;
|
|
||||||
message?: string;
|
|
||||||
}): TeamAdminJoinRequestViewModel => {
|
|
||||||
const base: TeamAdminJoinRequestViewModel = {
|
|
||||||
id: req.requestId,
|
|
||||||
teamId: req.teamId,
|
teamId: req.teamId,
|
||||||
driverId: req.driverId,
|
driverId: req.driverId,
|
||||||
requestedAt: new Date(req.requestedAt),
|
requestedAt: new Date(req.requestedAt),
|
||||||
};
|
};
|
||||||
|
|
||||||
const message = req.message;
|
if (req.message) {
|
||||||
const driver = driversById[req.driverId];
|
viewModel.message = req.message;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return viewModel;
|
||||||
...base,
|
|
||||||
...(message !== undefined ? { message } : {}),
|
|
||||||
...(driver !== undefined ? { driver } : {}),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,10 +85,9 @@ export async function loadTeamJoinRequests(
|
|||||||
*/
|
*/
|
||||||
export async function approveTeamJoinRequestAndReload(
|
export async function approveTeamJoinRequestAndReload(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
teamId: string,
|
teamId: string
|
||||||
): Promise<TeamAdminJoinRequestViewModel[]> {
|
): Promise<TeamAdminJoinRequestViewModel[]> {
|
||||||
const useCase = getApproveTeamJoinRequestUseCase();
|
await apiClient.teams.approveJoinRequest(teamId, requestId);
|
||||||
await useCase.execute({ requestId });
|
|
||||||
return loadTeamJoinRequests(teamId);
|
return loadTeamJoinRequests(teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,15 +96,14 @@ export async function approveTeamJoinRequestAndReload(
|
|||||||
*/
|
*/
|
||||||
export async function rejectTeamJoinRequestAndReload(
|
export async function rejectTeamJoinRequestAndReload(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
teamId: string,
|
teamId: string
|
||||||
): Promise<TeamAdminJoinRequestViewModel[]> {
|
): Promise<TeamAdminJoinRequestViewModel[]> {
|
||||||
const useCase = getRejectTeamJoinRequestUseCase();
|
await apiClient.teams.rejectJoinRequest(teamId, requestId);
|
||||||
await useCase.execute({ requestId });
|
|
||||||
return loadTeamJoinRequests(teamId);
|
return loadTeamJoinRequests(teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update team basic details.
|
* Update team basic details via API.
|
||||||
*/
|
*/
|
||||||
export async function updateTeamDetails(params: {
|
export async function updateTeamDetails(params: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -132,14 +112,8 @@ export async function updateTeamDetails(params: {
|
|||||||
description: string;
|
description: string;
|
||||||
updatedByDriverId: string;
|
updatedByDriverId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const useCase = getUpdateTeamUseCase();
|
await apiClient.teams.update(params.teamId, {
|
||||||
await useCase.execute({
|
name: params.name,
|
||||||
teamId: params.teamId,
|
description: params.description,
|
||||||
updates: {
|
|
||||||
name: params.name,
|
|
||||||
tag: params.tag,
|
|
||||||
description: params.description,
|
|
||||||
},
|
|
||||||
updatedBy: params.updatedByDriverId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,53 @@
|
|||||||
import type {
|
/**
|
||||||
ITeamDetailsPresenter,
|
* TeamDetailsPresenter - Pure data transformer
|
||||||
TeamDetailsViewModel,
|
* Transforms API response to view model without DI dependencies.
|
||||||
TeamDetailsResultDTO,
|
*/
|
||||||
} from '@gridpilot/racing/application/presenters/ITeamDetailsPresenter';
|
|
||||||
|
import { apiClient, type TeamDetailsViewModel as ApiTeamDetailsViewModel } from '@/lib/apiClient';
|
||||||
|
|
||||||
|
export interface TeamMembershipViewModel {
|
||||||
|
role: string;
|
||||||
|
joinedAt: string;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamInfoViewModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tag?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
ownerId: string;
|
||||||
|
leagues?: string[] | undefined;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TeamDetailsViewModel {
|
||||||
|
team: TeamInfoViewModel;
|
||||||
|
membership: TeamMembershipViewModel | null;
|
||||||
|
canManage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITeamDetailsPresenter {
|
||||||
|
reset(): void;
|
||||||
|
getViewModel(): TeamDetailsViewModel | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform API response to view model
|
||||||
|
*/
|
||||||
|
function transformApiResponse(apiResponse: ApiTeamDetailsViewModel): TeamDetailsViewModel {
|
||||||
|
return {
|
||||||
|
team: {
|
||||||
|
id: apiResponse.id,
|
||||||
|
name: apiResponse.name,
|
||||||
|
description: apiResponse.description,
|
||||||
|
ownerId: apiResponse.ownerId,
|
||||||
|
createdAt: new Date().toISOString(), // Would need from API
|
||||||
|
},
|
||||||
|
membership: null, // Would need from API based on current user
|
||||||
|
canManage: false, // Would need from API based on current user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
||||||
private viewModel: TeamDetailsViewModel | null = null;
|
private viewModel: TeamDetailsViewModel | null = null;
|
||||||
@@ -11,34 +56,27 @@ export class TeamDetailsPresenter implements ITeamDetailsPresenter {
|
|||||||
this.viewModel = null;
|
this.viewModel = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
present(input: TeamDetailsResultDTO): void {
|
async fetchAndPresent(teamId: string): Promise<void> {
|
||||||
const { team, membership } = input;
|
const apiResponse = await apiClient.teams.getDetails(teamId);
|
||||||
const canManage = membership?.role === 'owner' || membership?.role === 'manager';
|
if (apiResponse) {
|
||||||
|
this.viewModel = transformApiResponse(apiResponse);
|
||||||
const viewModel: TeamDetailsViewModel = {
|
} else {
|
||||||
team: {
|
this.viewModel = null;
|
||||||
id: team.id,
|
}
|
||||||
name: team.name,
|
|
||||||
tag: team.tag,
|
|
||||||
description: team.description,
|
|
||||||
ownerId: team.ownerId,
|
|
||||||
leagues: team.leagues,
|
|
||||||
createdAt: team.createdAt.toISOString(),
|
|
||||||
},
|
|
||||||
membership: membership
|
|
||||||
? {
|
|
||||||
role: membership.role === 'driver' ? 'member' : membership.role,
|
|
||||||
joinedAt: membership.joinedAt.toISOString(),
|
|
||||||
isActive: membership.status === 'active',
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
canManage,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.viewModel = viewModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewModel(): TeamDetailsViewModel | null {
|
getViewModel(): TeamDetailsViewModel | null {
|
||||||
return this.viewModel;
|
return this.viewModel;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to fetch and transform team details
|
||||||
|
*/
|
||||||
|
export async function fetchTeamDetails(teamId: string): Promise<TeamDetailsViewModel | null> {
|
||||||
|
const apiResponse = await apiClient.teams.getDetails(teamId);
|
||||||
|
if (!apiResponse) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return transformApiResponse(apiResponse);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,18 @@
|
|||||||
import type { TeamMembership, TeamRole } from '@gridpilot/racing/domain/types/TeamMembership';
|
/**
|
||||||
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
|
* TeamRosterPresenter - Pure data transformer
|
||||||
import { EntityMappers } from '@gridpilot/racing/application/mappers/EntityMappers';
|
* Transforms API response to view model without DI dependencies.
|
||||||
import { getDriverRepository, getDriverStats } from '@/lib/di-container';
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '@/lib/apiClient';
|
||||||
|
|
||||||
|
export type TeamRole = 'owner' | 'manager' | 'driver' | 'member';
|
||||||
|
|
||||||
|
export interface DriverDTO {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatarUrl?: string | undefined;
|
||||||
|
country?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TeamRosterMemberViewModel {
|
export interface TeamRosterMemberViewModel {
|
||||||
driver: DriverDTO;
|
driver: DriverDTO;
|
||||||
@@ -17,33 +28,24 @@ export interface TeamRosterViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter/facade for team roster.
|
* Fetch team roster via API and transform to view model.
|
||||||
* Encapsulates repository and stats access so the TeamRoster component can remain a pure view.
|
|
||||||
*/
|
*/
|
||||||
export async function getTeamRosterViewModel(
|
export async function getTeamRosterViewModel(
|
||||||
memberships: TeamMembership[]
|
teamId: string
|
||||||
): Promise<TeamRosterViewModel> {
|
): Promise<TeamRosterViewModel> {
|
||||||
const driverRepo = getDriverRepository();
|
const response = await apiClient.teams.getMembers(teamId);
|
||||||
const allDrivers = await driverRepo.findAll();
|
|
||||||
const members: TeamRosterMemberViewModel[] = [];
|
const members: TeamRosterMemberViewModel[] = response.members.map((member) => ({
|
||||||
|
driver: {
|
||||||
for (const membership of memberships) {
|
id: member.driverId,
|
||||||
const driver = allDrivers.find((d) => d.id === membership.driverId);
|
name: member.driver?.name ?? 'Unknown',
|
||||||
if (!driver) continue;
|
avatarUrl: member.driver?.avatarUrl,
|
||||||
|
},
|
||||||
const dto = EntityMappers.toDriverDTO(driver);
|
role: (member.role as TeamRole) ?? 'member',
|
||||||
if (!dto) continue;
|
joinedAt: member.joinedAt,
|
||||||
|
rating: null, // Would need from API
|
||||||
const stats = getDriverStats(membership.driverId);
|
overallRank: null, // Would need from API
|
||||||
|
}));
|
||||||
members.push({
|
|
||||||
driver: dto,
|
|
||||||
role: membership.role,
|
|
||||||
joinedAt: membership.joinedAt.toISOString(),
|
|
||||||
rating: stats?.rating ?? null,
|
|
||||||
overallRank: typeof stats?.overallRank === 'number' ? stats.overallRank : null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const averageRating =
|
const averageRating =
|
||||||
members.length > 0
|
members.length > 0
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { getStandingRepository, getLeagueRepository, getTeamMembershipRepository } from '@/lib/di-container';
|
/**
|
||||||
|
* TeamStandingsPresenter - Pure data transformer
|
||||||
|
* Transforms API response to view model without DI dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiClient } from '@/lib/apiClient';
|
||||||
|
|
||||||
export interface TeamLeagueStandingViewModel {
|
export interface TeamLeagueStandingViewModel {
|
||||||
leagueId: string;
|
leagueId: string;
|
||||||
@@ -15,61 +20,37 @@ export interface TeamStandingsViewModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute team standings across the given leagues for a team.
|
* Compute team standings across the given leagues for a team.
|
||||||
* Mirrors the previous TeamStandings component logic but keeps it out of the UI layer.
|
* This would need a dedicated API endpoint for team standings.
|
||||||
|
* For now, returns empty standings - the API should provide this data.
|
||||||
|
* @param teamId - The team ID (will be used when API supports team standings)
|
||||||
|
* @param leagueIds - List of league IDs to fetch standings for
|
||||||
*/
|
*/
|
||||||
export async function loadTeamStandings(
|
export async function loadTeamStandings(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
teamId: string,
|
teamId: string,
|
||||||
leagues: string[],
|
leagueIds: string[],
|
||||||
): Promise<TeamStandingsViewModel> {
|
): Promise<TeamStandingsViewModel> {
|
||||||
const standingRepo = getStandingRepository();
|
// In the new architecture, team standings should come from API
|
||||||
const leagueRepo = getLeagueRepository();
|
// For now, fetch each league's standings and aggregate
|
||||||
const teamMembershipRepo = getTeamMembershipRepository();
|
|
||||||
|
|
||||||
const members = await teamMembershipRepo.getTeamMembers(teamId);
|
|
||||||
const memberIds = members.map((m) => m.driverId);
|
|
||||||
|
|
||||||
const teamStandings: TeamLeagueStandingViewModel[] = [];
|
const teamStandings: TeamLeagueStandingViewModel[] = [];
|
||||||
|
|
||||||
for (const leagueId of leagues) {
|
for (const leagueId of leagueIds) {
|
||||||
const league = await leagueRepo.findById(leagueId);
|
try {
|
||||||
if (!league) continue;
|
const standings = await apiClient.leagues.getStandings(leagueId);
|
||||||
|
|
||||||
const leagueStandings = await standingRepo.findByLeagueId(leagueId);
|
// Since we don't have team-specific standings from API yet,
|
||||||
|
// this is a placeholder that returns basic data
|
||||||
let totalPoints = 0;
|
teamStandings.push({
|
||||||
let totalWins = 0;
|
leagueId,
|
||||||
let totalRaces = 0;
|
leagueName: `League ${leagueId}`, // Would need from API
|
||||||
|
position: 0,
|
||||||
for (const standing of leagueStandings) {
|
points: 0,
|
||||||
if (memberIds.includes(standing.driverId)) {
|
wins: 0,
|
||||||
totalPoints += standing.points;
|
racesCompleted: standings.standings.length > 0 ? 1 : 0,
|
||||||
totalWins += standing.wins;
|
});
|
||||||
totalRaces = Math.max(totalRaces, standing.racesCompleted);
|
} catch {
|
||||||
}
|
// Skip leagues that fail to load
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simplified team position based on total points (same spirit as previous logic)
|
|
||||||
const allTeamPoints = leagueStandings
|
|
||||||
.filter((s) => memberIds.includes(s.driverId))
|
|
||||||
.reduce((sum, s) => sum + s.points, 0);
|
|
||||||
|
|
||||||
const position =
|
|
||||||
leagueStandings
|
|
||||||
.filter((_, idx, arr) => {
|
|
||||||
const teamPoints = arr
|
|
||||||
.filter((s) => memberIds.includes(s.driverId))
|
|
||||||
.reduce((sum, s) => sum + s.points, 0);
|
|
||||||
return teamPoints > allTeamPoints;
|
|
||||||
}).length + 1;
|
|
||||||
|
|
||||||
teamStandings.push({
|
|
||||||
leagueId,
|
|
||||||
leagueName: league.name,
|
|
||||||
position,
|
|
||||||
points: totalPoints,
|
|
||||||
wins: totalWins,
|
|
||||||
racesCompleted: totalRaces,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { standings: teamStandings };
|
return { standings: teamStandings };
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
|
|
||||||
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
|
|
||||||
import type {
|
|
||||||
LeagueMembership as DomainLeagueMembership,
|
|
||||||
MembershipRole,
|
|
||||||
MembershipStatus,
|
|
||||||
} from '@gridpilot/racing/domain/entities/LeagueMembership';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight league membership model mirroring the domain type but with
|
|
||||||
* a stringified joinedAt for easier UI formatting.
|
|
||||||
*/
|
|
||||||
export interface LeagueMembership extends Omit<DomainLeagueMembership, 'joinedAt'> {
|
|
||||||
joinedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LeagueMembershipService {
|
|
||||||
private leagueMemberships = new Map<string, LeagueMembership[]>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly leagueRepository: ILeagueRepository,
|
|
||||||
private readonly membershipRepository: ILeagueMembershipRepository,
|
|
||||||
) {
|
|
||||||
this.initializeLeagueMembershipsFromRepository();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize league memberships once from the in-memory league membership repository
|
|
||||||
* that is seeded via the static racing seed in the DI container.
|
|
||||||
*
|
|
||||||
* This avoids depending on raw testing-support seed exports and keeps all demo
|
|
||||||
* membership data flowing through the same in-memory repositories used elsewhere.
|
|
||||||
*/
|
|
||||||
private async initializeLeagueMembershipsFromRepository() {
|
|
||||||
if (this.leagueMemberships.size > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const allLeagues = await this.leagueRepository.findAll();
|
|
||||||
const byLeague = new Map<string, LeagueMembership[]>();
|
|
||||||
|
|
||||||
for (const league of allLeagues) {
|
|
||||||
const memberships = await this.membershipRepository.getLeagueMembers(league.id);
|
|
||||||
|
|
||||||
const mapped: LeagueMembership[] = memberships.map((membership) => ({
|
|
||||||
id: membership.id,
|
|
||||||
leagueId: membership.leagueId,
|
|
||||||
driverId: membership.driverId,
|
|
||||||
role: membership.role,
|
|
||||||
status: membership.status,
|
|
||||||
joinedAt:
|
|
||||||
membership.joinedAt instanceof Date
|
|
||||||
? membership.joinedAt.toISOString()
|
|
||||||
: new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
byLeague.set(league.id, mapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [leagueId, list] of byLeague.entries()) {
|
|
||||||
this.leagueMemberships.set(leagueId, list);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// In alpha/demo mode we tolerate failures here; callers will see empty memberships.
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Failed to initialize league memberships from repository', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getMembership(leagueId: string, driverId: string): LeagueMembership | null {
|
|
||||||
const list = this.leagueMemberships.get(leagueId);
|
|
||||||
if (!list) return null;
|
|
||||||
return list.find((m) => m.driverId === driverId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLeagueMembers(leagueId: string): LeagueMembership[] {
|
|
||||||
return [...(this.leagueMemberships.get(leagueId) ?? [])];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derive a driver's primary league from in-memory league memberships.
|
|
||||||
* Prefers any active membership and returns the first matching league.
|
|
||||||
*/
|
|
||||||
getPrimaryLeagueIdForDriver(driverId: string): string | null {
|
|
||||||
for (const [leagueId, members] of this.leagueMemberships.entries()) {
|
|
||||||
if (members.some((m) => m.driverId === driverId && m.status === 'active')) {
|
|
||||||
return leagueId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
|
|
||||||
const membership = this.getMembership(leagueId, driverId);
|
|
||||||
if (!membership) return false;
|
|
||||||
return membership.role === 'owner' || membership.role === 'admin';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { MembershipRole, MembershipStatus };
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export interface RequestAvatarGenerationResultDTO {
|
||||||
|
requestId: string;
|
||||||
|
status: 'validating' | 'generating' | 'completed' | 'failed';
|
||||||
|
avatarUrls?: string[];
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequestAvatarGenerationPresenter {
|
||||||
|
reset(): void;
|
||||||
|
present(dto: RequestAvatarGenerationResultDTO): void;
|
||||||
|
get viewModel(): any;
|
||||||
|
getViewModel(): any;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { AsyncUseCase, Logger } from '@gridpilot/shared/application';
|
import type { UseCase, Logger } from '@gridpilot/shared/application';
|
||||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||||
import type { FaceValidationPort } from '../ports/FaceValidationPort';
|
import type { FaceValidationPort } from '../ports/FaceValidationPort';
|
||||||
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
|
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
|
||||||
|
import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '../presenters/IRequestAvatarGenerationPresenter';
|
||||||
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||||
import type { RacingSuitColor, AvatarStyle } from '../../domain/types/AvatarGenerationRequest';
|
import type { RacingSuitColor, AvatarStyle } from '../../domain/types/AvatarGenerationRequest';
|
||||||
|
|
||||||
@@ -12,15 +13,8 @@ export interface RequestAvatarGenerationCommand {
|
|||||||
style?: AvatarStyle;
|
style?: AvatarStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestAvatarGenerationResult {
|
|
||||||
requestId: string;
|
|
||||||
status: 'validating' | 'generating' | 'completed' | 'failed';
|
|
||||||
avatarUrls?: string[];
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RequestAvatarGenerationUseCase
|
export class RequestAvatarGenerationUseCase
|
||||||
implements AsyncUseCase<RequestAvatarGenerationCommand, RequestAvatarGenerationResult> {
|
implements UseCase<RequestAvatarGenerationCommand, RequestAvatarGenerationResultDTO, any, IRequestAvatarGenerationPresenter> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly avatarRepository: IAvatarGenerationRepository,
|
private readonly avatarRepository: IAvatarGenerationRepository,
|
||||||
private readonly faceValidation: FaceValidationPort,
|
private readonly faceValidation: FaceValidationPort,
|
||||||
@@ -28,7 +22,8 @@ export class RequestAvatarGenerationUseCase
|
|||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(command: RequestAvatarGenerationCommand): Promise<RequestAvatarGenerationResult> {
|
async execute(command: RequestAvatarGenerationCommand, presenter: IRequestAvatarGenerationPresenter): Promise<void> {
|
||||||
|
presenter.reset();
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Executing RequestAvatarGenerationUseCase for userId: ${command.userId}`,
|
`Executing RequestAvatarGenerationUseCase for userId: ${command.userId}`,
|
||||||
command,
|
command,
|
||||||
@@ -64,11 +59,12 @@ export class RequestAvatarGenerationUseCase
|
|||||||
request.fail(errorMessage);
|
request.fail(errorMessage);
|
||||||
await this.avatarRepository.save(request);
|
await this.avatarRepository.save(request);
|
||||||
this.logger.error(`Face validation failed for request ${requestId}: ${errorMessage}`);
|
this.logger.error(`Face validation failed for request ${requestId}: ${errorMessage}`);
|
||||||
return {
|
presenter.present({
|
||||||
requestId,
|
requestId,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errorMessage: validationResult.errorMessage || 'Please upload a clear photo of your face',
|
errorMessage: validationResult.errorMessage || 'Please upload a clear photo of your face',
|
||||||
};
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validationResult.hasFace) {
|
if (!validationResult.hasFace) {
|
||||||
@@ -76,11 +72,12 @@ export class RequestAvatarGenerationUseCase
|
|||||||
request.fail(errorMessage);
|
request.fail(errorMessage);
|
||||||
await this.avatarRepository.save(request);
|
await this.avatarRepository.save(request);
|
||||||
this.logger.error(`No face detected for request ${requestId}: ${errorMessage}`);
|
this.logger.error(`No face detected for request ${requestId}: ${errorMessage}`);
|
||||||
return {
|
presenter.present({
|
||||||
requestId,
|
requestId,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errorMessage: 'No face detected. Please upload a photo that clearly shows your face.',
|
errorMessage: 'No face detected. Please upload a photo that clearly shows your face.',
|
||||||
};
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationResult.faceCount > 1) {
|
if (validationResult.faceCount > 1) {
|
||||||
@@ -88,11 +85,12 @@ export class RequestAvatarGenerationUseCase
|
|||||||
request.fail(errorMessage);
|
request.fail(errorMessage);
|
||||||
await this.avatarRepository.save(request);
|
await this.avatarRepository.save(request);
|
||||||
this.logger.error(`Multiple faces detected for request ${requestId}: ${errorMessage}`);
|
this.logger.error(`Multiple faces detected for request ${requestId}: ${errorMessage}`);
|
||||||
return {
|
presenter.present({
|
||||||
requestId,
|
requestId,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errorMessage: 'Multiple faces detected. Please upload a photo with only your face.',
|
errorMessage: 'Multiple faces detected. Please upload a photo with only your face.',
|
||||||
};
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.logger.info(`Face validation successful for request ${requestId}.`);
|
this.logger.info(`Face validation successful for request ${requestId}.`);
|
||||||
|
|
||||||
@@ -119,11 +117,12 @@ export class RequestAvatarGenerationUseCase
|
|||||||
request.fail(errorMessage);
|
request.fail(errorMessage);
|
||||||
await this.avatarRepository.save(request);
|
await this.avatarRepository.save(request);
|
||||||
this.logger.error(`Avatar generation failed for request ${requestId}: ${errorMessage}`);
|
this.logger.error(`Avatar generation failed for request ${requestId}: ${errorMessage}`);
|
||||||
return {
|
presenter.present({
|
||||||
requestId,
|
requestId,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
errorMessage: generationResult.errorMessage || 'Failed to generate avatars. Please try again.',
|
errorMessage: generationResult.errorMessage || 'Failed to generate avatars. Please try again.',
|
||||||
};
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete with generated avatars
|
// Complete with generated avatars
|
||||||
@@ -132,18 +131,18 @@ export class RequestAvatarGenerationUseCase
|
|||||||
await this.avatarRepository.save(request);
|
await this.avatarRepository.save(request);
|
||||||
this.logger.info(`Avatar generation completed successfully for request ${requestId}.`);
|
this.logger.info(`Avatar generation completed successfully for request ${requestId}.`);
|
||||||
|
|
||||||
return {
|
presenter.present({
|
||||||
requestId,
|
requestId,
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
avatarUrls,
|
avatarUrls,
|
||||||
};
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`An unexpected error occurred during avatar generation for userId: ${command.userId}`,
|
`An unexpected error occurred during avatar generation for userId: ${command.userId}`,
|
||||||
error,
|
error as Error,
|
||||||
);
|
);
|
||||||
// Re-throw or return a generic error, depending on desired error handling strategy
|
// Re-throw or return a generic error, depending on desired error handling strategy
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
core/payments/application/index.ts
Normal file
2
core/payments/application/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './presenters';
|
||||||
|
export * from './use-cases';
|
||||||
16
core/payments/application/presenters/IAwardPrizePresenter.ts
Normal file
16
core/payments/application/presenters/IAwardPrizePresenter.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IAwardPrizePresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { PrizeDto } from './IGetPrizesPresenter';
|
||||||
|
|
||||||
|
export interface AwardPrizeResultDTO {
|
||||||
|
prize: PrizeDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AwardPrizeViewModel {
|
||||||
|
prize: PrizeDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAwardPrizePresenter extends Presenter<AwardPrizeResultDTO, AwardPrizeViewModel> {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: ICreatePaymentPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { PaymentDto } from './IGetPaymentsPresenter';
|
||||||
|
|
||||||
|
export interface CreatePaymentResultDTO {
|
||||||
|
payment: PaymentDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePaymentViewModel {
|
||||||
|
payment: PaymentDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICreatePaymentPresenter extends Presenter<CreatePaymentResultDTO, CreatePaymentViewModel> {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: ICreatePrizePresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { PrizeDto } from './IGetPrizesPresenter';
|
||||||
|
|
||||||
|
export interface CreatePrizeResultDTO {
|
||||||
|
prize: PrizeDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePrizeViewModel {
|
||||||
|
prize: PrizeDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICreatePrizePresenter extends Presenter<CreatePrizeResultDTO, CreatePrizeViewModel> {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IDeletePrizePresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
|
||||||
|
export interface DeletePrizeResultDTO {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeletePrizeViewModel {
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDeletePrizePresenter extends Presenter<DeletePrizeResultDTO, DeletePrizeViewModel> {}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IGetMembershipFeesPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { MembershipFeeType } from '../../domain/entities/MembershipFee';
|
||||||
|
import type { MemberPaymentStatus } from '../../domain/entities/MemberPayment';
|
||||||
|
|
||||||
|
export interface MembershipFeeDto {
|
||||||
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
|
seasonId?: string;
|
||||||
|
type: MembershipFeeType;
|
||||||
|
amount: number;
|
||||||
|
enabled: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemberPaymentDto {
|
||||||
|
id: string;
|
||||||
|
feeId: string;
|
||||||
|
driverId: string;
|
||||||
|
amount: number;
|
||||||
|
platformFee: number;
|
||||||
|
netAmount: number;
|
||||||
|
status: MemberPaymentStatus;
|
||||||
|
dueDate: Date;
|
||||||
|
paidAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetMembershipFeesResultDTO {
|
||||||
|
fee: MembershipFeeDto | null;
|
||||||
|
payments: MemberPaymentDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetMembershipFeesViewModel {
|
||||||
|
fee: MembershipFeeDto | null;
|
||||||
|
payments: MemberPaymentDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetMembershipFeesPresenter extends Presenter<GetMembershipFeesResultDTO, GetMembershipFeesViewModel> {}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IGetPaymentsPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { PaymentType, PayerType, PaymentStatus } from '../../domain/entities/Payment';
|
||||||
|
|
||||||
|
export interface PaymentDto {
|
||||||
|
id: string;
|
||||||
|
type: PaymentType;
|
||||||
|
amount: number;
|
||||||
|
platformFee: number;
|
||||||
|
netAmount: number;
|
||||||
|
payerId: string;
|
||||||
|
payerType: PayerType;
|
||||||
|
leagueId: string;
|
||||||
|
seasonId?: string;
|
||||||
|
status: PaymentStatus;
|
||||||
|
createdAt: Date;
|
||||||
|
completedAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPaymentsResultDTO {
|
||||||
|
payments: PaymentDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPaymentsViewModel {
|
||||||
|
payments: PaymentDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetPaymentsPresenter extends Presenter<GetPaymentsResultDTO, GetPaymentsViewModel> {}
|
||||||
31
core/payments/application/presenters/IGetPrizesPresenter.ts
Normal file
31
core/payments/application/presenters/IGetPrizesPresenter.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IGetPrizesPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { PrizeType } from '../../domain/entities/Prize';
|
||||||
|
|
||||||
|
export interface PrizeDto {
|
||||||
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
|
seasonId: string;
|
||||||
|
position: number;
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
type: PrizeType;
|
||||||
|
description?: string;
|
||||||
|
awarded: boolean;
|
||||||
|
awardedTo?: string;
|
||||||
|
awardedAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPrizesResultDTO {
|
||||||
|
prizes: PrizeDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPrizesViewModel {
|
||||||
|
prizes: PrizeDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetPrizesPresenter extends Presenter<GetPrizesResultDTO, GetPrizesViewModel> {}
|
||||||
40
core/payments/application/presenters/IGetWalletPresenter.ts
Normal file
40
core/payments/application/presenters/IGetWalletPresenter.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IGetWalletPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { TransactionType, ReferenceType } from '../../domain/entities/Wallet';
|
||||||
|
|
||||||
|
export interface WalletDto {
|
||||||
|
id: string;
|
||||||
|
leagueId: string;
|
||||||
|
balance: number;
|
||||||
|
totalRevenue: number;
|
||||||
|
totalPlatformFees: number;
|
||||||
|
totalWithdrawn: number;
|
||||||
|
currency: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionDto {
|
||||||
|
id: string;
|
||||||
|
walletId: string;
|
||||||
|
type: TransactionType;
|
||||||
|
amount: number;
|
||||||
|
description: string;
|
||||||
|
referenceId?: string;
|
||||||
|
referenceType?: ReferenceType;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetWalletResultDTO {
|
||||||
|
wallet: WalletDto;
|
||||||
|
transactions: TransactionDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetWalletViewModel {
|
||||||
|
wallet: WalletDto;
|
||||||
|
transactions: TransactionDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetWalletPresenter extends Presenter<GetWalletResultDTO, GetWalletViewModel> {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IProcessWalletTransactionPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { WalletDto, TransactionDto } from './IGetWalletPresenter';
|
||||||
|
|
||||||
|
export interface ProcessWalletTransactionResultDTO {
|
||||||
|
wallet: WalletDto;
|
||||||
|
transaction: TransactionDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessWalletTransactionViewModel {
|
||||||
|
wallet: WalletDto;
|
||||||
|
transaction: TransactionDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProcessWalletTransactionPresenter extends Presenter<ProcessWalletTransactionResultDTO, ProcessWalletTransactionViewModel> {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IUpdateMemberPaymentPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { MemberPaymentDto } from './IGetMembershipFeesPresenter';
|
||||||
|
|
||||||
|
export interface UpdateMemberPaymentResultDTO {
|
||||||
|
payment: MemberPaymentDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateMemberPaymentViewModel {
|
||||||
|
payment: MemberPaymentDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUpdateMemberPaymentPresenter extends Presenter<UpdateMemberPaymentResultDTO, UpdateMemberPaymentViewModel> {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IUpdatePaymentStatusPresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { PaymentDto } from './IGetPaymentsPresenter';
|
||||||
|
|
||||||
|
export interface UpdatePaymentStatusResultDTO {
|
||||||
|
payment: PaymentDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePaymentStatusViewModel {
|
||||||
|
payment: PaymentDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUpdatePaymentStatusPresenter extends Presenter<UpdatePaymentStatusResultDTO, UpdatePaymentStatusViewModel> {}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Presenter Interface: IUpsertMembershipFeePresenter
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||||
|
import type { MembershipFeeDto } from './IGetMembershipFeesPresenter';
|
||||||
|
|
||||||
|
export interface UpsertMembershipFeeResultDTO {
|
||||||
|
fee: MembershipFeeDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertMembershipFeeViewModel {
|
||||||
|
fee: MembershipFeeDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUpsertMembershipFeePresenter extends Presenter<UpsertMembershipFeeResultDTO, UpsertMembershipFeeViewModel> {}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user