fix data flow issues

This commit is contained in:
2025-12-19 21:58:03 +01:00
parent 94fc538f44
commit ec177a75ce
37 changed files with 1336 additions and 534 deletions

View File

@@ -6,6 +6,7 @@ import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepos
import { IRankingService } from '@core/racing/domain/services/IRankingService';
import { IDriverStatsService } from '@core/racing/domain/services/IDriverStatsService';
import { DriverRatingProvider } from '@core/racing/application/ports/DriverRatingProvider';
import { DriverExtendedProfileProvider } from '@core/racing/application/ports/DriverExtendedProfileProvider';
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
import { INotificationPreferenceRepository } from '@core/notifications/domain/repositories/INotificationPreferenceRepository';
@@ -24,6 +25,7 @@ import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/
import { InMemoryRankingService } from '@adapters/racing/services/InMemoryRankingService';
import { InMemoryDriverStatsService } from '@adapters/racing/services/InMemoryDriverStatsService';
import { InMemoryDriverRatingProvider } from '@adapters/racing/ports/InMemoryDriverRatingProvider';
import { InMemoryDriverExtendedProfileProvider } from '@adapters/racing/ports/InMemoryDriverExtendedProfileProvider';
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
import { InMemoryNotificationPreferenceRepository } from '@adapters/notifications/persistence/inmemory/InMemoryNotificationPreferenceRepository';
@@ -34,6 +36,7 @@ export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const RANKING_SERVICE_TOKEN = 'IRankingService';
export const DRIVER_STATS_SERVICE_TOKEN = 'IDriverStatsService';
export const DRIVER_RATING_PROVIDER_TOKEN = 'DriverRatingProvider';
export const DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN = 'DriverExtendedProfileProvider';
export const IMAGE_SERVICE_PORT_TOKEN = 'IImageServicePort';
export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
export const NOTIFICATION_PREFERENCE_REPOSITORY_TOKEN = 'INotificationPreferenceRepository';
@@ -69,6 +72,11 @@ export const DriverProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryDriverRatingProvider(logger),
inject: [LOGGER_TOKEN],
},
{
provide: DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN,
useFactory: (logger: Logger) => new InMemoryDriverExtendedProfileProvider(logger),
inject: [LOGGER_TOKEN],
},
{
provide: IMAGE_SERVICE_PORT_TOKEN,
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
@@ -117,7 +125,7 @@ export const DriverProviders: Provider[] = [
},
{
provide: GET_PROFILE_OVERVIEW_USE_CASE_TOKEN,
useFactory: (driverRepo: IDriverRepository, imageService: IImageServicePort, logger: Logger) =>
useFactory: (driverRepo: IDriverRepository, imageService: IImageServicePort, driverExtendedProfileProvider: DriverExtendedProfileProvider, logger: Logger) =>
new GetProfileOverviewUseCase(
driverRepo,
// TODO: Add teamRepository, teamMembershipRepository, socialRepository, etc.
@@ -125,9 +133,10 @@ export const DriverProviders: Provider[] = [
null as any, // teamMembershipRepository
null as any, // socialRepository
imageService,
driverExtendedProfileProvider,
() => null, // getDriverStats
() => [], // getAllDriverRankings
),
inject: [DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_PORT_TOKEN, LOGGER_TOKEN],
inject: [DRIVER_REPOSITORY_TOKEN, IMAGE_SERVICE_PORT_TOKEN, DRIVER_EXTENDED_PROFILE_PROVIDER_TOKEN, LOGGER_TOKEN],
},
];

View File

@@ -29,6 +29,9 @@ import { GetLeagueOwnerSummaryQueryDTO } from './dtos/GetLeagueOwnerSummaryQuery
import { GetLeagueAdminConfigQueryDTO } from './dtos/GetLeagueAdminConfigQueryDTO';
import { GetLeagueProtestsQueryDTO } from './dtos/GetLeagueProtestsQueryDTO';
import { GetLeagueSeasonsQueryDTO } from './dtos/GetLeagueSeasonsQueryDTO';
import { GetLeagueWalletOutputDTO } from './dtos/GetLeagueWalletOutputDTO';
import { WithdrawFromLeagueWalletInputDTO } from './dtos/WithdrawFromLeagueWalletInputDTO';
import { WithdrawFromLeagueWalletOutputDTO } from './dtos/WithdrawFromLeagueWalletOutputDTO';
@ApiTags('leagues')
@Controller('leagues')
@@ -274,4 +277,22 @@ export class LeagueController {
async getRaces(@Param('leagueId') leagueId: string): Promise<GetLeagueRacesOutputDTO> {
return this.leagueService.getRaces(leagueId);
}
@Get(':leagueId/wallet')
@ApiOperation({ summary: 'Get league wallet information' })
@ApiResponse({ status: 200, description: 'League wallet data', type: GetLeagueWalletOutputDTO })
async getLeagueWallet(@Param('leagueId') leagueId: string): Promise<GetLeagueWalletOutputDTO> {
return this.leagueService.getLeagueWallet(leagueId);
}
@Post(':leagueId/wallet/withdraw')
@ApiOperation({ summary: 'Withdraw from league wallet' })
@ApiBody({ type: WithdrawFromLeagueWalletInputDTO })
@ApiResponse({ status: 200, description: 'Withdrawal processed', type: WithdrawFromLeagueWalletOutputDTO })
async withdrawFromLeagueWallet(
@Param('leagueId') leagueId: string,
@Body() input: WithdrawFromLeagueWalletInputDTO,
): Promise<WithdrawFromLeagueWalletOutputDTO> {
return this.leagueService.withdrawFromLeagueWallet(leagueId, input);
}
}

View File

@@ -15,6 +15,8 @@ import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory
import { InMemoryRaceRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRepository';
import { InMemoryDriverRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverRepository';
import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemory/InMemoryStandingRepository';
import { InMemoryLeagueWalletRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository';
import { InMemoryTransactionRepository } from '@adapters/racing/persistence/inmemory/InMemoryTransactionRepository';
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
import { listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
@@ -38,6 +40,8 @@ import { GetLeagueMembershipsUseCase } from '@core/racing/application/use-cases/
import { GetLeagueScheduleUseCase } from '@core/racing/application/use-cases/GetLeagueScheduleUseCase';
import { GetLeagueStatsUseCase } from '@core/racing/application/use-cases/GetLeagueStatsUseCase';
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
// Define injection tokens
export const LEAGUE_REPOSITORY_TOKEN = 'ILeagueRepository';
@@ -50,6 +54,8 @@ export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
export const RACE_REPOSITORY_TOKEN = 'IRaceRepository';
export const DRIVER_REPOSITORY_TOKEN = 'IDriverRepository';
export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
export const LOGGER_TOKEN = 'Logger'; // Already defined in AuthProviders, but good to have here too
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
@@ -105,6 +111,16 @@ export const LeagueProviders: Provider[] = [
useFactory: (logger: Logger) => new InMemoryDriverRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LEAGUE_WALLET_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryLeagueWalletRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: TRANSACTION_REPOSITORY_TOKEN,
useFactory: (logger: Logger) => new InMemoryTransactionRepository(logger),
inject: [LOGGER_TOKEN],
},
{
provide: LOGGER_TOKEN,
useClass: ConsoleLogger,
@@ -132,6 +148,8 @@ export const LeagueProviders: Provider[] = [
GetLeagueScheduleUseCase,
GetLeagueStatsUseCase,
GetLeagueAdminPermissionsUseCase,
GetLeagueWalletUseCase,
WithdrawFromLeagueWalletUseCase,
{
provide: ListLeagueScoringPresetsUseCase,
useFactory: () => new ListLeagueScoringPresetsUseCase(listLeagueScoringPresets()),

View File

@@ -58,6 +58,8 @@ import { RejectLeagueJoinRequestUseCase } from '@core/racing/application/use-cas
import { RemoveLeagueMemberUseCase } from '@core/racing/application/use-cases/RemoveLeagueMemberUseCase';
import { TransferLeagueOwnershipUseCase } from '@core/racing/application/use-cases/TransferLeagueOwnershipUseCase';
import { UpdateLeagueMemberRoleUseCase } from '@core/racing/application/use-cases/UpdateLeagueMemberRoleUseCase';
import { GetLeagueWalletUseCase } from '@core/racing/application/use-cases/GetLeagueWalletUseCase';
import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-cases/WithdrawFromLeagueWalletUseCase';
// API Presenters
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
@@ -108,6 +110,8 @@ export class LeagueService {
private readonly getLeagueMembershipsUseCase: GetLeagueMembershipsUseCase,
private readonly getLeagueScheduleUseCase: GetLeagueScheduleUseCase,
private readonly getLeagueAdminPermissionsUseCase: GetLeagueAdminPermissionsUseCase,
private readonly getLeagueWalletUseCase: GetLeagueWalletUseCase,
private readonly withdrawFromLeagueWalletUseCase: WithdrawFromLeagueWalletUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
@@ -438,4 +442,32 @@ export class LeagueService {
races: [],
};
}
async getLeagueWallet(leagueId: string): Promise<GetLeagueWalletOutputDTO> {
this.logger.debug('Getting league wallet', { leagueId });
const result = await this.getLeagueWalletUseCase.execute({ leagueId });
if (result.isErr()) {
throw new Error(result.unwrapErr().message);
}
return result.unwrap();
}
async withdrawFromLeagueWallet(leagueId: string, input: WithdrawFromLeagueWalletInputDTO): Promise<WithdrawFromLeagueWalletOutputDTO> {
this.logger.debug('Withdrawing from league wallet', { leagueId, amount: input.amount });
const result = await this.withdrawFromLeagueWalletUseCase.execute({
leagueId,
amount: input.amount,
currency: input.currency,
seasonId: input.seasonId,
destinationAccount: input.destinationAccount,
});
if (result.isErr()) {
const error = result.unwrapErr();
if (error.code === 'WITHDRAWAL_NOT_ALLOWED') {
return { success: false, message: error.message };
}
throw new Error(error.message);
}
return result.unwrap();
}
}

View File

@@ -0,0 +1,59 @@
import { ApiProperty } from '@nestjs/swagger';
export class WalletTransactionDTO {
@ApiProperty()
id: string;
@ApiProperty({ enum: ['sponsorship', 'membership', 'withdrawal', 'prize'] })
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
@ApiProperty()
description: string;
@ApiProperty()
amount: number;
@ApiProperty()
fee: number;
@ApiProperty()
netAmount: number;
@ApiProperty()
date: string;
@ApiProperty({ enum: ['completed', 'pending', 'failed'] })
status: 'completed' | 'pending' | 'failed';
@ApiProperty({ required: false })
reference?: string;
}
export class GetLeagueWalletOutputDTO {
@ApiProperty()
balance: number;
@ApiProperty()
currency: string;
@ApiProperty()
totalRevenue: number;
@ApiProperty()
totalFees: number;
@ApiProperty()
totalWithdrawals: number;
@ApiProperty()
pendingPayouts: number;
@ApiProperty()
canWithdraw: boolean;
@ApiProperty({ required: false })
withdrawalBlockReason?: string;
@ApiProperty({ type: [WalletTransactionDTO] })
transactions: WalletTransactionDTO[];
}

View File

@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
export class WithdrawFromLeagueWalletInputDTO {
@ApiProperty()
amount: number;
@ApiProperty()
currency: string;
@ApiProperty()
seasonId: string;
@ApiProperty()
destinationAccount: string;
}

View File

@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
export class WithdrawFromLeagueWalletOutputDTO {
@ApiProperty()
success: boolean;
@ApiProperty({ required: false })
message?: string;
}