import type { League } from '@core/racing/domain/entities/League'; import { LeagueWallet } from '@core/racing/domain/entities/league-wallet/LeagueWallet'; import { LeagueWalletId } from '@core/racing/domain/entities/league-wallet/LeagueWalletId'; import { Transaction } from '@core/racing/domain/entities/league-wallet/Transaction'; import { TransactionId } from '@core/racing/domain/entities/league-wallet/TransactionId'; import { Money } from '@core/racing/domain/value-objects/Money'; import { faker } from '@faker-js/faker'; import { seedId } from './SeedIdHelper'; type LeagueWalletSeed = { wallets: LeagueWallet[]; transactions: Transaction[]; }; export class RacingLeagueWalletFactory { constructor( private readonly baseDate: Date, private readonly persistence: 'postgres' | 'inmemory' = 'inmemory', ) {} create(leagues: League[]): LeagueWalletSeed { const wallets: LeagueWallet[] = []; const transactions: Transaction[] = []; for (const league of leagues) { const leagueId = league.id.toString(); const walletId = seedId(`wallet-${leagueId}`, this.persistence); const createdAt = faker.date.past({ years: 2, refDate: this.baseDate }); // Ensure coverage: // - some leagues have no wallet (wallet use-case returns WALLET_NOT_FOUND) // - some wallets have no transactions // - some wallets have varied transaction types + statuses const shouldHaveWallet = leagueId !== 'league-2' && leagueId !== 'league-7'; if (!shouldHaveWallet) { continue; } const currency: 'USD' | 'EUR' | 'GBP' = leagueId === 'league-1' ? 'EUR' : 'USD'; const baselineBalance = leagueId === 'league-5' ? Money.create(2880, 'USD') : Money.create(faker.number.int({ min: 0, max: 12_000 }), currency); let wallet = LeagueWallet.create({ id: walletId, leagueId, balance: baselineBalance, createdAt, }); const transactionCount = leagueId === 'league-3' ? 0 : leagueId === 'league-5' ? 14 : faker.number.int({ min: 2, max: 18 }); for (let i = 0; i < transactionCount; i++) { const id = TransactionId.create(seedId(`tx-${leagueId}-${i + 1}`, this.persistence)); const type = this.pickTransactionType(i, leagueId); const amount = this.pickAmount(type, currency, leagueId); let status = this.pickStatus(type, i, leagueId); const isDebit = type === 'withdrawal' || type === 'refund'; if (status === 'completed' && isDebit) { const expectedNetAmount = amount.calculateNetAmount(); if (!wallet.canWithdraw(expectedNetAmount)) { status = type === 'withdrawal' ? 'failed' : 'cancelled'; } } const tx = Transaction.create({ id, walletId: LeagueWalletId.create(walletId), type, amount, status, completedAt: status === 'completed' ? faker.date.recent({ days: 180, refDate: this.baseDate }) : undefined, createdAt: faker.date.recent({ days: 240, refDate: this.baseDate }), description: this.describe(type, leagueId), metadata: { leagueId, source: type === 'sponsorship_payment' ? 'sponsorship' : 'wallet', }, }); transactions.push(tx); // Update wallet balance for completed transactions only. // Use netAmount because wallet stores net proceeds after platform fee. if (tx.status === 'completed') { if (tx.type === 'withdrawal' || tx.type === 'refund') { wallet = wallet.withdrawFunds(tx.netAmount, tx.id.toString()); } else { wallet = wallet.addFunds(tx.netAmount, tx.id.toString()); } } } // Explicit edge-case: pending prize payout shows up in "pending payouts" if (leagueId === 'league-5') { const pendingPrize = Transaction.create({ id: TransactionId.create(seedId(`tx-${leagueId}-pending-prize`, this.persistence)), walletId: LeagueWalletId.create(walletId), type: 'prize_payout', amount: Money.create(600, currency), status: 'pending', createdAt: faker.date.recent({ days: 15, refDate: this.baseDate }), completedAt: undefined, description: 'Season prize pool payout (pending)', metadata: { seasonId: seedId('season-2', this.persistence), placement: 'P1-P3' }, }); transactions.push(pendingPrize); } wallets.push(wallet); } return { wallets, transactions }; } private pickTransactionType(index: number, leagueId: string) { if (leagueId === 'league-5') { const types = [ 'sponsorship_payment', 'membership_payment', 'membership_payment', 'sponsorship_payment', 'prize_payout', 'withdrawal', ] as const; return types[index % types.length]!; } const types = [ 'membership_payment', 'sponsorship_payment', 'membership_payment', 'withdrawal', 'refund', 'prize_payout', ] as const; return types[index % types.length]!; } private pickAmount(type: Transaction['type'], currency: 'USD' | 'EUR' | 'GBP', leagueId: string): Money { const asUsd = (amount: number) => Money.create(amount, currency); switch (type) { case 'sponsorship_payment': return asUsd(leagueId === 'league-5' ? faker.number.int({ min: 400, max: 1600 }) : faker.number.int({ min: 150, max: 2400 })); case 'membership_payment': return asUsd(faker.number.int({ min: 10, max: 120 })); case 'withdrawal': return asUsd(faker.number.int({ min: 25, max: 800 })); case 'refund': return asUsd(faker.number.int({ min: 10, max: 150 })); case 'prize_payout': return asUsd(faker.number.int({ min: 100, max: 1500 })); } } private pickStatus(type: Transaction['type'], index: number, leagueId: string): Transaction['status'] { if (leagueId === 'league-5') { if (type === 'withdrawal' && index % 2 === 0) return 'completed'; if (type === 'prize_payout' && index % 3 === 0) return 'pending'; return 'completed'; } if (type === 'withdrawal') { return faker.helpers.arrayElement(['completed', 'pending', 'failed'] as const); } if (type === 'refund') { return faker.helpers.arrayElement(['completed', 'cancelled'] as const); } return faker.helpers.arrayElement(['completed', 'pending'] as const); } private describe(type: Transaction['type'], leagueId: string): string { switch (type) { case 'sponsorship_payment': return leagueId === 'league-5' ? 'Season sponsorship payment' : 'Sponsorship payment'; case 'membership_payment': return 'Membership fee'; case 'withdrawal': return 'Owner withdrawal'; case 'refund': return 'Refund processed'; case 'prize_payout': return 'Prize payout'; } } }