Files
gridpilot.gg/adapters/bootstrap/racing/RacingLeagueWalletFactory.ts
2026-01-16 21:40:26 +01:00

201 lines
6.9 KiB
TypeScript

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';
}
}
}