fix data flow issues
This commit is contained in:
54
apps/website/lib/api/wallets/WalletsApiClient.ts
Normal file
54
apps/website/lib/api/wallets/WalletsApiClient.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
|
||||
export interface LeagueWalletDTO {
|
||||
balance: number;
|
||||
currency: string;
|
||||
totalRevenue: number;
|
||||
totalFees: number;
|
||||
totalWithdrawals: number;
|
||||
pendingPayouts: number;
|
||||
canWithdraw: boolean;
|
||||
withdrawalBlockReason?: string;
|
||||
transactions: WalletTransactionDTO[];
|
||||
}
|
||||
|
||||
export interface WalletTransactionDTO {
|
||||
id: string;
|
||||
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
|
||||
description: string;
|
||||
amount: number;
|
||||
fee: number;
|
||||
netAmount: number;
|
||||
date: string; // ISO string
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
reference?: string;
|
||||
}
|
||||
|
||||
export interface WithdrawRequestDTO {
|
||||
amount: number;
|
||||
currency: string;
|
||||
seasonId: string;
|
||||
destinationAccount: string;
|
||||
}
|
||||
|
||||
export interface WithdrawResponseDTO {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wallets API Client
|
||||
*
|
||||
* Handles all wallet-related API operations.
|
||||
*/
|
||||
export class WalletsApiClient extends BaseApiClient {
|
||||
/** Get league wallet */
|
||||
getLeagueWallet(leagueId: string): Promise<LeagueWalletDTO> {
|
||||
return this.get<LeagueWalletDTO>(`/leagues/${leagueId}/wallet`);
|
||||
}
|
||||
|
||||
/** Withdraw from league wallet */
|
||||
withdrawFromLeagueWallet(leagueId: string, request: WithdrawRequestDTO): Promise<WithdrawResponseDTO> {
|
||||
return this.post<WithdrawResponseDTO>(`/leagues/${leagueId}/wallet/withdraw`, request);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { TeamsApiClient } from '../api/teams/TeamsApiClient';
|
||||
import { LeaguesApiClient } from '../api/leagues/LeaguesApiClient';
|
||||
import { SponsorsApiClient } from '../api/sponsors/SponsorsApiClient';
|
||||
import { PaymentsApiClient } from '../api/payments/PaymentsApiClient';
|
||||
import { WalletsApiClient } from '../api/wallets/WalletsApiClient';
|
||||
import { AuthApiClient } from '../api/auth/AuthApiClient';
|
||||
import { AnalyticsApiClient } from '../api/analytics/AnalyticsApiClient';
|
||||
import { MediaApiClient } from '../api/media/MediaApiClient';
|
||||
@@ -17,6 +18,7 @@ import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger';
|
||||
// Services
|
||||
import { RaceService } from './races/RaceService';
|
||||
import { RaceResultsService } from './races/RaceResultsService';
|
||||
import { RaceStewardingService } from './races/RaceStewardingService';
|
||||
import { DriverService } from './drivers/DriverService';
|
||||
import { DriverRegistrationService } from './drivers/DriverRegistrationService';
|
||||
import { TeamService } from './teams/TeamService';
|
||||
@@ -24,6 +26,8 @@ import { TeamJoinService } from './teams/TeamJoinService';
|
||||
import { LeagueService } from './leagues/LeagueService';
|
||||
import { LeagueMembershipService } from './leagues/LeagueMembershipService';
|
||||
import { LeagueSettingsService } from './leagues/LeagueSettingsService';
|
||||
import { LeagueStewardingService } from './leagues/LeagueStewardingService';
|
||||
import { LeagueWalletService } from './leagues/LeagueWalletService';
|
||||
import { SponsorService } from './sponsors/SponsorService';
|
||||
import { SponsorshipService } from './sponsors/SponsorshipService';
|
||||
import { PaymentService } from './payments/PaymentService';
|
||||
@@ -56,6 +60,7 @@ export class ServiceFactory {
|
||||
leagues: LeaguesApiClient;
|
||||
sponsors: SponsorsApiClient;
|
||||
payments: PaymentsApiClient;
|
||||
wallets: WalletsApiClient;
|
||||
auth: AuthApiClient;
|
||||
analytics: AnalyticsApiClient;
|
||||
media: MediaApiClient;
|
||||
@@ -73,6 +78,7 @@ export class ServiceFactory {
|
||||
leagues: new LeaguesApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
sponsors: new SponsorsApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
payments: new PaymentsApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
wallets: new WalletsApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
auth: new AuthApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
analytics: new AnalyticsApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
media: new MediaApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
@@ -96,6 +102,17 @@ export class ServiceFactory {
|
||||
return new RaceResultsService(this.apiClients.races);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create RaceStewardingService instance
|
||||
*/
|
||||
createRaceStewardingService(): RaceStewardingService {
|
||||
return new RaceStewardingService(
|
||||
this.apiClients.races,
|
||||
this.apiClients.protests,
|
||||
this.apiClients.penalties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DriverService instance
|
||||
*/
|
||||
@@ -145,6 +162,26 @@ export class ServiceFactory {
|
||||
return new LeagueSettingsService(this.apiClients.leagues, this.apiClients.drivers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create LeagueStewardingService instance
|
||||
*/
|
||||
createLeagueStewardingService(): LeagueStewardingService {
|
||||
return new LeagueStewardingService(
|
||||
this.createRaceService(),
|
||||
this.createProtestService(),
|
||||
this.createPenaltyService(),
|
||||
this.createDriverService(),
|
||||
this.createLeagueMembershipService()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create LeagueWalletService instance
|
||||
*/
|
||||
createLeagueWalletService(): LeagueWalletService {
|
||||
return new LeagueWalletService(this.apiClients.wallets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SponsorService instance
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ServiceFactory } from './ServiceFactory';
|
||||
// Import all service types
|
||||
import { RaceService } from './races/RaceService';
|
||||
import { RaceResultsService } from './races/RaceResultsService';
|
||||
import { RaceStewardingService } from './races/RaceStewardingService';
|
||||
import { DriverService } from './drivers/DriverService';
|
||||
import { DriverRegistrationService } from './drivers/DriverRegistrationService';
|
||||
import { TeamService } from './teams/TeamService';
|
||||
@@ -13,6 +14,8 @@ import { TeamJoinService } from './teams/TeamJoinService';
|
||||
import { LeagueService } from './leagues/LeagueService';
|
||||
import { LeagueMembershipService } from './leagues/LeagueMembershipService';
|
||||
import { LeagueSettingsService } from './leagues/LeagueSettingsService';
|
||||
import { LeagueStewardingService } from './leagues/LeagueStewardingService';
|
||||
import { LeagueWalletService } from './leagues/LeagueWalletService';
|
||||
import { SponsorService } from './sponsors/SponsorService';
|
||||
import { SponsorshipService } from './sponsors/SponsorshipService';
|
||||
import { PaymentService } from './payments/PaymentService';
|
||||
@@ -30,6 +33,7 @@ import { PenaltyService } from './penalties/PenaltyService';
|
||||
export interface Services {
|
||||
raceService: RaceService;
|
||||
raceResultsService: RaceResultsService;
|
||||
raceStewardingService: RaceStewardingService;
|
||||
driverService: DriverService;
|
||||
driverRegistrationService: DriverRegistrationService;
|
||||
teamService: TeamService;
|
||||
@@ -37,6 +41,8 @@ export interface Services {
|
||||
leagueService: LeagueService;
|
||||
leagueMembershipService: LeagueMembershipService;
|
||||
leagueSettingsService: LeagueSettingsService;
|
||||
leagueStewardingService: LeagueStewardingService;
|
||||
leagueWalletService: LeagueWalletService;
|
||||
sponsorService: SponsorService;
|
||||
sponsorshipService: SponsorshipService;
|
||||
paymentService: PaymentService;
|
||||
@@ -65,6 +71,7 @@ export function ServiceProvider({ children }: ServiceProviderProps) {
|
||||
return {
|
||||
raceService: serviceFactory.createRaceService(),
|
||||
raceResultsService: serviceFactory.createRaceResultsService(),
|
||||
raceStewardingService: serviceFactory.createRaceStewardingService(),
|
||||
driverService: serviceFactory.createDriverService(),
|
||||
driverRegistrationService: serviceFactory.createDriverRegistrationService(),
|
||||
teamService: serviceFactory.createTeamService(),
|
||||
@@ -72,6 +79,8 @@ export function ServiceProvider({ children }: ServiceProviderProps) {
|
||||
leagueService: serviceFactory.createLeagueService(),
|
||||
leagueMembershipService: serviceFactory.createLeagueMembershipService(),
|
||||
leagueSettingsService: serviceFactory.createLeagueSettingsService(),
|
||||
leagueStewardingService: serviceFactory.createLeagueStewardingService(),
|
||||
leagueWalletService: serviceFactory.createLeagueWalletService(),
|
||||
sponsorService: serviceFactory.createSponsorService(),
|
||||
sponsorshipService: serviceFactory.createSponsorshipService(),
|
||||
paymentService: serviceFactory.createPaymentService(),
|
||||
|
||||
93
apps/website/lib/services/leagues/LeagueStewardingService.ts
Normal file
93
apps/website/lib/services/leagues/LeagueStewardingService.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { RaceService } from '../races/RaceService';
|
||||
import { ProtestService } from '../protests/ProtestService';
|
||||
import { PenaltyService } from '../penalties/PenaltyService';
|
||||
import { DriverService } from '../drivers/DriverService';
|
||||
import { LeagueMembershipService } from './LeagueMembershipService';
|
||||
import { LeagueStewardingViewModel, RaceWithProtests } from '../../view-models/LeagueStewardingViewModel';
|
||||
|
||||
/**
|
||||
* League Stewarding Service
|
||||
*
|
||||
* Orchestrates league stewarding operations by coordinating calls to race, protest, penalty, driver, and membership services.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class LeagueStewardingService {
|
||||
constructor(
|
||||
private readonly raceService: RaceService,
|
||||
private readonly protestService: ProtestService,
|
||||
private readonly penaltyService: PenaltyService,
|
||||
private readonly driverService: DriverService,
|
||||
private readonly leagueMembershipService: LeagueMembershipService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get league stewarding data for all races in a league
|
||||
*/
|
||||
async getLeagueStewardingData(leagueId: string): Promise<LeagueStewardingViewModel> {
|
||||
// Get all races for this league
|
||||
const leagueRaces = await this.raceService.findByLeagueId(leagueId);
|
||||
|
||||
// Get protests and penalties for each race
|
||||
const protestsMap: Record<string, any[]> = {};
|
||||
const penaltiesMap: Record<string, any[]> = {};
|
||||
const driverIds = new Set<string>();
|
||||
|
||||
for (const race of leagueRaces) {
|
||||
const raceProtests = await this.protestService.findByRaceId(race.id);
|
||||
const racePenalties = await this.penaltyService.findByRaceId(race.id);
|
||||
|
||||
protestsMap[race.id] = raceProtests;
|
||||
penaltiesMap[race.id] = racePenalties;
|
||||
|
||||
// Collect driver IDs
|
||||
raceProtests.forEach((p: any) => {
|
||||
driverIds.add(p.protestingDriverId);
|
||||
driverIds.add(p.accusedDriverId);
|
||||
});
|
||||
racePenalties.forEach((p: any) => {
|
||||
driverIds.add(p.driverId);
|
||||
});
|
||||
}
|
||||
|
||||
// Load driver info
|
||||
const driverEntities = await this.driverService.findByIds(Array.from(driverIds));
|
||||
const driverMap: Record<string, any> = {};
|
||||
driverEntities.forEach((driver) => {
|
||||
if (driver) {
|
||||
driverMap[driver.id] = driver;
|
||||
}
|
||||
});
|
||||
|
||||
// Compute race data with protest/penalty info
|
||||
const racesWithData: RaceWithProtests[] = leagueRaces.map(race => {
|
||||
const protests = protestsMap[race.id] || [];
|
||||
const penalties = penaltiesMap[race.id] || [];
|
||||
return {
|
||||
race: {
|
||||
id: race.id,
|
||||
track: race.track,
|
||||
scheduledAt: new Date(race.scheduledAt),
|
||||
},
|
||||
pendingProtests: protests.filter(p => p.status === 'pending' || p.status === 'under_review'),
|
||||
resolvedProtests: protests.filter(p => p.status === 'upheld' || p.status === 'dismissed' || p.status === 'withdrawn'),
|
||||
penalties
|
||||
};
|
||||
}).sort((a, b) => b.race.scheduledAt.getTime() - a.race.scheduledAt.getTime());
|
||||
|
||||
return new LeagueStewardingViewModel(racesWithData, driverMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Review a protest
|
||||
*/
|
||||
async reviewProtest(input: { protestId: string; stewardId: string; decision: string; decisionNotes: string }): Promise<void> {
|
||||
await this.protestService.reviewProtest(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a penalty
|
||||
*/
|
||||
async applyPenalty(input: any): Promise<void> {
|
||||
await this.penaltyService.applyPenalty(input);
|
||||
}
|
||||
}
|
||||
71
apps/website/lib/services/leagues/LeagueWalletService.ts
Normal file
71
apps/website/lib/services/leagues/LeagueWalletService.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { WalletsApiClient, LeagueWalletDTO, WithdrawRequestDTO, WithdrawResponseDTO } from '@/lib/api/wallets/WalletsApiClient';
|
||||
import { LeagueWalletViewModel } from '@/lib/view-models/LeagueWalletViewModel';
|
||||
import { WalletTransactionViewModel } from '@/lib/view-models/WalletTransactionViewModel';
|
||||
import { SubmitBlocker, ThrottleBlocker } from '@/lib/blockers';
|
||||
|
||||
/**
|
||||
* League Wallet Service
|
||||
*
|
||||
* Orchestrates league wallet operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class LeagueWalletService {
|
||||
private readonly submitBlocker = new SubmitBlocker();
|
||||
private readonly throttle = new ThrottleBlocker(500);
|
||||
|
||||
constructor(
|
||||
private readonly apiClient: WalletsApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get wallet for a league
|
||||
*/
|
||||
async getWalletForLeague(leagueId: string): Promise<LeagueWalletViewModel> {
|
||||
const dto = await this.apiClient.getLeagueWallet(leagueId);
|
||||
const transactions = dto.transactions.map(t => new WalletTransactionViewModel({
|
||||
id: t.id,
|
||||
type: t.type,
|
||||
description: t.description,
|
||||
amount: t.amount,
|
||||
fee: t.fee,
|
||||
netAmount: t.netAmount,
|
||||
date: new Date(t.date),
|
||||
status: t.status,
|
||||
reference: t.reference,
|
||||
}));
|
||||
return new LeagueWalletViewModel({
|
||||
balance: dto.balance,
|
||||
currency: dto.currency,
|
||||
totalRevenue: dto.totalRevenue,
|
||||
totalFees: dto.totalFees,
|
||||
totalWithdrawals: dto.totalWithdrawals,
|
||||
pendingPayouts: dto.pendingPayouts,
|
||||
transactions,
|
||||
canWithdraw: dto.canWithdraw,
|
||||
withdrawalBlockReason: dto.withdrawalBlockReason,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw from league wallet
|
||||
*/
|
||||
async withdraw(leagueId: string, amount: number, currency: string, seasonId: string, destinationAccount: string): Promise<WithdrawResponseDTO> {
|
||||
if (!this.submitBlocker.canExecute() || !this.throttle.canExecute()) {
|
||||
throw new Error('Request blocked due to rate limiting');
|
||||
}
|
||||
|
||||
this.submitBlocker.block();
|
||||
this.throttle.block();
|
||||
try {
|
||||
const request: WithdrawRequestDTO = {
|
||||
amount,
|
||||
currency,
|
||||
seasonId,
|
||||
destinationAccount,
|
||||
};
|
||||
return await this.apiClient.withdrawFromLeagueWallet(leagueId, request);
|
||||
} finally {
|
||||
this.submitBlocker.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,16 @@ export class RaceService {
|
||||
*/
|
||||
async getRacesPageData(): Promise<RacesPageViewModel> {
|
||||
const dto = await this.apiClient.getPageData();
|
||||
return new RacesPageViewModel(this.transformRacesPageData(dto));
|
||||
return new RacesPageViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all races page data with view model transformation
|
||||
* Currently same as getRacesPageData, but can be extended for different filtering
|
||||
*/
|
||||
async getAllRacesPageData(): Promise<RacesPageViewModel> {
|
||||
const dto = await this.apiClient.getPageData();
|
||||
return new RacesPageViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,38 +91,6 @@ export class RaceService {
|
||||
await this.apiClient.complete(raceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API races page data to view model format
|
||||
*/
|
||||
private transformRacesPageData(dto: RacesPageDataDto): {
|
||||
upcomingRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
completedRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
|
||||
totalCount: number;
|
||||
} {
|
||||
const upcomingRaces = dto.races
|
||||
.filter(race => race.status !== 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
const completedRaces = dto.races
|
||||
.filter(race => race.status === 'completed')
|
||||
.map(race => ({
|
||||
id: race.id,
|
||||
title: `${race.track} - ${race.car}`,
|
||||
scheduledTime: race.scheduledAt,
|
||||
status: race.status,
|
||||
}));
|
||||
|
||||
return {
|
||||
upcomingRaces,
|
||||
completedRaces,
|
||||
totalCount: dto.races.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find races by league ID
|
||||
|
||||
36
apps/website/lib/services/races/RaceStewardingService.ts
Normal file
36
apps/website/lib/services/races/RaceStewardingService.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { RacesApiClient } from '../../api/races/RacesApiClient';
|
||||
import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient';
|
||||
import { PenaltiesApiClient } from '../../api/penalties/PenaltiesApiClient';
|
||||
import { RaceStewardingViewModel } from '../../view-models/RaceStewardingViewModel';
|
||||
|
||||
/**
|
||||
* Race Stewarding Service
|
||||
*
|
||||
* Orchestrates race stewarding operations by coordinating API calls for race details,
|
||||
* protests, and penalties, and returning a unified view model.
|
||||
*/
|
||||
export class RaceStewardingService {
|
||||
constructor(
|
||||
private readonly racesApiClient: RacesApiClient,
|
||||
private readonly protestsApiClient: ProtestsApiClient,
|
||||
private readonly penaltiesApiClient: PenaltiesApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get race stewarding data with view model transformation
|
||||
*/
|
||||
async getRaceStewardingData(raceId: string, driverId: string): Promise<RaceStewardingViewModel> {
|
||||
// Fetch all data in parallel
|
||||
const [raceDetail, protests, penalties] = await Promise.all([
|
||||
this.racesApiClient.getDetail(raceId, driverId),
|
||||
this.protestsApiClient.getRaceProtests(raceId),
|
||||
this.penaltiesApiClient.getRacePenalties(raceId),
|
||||
]);
|
||||
|
||||
return new RaceStewardingViewModel({
|
||||
raceDetail,
|
||||
protests,
|
||||
penalties,
|
||||
});
|
||||
}
|
||||
}
|
||||
74
apps/website/lib/view-models/LeagueStewardingViewModel.ts
Normal file
74
apps/website/lib/view-models/LeagueStewardingViewModel.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* League Stewarding View Model
|
||||
* Represents all data needed for league stewarding across all races
|
||||
*/
|
||||
export class LeagueStewardingViewModel {
|
||||
constructor(
|
||||
public readonly racesWithData: RaceWithProtests[],
|
||||
public readonly driverMap: Record<string, { id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number }>
|
||||
) {}
|
||||
|
||||
/** UI-specific: Total pending protests count */
|
||||
get totalPending(): number {
|
||||
return this.racesWithData.reduce((sum, r) => sum + r.pendingProtests.length, 0);
|
||||
}
|
||||
|
||||
/** UI-specific: Total resolved protests count */
|
||||
get totalResolved(): number {
|
||||
return this.racesWithData.reduce((sum, r) => sum + r.resolvedProtests.length, 0);
|
||||
}
|
||||
|
||||
/** UI-specific: Total penalties count */
|
||||
get totalPenalties(): number {
|
||||
return this.racesWithData.reduce((sum, r) => sum + r.penalties.length, 0);
|
||||
}
|
||||
|
||||
/** UI-specific: Filtered races for pending tab */
|
||||
get pendingRaces(): RaceWithProtests[] {
|
||||
return this.racesWithData.filter(r => r.pendingProtests.length > 0);
|
||||
}
|
||||
|
||||
/** UI-specific: Filtered races for history tab */
|
||||
get historyRaces(): RaceWithProtests[] {
|
||||
return this.racesWithData.filter(r => r.resolvedProtests.length > 0 || r.penalties.length > 0);
|
||||
}
|
||||
|
||||
/** UI-specific: All drivers for quick penalty modal */
|
||||
get allDrivers(): Array<{ id: string; name: string; avatarUrl?: string; iracingId?: string; rating?: number }> {
|
||||
return Object.values(this.driverMap);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RaceWithProtests {
|
||||
race: {
|
||||
id: string;
|
||||
track: string;
|
||||
scheduledAt: Date;
|
||||
};
|
||||
pendingProtests: Protest[];
|
||||
resolvedProtests: Protest[];
|
||||
penalties: Penalty[];
|
||||
}
|
||||
|
||||
export interface Protest {
|
||||
id: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
incident: {
|
||||
lap: number;
|
||||
description: string;
|
||||
};
|
||||
filedAt: string;
|
||||
status: string;
|
||||
decisionNotes?: string;
|
||||
proofVideoUrl?: string;
|
||||
}
|
||||
|
||||
export interface Penalty {
|
||||
id: string;
|
||||
driverId: string;
|
||||
type: string;
|
||||
value: number;
|
||||
reason: string;
|
||||
notes?: string;
|
||||
}
|
||||
60
apps/website/lib/view-models/LeagueWalletViewModel.ts
Normal file
60
apps/website/lib/view-models/LeagueWalletViewModel.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { WalletTransactionViewModel } from './WalletTransactionViewModel';
|
||||
|
||||
export class LeagueWalletViewModel {
|
||||
balance: number;
|
||||
currency: string;
|
||||
totalRevenue: number;
|
||||
totalFees: number;
|
||||
totalWithdrawals: number;
|
||||
pendingPayouts: number;
|
||||
transactions: WalletTransactionViewModel[];
|
||||
canWithdraw: boolean;
|
||||
withdrawalBlockReason?: string;
|
||||
|
||||
constructor(dto: {
|
||||
balance: number;
|
||||
currency: string;
|
||||
totalRevenue: number;
|
||||
totalFees: number;
|
||||
totalWithdrawals: number;
|
||||
pendingPayouts: number;
|
||||
transactions: WalletTransactionViewModel[];
|
||||
canWithdraw: boolean;
|
||||
withdrawalBlockReason?: string;
|
||||
}) {
|
||||
this.balance = dto.balance;
|
||||
this.currency = dto.currency;
|
||||
this.totalRevenue = dto.totalRevenue;
|
||||
this.totalFees = dto.totalFees;
|
||||
this.totalWithdrawals = dto.totalWithdrawals;
|
||||
this.pendingPayouts = dto.pendingPayouts;
|
||||
this.transactions = dto.transactions;
|
||||
this.canWithdraw = dto.canWithdraw;
|
||||
this.withdrawalBlockReason = dto.withdrawalBlockReason;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted balance */
|
||||
get formattedBalance(): string {
|
||||
return `$${this.balance.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted total revenue */
|
||||
get formattedTotalRevenue(): string {
|
||||
return `$${this.totalRevenue.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted total fees */
|
||||
get formattedTotalFees(): string {
|
||||
return `$${this.totalFees.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted pending payouts */
|
||||
get formattedPendingPayouts(): string {
|
||||
return `$${this.pendingPayouts.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Filtered transactions by type */
|
||||
getFilteredTransactions(type: 'all' | 'sponsorship' | 'membership' | 'withdrawal' | 'prize'): WalletTransactionViewModel[] {
|
||||
return type === 'all' ? this.transactions : this.transactions.filter(t => t.type === type);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,65 @@
|
||||
// Note: No generated DTO available for RaceListItem yet
|
||||
// DTO matching the backend RacesPageDataRaceDTO
|
||||
interface RaceListItemDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
scheduledTime: string;
|
||||
status: string;
|
||||
trackName?: string;
|
||||
strengthOfField: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
}
|
||||
|
||||
export class RaceListItemViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
scheduledTime: string;
|
||||
status: string;
|
||||
trackName?: string;
|
||||
strengthOfField: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
|
||||
constructor(dto: RaceListItemDTO) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.track = dto.track;
|
||||
this.car = dto.car;
|
||||
this.scheduledAt = dto.scheduledAt;
|
||||
this.status = dto.status;
|
||||
this.leagueId = dto.leagueId;
|
||||
this.leagueName = dto.leagueName;
|
||||
this.scheduledTime = dto.scheduledTime;
|
||||
this.status = dto.status;
|
||||
if (dto.trackName !== undefined) this.trackName = dto.trackName;
|
||||
this.strengthOfField = dto.strengthOfField;
|
||||
this.isUpcoming = dto.isUpcoming;
|
||||
this.isLive = dto.isLive;
|
||||
this.isPast = dto.isPast;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted scheduled time */
|
||||
get formattedScheduledTime(): string {
|
||||
return new Date(this.scheduledTime).toLocaleString();
|
||||
return new Date(this.scheduledAt).toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Badge variant for status */
|
||||
get statusBadgeVariant(): string {
|
||||
switch (this.status) {
|
||||
case 'upcoming': return 'info';
|
||||
case 'live': return 'success';
|
||||
case 'finished': return 'secondary';
|
||||
case 'scheduled': return 'info';
|
||||
case 'running': return 'success';
|
||||
case 'completed': return 'secondary';
|
||||
case 'cancelled': return 'danger';
|
||||
default: return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Whether race is upcoming */
|
||||
get isUpcoming(): boolean {
|
||||
return this.status === 'upcoming';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether race is live */
|
||||
get isLive(): boolean {
|
||||
return this.status === 'live';
|
||||
}
|
||||
|
||||
/** UI-specific: Time until start in minutes */
|
||||
get timeUntilStart(): number {
|
||||
const now = new Date();
|
||||
const scheduled = new Date(this.scheduledTime);
|
||||
const scheduled = new Date(this.scheduledAt);
|
||||
return Math.max(0, Math.floor((scheduled.getTime() - now.getTime()) / (1000 * 60)));
|
||||
}
|
||||
|
||||
|
||||
99
apps/website/lib/view-models/RaceStewardingViewModel.ts
Normal file
99
apps/website/lib/view-models/RaceStewardingViewModel.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// DTO interfaces matching the API responses
|
||||
interface RaceDetailDTO {
|
||||
race: {
|
||||
id: string;
|
||||
track: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
} | null;
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface RaceProtestsDTO {
|
||||
protests: Array<{
|
||||
id: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
incident: {
|
||||
lap: number;
|
||||
description: string;
|
||||
};
|
||||
filedAt: string;
|
||||
status: string;
|
||||
decisionNotes?: string;
|
||||
proofVideoUrl?: string;
|
||||
}>;
|
||||
driverMap: Record<string, { id: string; name: string }>;
|
||||
}
|
||||
|
||||
interface RacePenaltiesDTO {
|
||||
penalties: Array<{
|
||||
id: string;
|
||||
driverId: string;
|
||||
type: string;
|
||||
value: number;
|
||||
reason: string;
|
||||
notes?: string;
|
||||
}>;
|
||||
driverMap: Record<string, { id: string; name: string }>;
|
||||
}
|
||||
|
||||
interface RaceStewardingDTO {
|
||||
raceDetail: RaceDetailDTO;
|
||||
protests: RaceProtestsDTO;
|
||||
penalties: RacePenaltiesDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Race Stewarding View Model
|
||||
* Represents all data needed for race stewarding (protests, penalties, race info)
|
||||
*/
|
||||
export class RaceStewardingViewModel {
|
||||
race: RaceDetailDTO['race'];
|
||||
league: RaceDetailDTO['league'];
|
||||
protests: RaceProtestsDTO['protests'];
|
||||
penalties: RacePenaltiesDTO['penalties'];
|
||||
driverMap: Record<string, { id: string; name: string }>;
|
||||
|
||||
constructor(dto: RaceStewardingDTO) {
|
||||
this.race = dto.raceDetail.race;
|
||||
this.league = dto.raceDetail.league;
|
||||
this.protests = dto.protests.protests;
|
||||
this.penalties = dto.penalties.penalties;
|
||||
|
||||
// Merge driver maps from protests and penalties
|
||||
this.driverMap = { ...dto.protests.driverMap, ...dto.penalties.driverMap };
|
||||
}
|
||||
|
||||
/** UI-specific: Pending protests */
|
||||
get pendingProtests() {
|
||||
return this.protests.filter(p => p.status === 'pending' || p.status === 'under_review');
|
||||
}
|
||||
|
||||
/** UI-specific: Resolved protests */
|
||||
get resolvedProtests() {
|
||||
return this.protests.filter(p =>
|
||||
p.status === 'upheld' ||
|
||||
p.status === 'dismissed' ||
|
||||
p.status === 'withdrawn'
|
||||
);
|
||||
}
|
||||
|
||||
/** UI-specific: Total pending protests count */
|
||||
get pendingCount(): number {
|
||||
return this.pendingProtests.length;
|
||||
}
|
||||
|
||||
/** UI-specific: Total resolved protests count */
|
||||
get resolvedCount(): number {
|
||||
return this.resolvedProtests.length;
|
||||
}
|
||||
|
||||
/** UI-specific: Total penalties count */
|
||||
get penaltiesCount(): number {
|
||||
return this.penalties.length;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,65 @@
|
||||
// Note: No generated DTO available for RaceCard yet
|
||||
interface RaceCardDTO {
|
||||
id: string;
|
||||
title: string;
|
||||
scheduledTime: string;
|
||||
status: string;
|
||||
}
|
||||
import { RaceListItemViewModel } from './RaceListItemViewModel';
|
||||
|
||||
/**
|
||||
* Race card view model
|
||||
* Represents a race card in list views
|
||||
*/
|
||||
export class RaceCardViewModel {
|
||||
id: string;
|
||||
title: string;
|
||||
scheduledTime: string;
|
||||
status: string;
|
||||
|
||||
constructor(dto: RaceCardDTO) {
|
||||
this.id = dto.id;
|
||||
this.title = dto.title;
|
||||
this.scheduledTime = dto.scheduledTime;
|
||||
this.status = dto.status;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted scheduled time */
|
||||
get formattedScheduledTime(): string {
|
||||
return new Date(this.scheduledTime).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
// Note: No generated DTO available for RacesPage yet
|
||||
// DTO matching the backend RacesPageDataDTO
|
||||
interface RacesPageDTO {
|
||||
upcomingRaces: RaceCardDTO[];
|
||||
completedRaces: RaceCardDTO[];
|
||||
totalCount: number;
|
||||
races: Array<{
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string;
|
||||
status: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
strengthOfField: number | null;
|
||||
isUpcoming: boolean;
|
||||
isLive: boolean;
|
||||
isPast: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Races page view model
|
||||
* Represents the races page data
|
||||
* Represents the races page data with all races in a single list
|
||||
*/
|
||||
export class RacesPageViewModel {
|
||||
upcomingRaces: RaceCardViewModel[];
|
||||
completedRaces: RaceCardViewModel[];
|
||||
totalCount: number;
|
||||
races: RaceListItemViewModel[];
|
||||
|
||||
constructor(dto: RacesPageDTO) {
|
||||
this.upcomingRaces = dto.upcomingRaces.map(r => new RaceCardViewModel(r));
|
||||
this.completedRaces = dto.completedRaces.map(r => new RaceCardViewModel(r));
|
||||
this.totalCount = dto.totalCount;
|
||||
this.races = dto.races.map(r => new RaceListItemViewModel(r));
|
||||
}
|
||||
|
||||
/** UI-specific: Total upcoming races */
|
||||
get upcomingCount(): number {
|
||||
return this.upcomingRaces.length;
|
||||
/** UI-specific: Total races */
|
||||
get totalCount(): number {
|
||||
return this.races.length;
|
||||
}
|
||||
|
||||
/** UI-specific: Total completed races */
|
||||
get completedCount(): number {
|
||||
return this.completedRaces.length;
|
||||
/** UI-specific: Upcoming races */
|
||||
get upcomingRaces(): RaceListItemViewModel[] {
|
||||
return this.races.filter(r => r.isUpcoming);
|
||||
}
|
||||
|
||||
/** UI-specific: Live races */
|
||||
get liveRaces(): RaceListItemViewModel[] {
|
||||
return this.races.filter(r => r.isLive);
|
||||
}
|
||||
|
||||
/** UI-specific: Past races */
|
||||
get pastRaces(): RaceListItemViewModel[] {
|
||||
return this.races.filter(r => r.isPast);
|
||||
}
|
||||
|
||||
/** UI-specific: Scheduled races */
|
||||
get scheduledRaces(): RaceListItemViewModel[] {
|
||||
return this.races.filter(r => r.status === 'scheduled');
|
||||
}
|
||||
|
||||
/** UI-specific: Running races */
|
||||
get runningRaces(): RaceListItemViewModel[] {
|
||||
return this.races.filter(r => r.status === 'running');
|
||||
}
|
||||
|
||||
/** UI-specific: Completed races */
|
||||
get completedRaces(): RaceListItemViewModel[] {
|
||||
return this.races.filter(r => r.status === 'completed');
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,45 @@
|
||||
import { TransactionDto } from '../types/generated/TransactionDto';
|
||||
|
||||
// TODO: Use generated TransactionDto when it includes all required fields
|
||||
export type FullTransactionDto = TransactionDto & {
|
||||
amount: number;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
type: 'deposit' | 'withdrawal';
|
||||
};
|
||||
|
||||
export class WalletTransactionViewModel {
|
||||
id: string;
|
||||
walletId: string;
|
||||
amount: number;
|
||||
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
|
||||
description: string;
|
||||
createdAt: string;
|
||||
type: 'deposit' | 'withdrawal';
|
||||
amount: number;
|
||||
fee: number;
|
||||
netAmount: number;
|
||||
date: Date;
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
reference?: string;
|
||||
|
||||
constructor(dto: FullTransactionDto) {
|
||||
constructor(dto: {
|
||||
id: string;
|
||||
type: 'sponsorship' | 'membership' | 'withdrawal' | 'prize';
|
||||
description: string;
|
||||
amount: number;
|
||||
fee: number;
|
||||
netAmount: number;
|
||||
date: Date;
|
||||
status: 'completed' | 'pending' | 'failed';
|
||||
reference?: string;
|
||||
}) {
|
||||
this.id = dto.id;
|
||||
this.walletId = dto.walletId;
|
||||
this.amount = dto.amount;
|
||||
this.description = dto.description;
|
||||
this.createdAt = dto.createdAt;
|
||||
this.type = dto.type;
|
||||
this.description = dto.description;
|
||||
this.amount = dto.amount;
|
||||
this.fee = dto.fee;
|
||||
this.netAmount = dto.netAmount;
|
||||
this.date = dto.date;
|
||||
this.status = dto.status;
|
||||
this.reference = dto.reference;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted amount with sign */
|
||||
get formattedAmount(): string {
|
||||
const sign = this.type === 'deposit' ? '+' : '-';
|
||||
return `${sign}$${this.amount.toFixed(2)}`;
|
||||
const sign = this.amount > 0 ? '+' : '';
|
||||
return `${sign}$${Math.abs(this.amount).toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Amount color */
|
||||
get amountColor(): string {
|
||||
return this.type === 'deposit' ? 'green' : 'red';
|
||||
return this.amount > 0 ? 'green' : 'red';
|
||||
}
|
||||
|
||||
/** UI-specific: Type display */
|
||||
@@ -41,13 +47,13 @@ export class WalletTransactionViewModel {
|
||||
return this.type.charAt(0).toUpperCase() + this.type.slice(1);
|
||||
}
|
||||
|
||||
/** UI-specific: Amount color */
|
||||
get amountColor(): string {
|
||||
return this.type === 'deposit' ? 'green' : 'red';
|
||||
/** UI-specific: Formatted date */
|
||||
get formattedDate(): string {
|
||||
return this.date.toLocaleDateString();
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted created date */
|
||||
get formattedCreatedAt(): string {
|
||||
return new Date(this.createdAt).toLocaleString();
|
||||
/** UI-specific: Is incoming */
|
||||
get isIncoming(): boolean {
|
||||
return this.amount > 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user