201 lines
6.9 KiB
TypeScript
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';
|
|
}
|
|
}
|
|
} |