Files
gridpilot.gg/core/racing/domain/entities/league-wallet/Transaction.ts
2026-01-16 16:46:57 +01:00

192 lines
5.0 KiB
TypeScript

/**
* Domain Entity: Transaction
*
* Represents a financial transaction in the league wallet system.
*/
import { RacingDomainInvariantError, RacingDomainValidationError } from '../../errors/RacingDomainError';
import { Entity } from '@core/shared/domain/Entity';
import type { Money } from '../../value-objects/Money';
import { LeagueWalletId } from './LeagueWalletId';
import { TransactionId } from './TransactionId';
export type TransactionType =
| 'sponsorship_payment'
| 'membership_payment'
| 'prize_payout'
| 'withdrawal'
| 'refund';
export type TransactionStatus = 'pending' | 'completed' | 'failed' | 'cancelled';
export interface TransactionProps {
id: TransactionId;
walletId: LeagueWalletId;
type: TransactionType;
amount: Money;
platformFee: Money;
netAmount: Money;
status: TransactionStatus;
createdAt: Date;
completedAt: Date | undefined;
description: string | undefined;
metadata: Record<string, unknown> | undefined;
}
export class Transaction extends Entity<TransactionId> {
readonly walletId: LeagueWalletId;
readonly type: TransactionType;
readonly amount: Money;
readonly platformFee: Money;
readonly netAmount: Money;
readonly status: TransactionStatus;
readonly createdAt: Date;
readonly completedAt: Date | undefined;
readonly description: string | undefined;
readonly metadata: Record<string, unknown> | undefined;
private constructor(props: TransactionProps) {
super(props.id);
this.walletId = props.walletId;
this.type = props.type;
this.amount = props.amount;
this.platformFee = props.platformFee;
this.netAmount = props.netAmount;
this.status = props.status;
this.createdAt = props.createdAt;
this.completedAt = props.completedAt;
this.description = props.description;
this.metadata = props.metadata;
}
static create(props: Omit<TransactionProps, 'createdAt' | 'status' | 'platformFee' | 'netAmount'> & {
createdAt?: Date;
status?: TransactionStatus;
}): Transaction {
this.validate(props);
const platformFee = props.amount.calculatePlatformFee();
const netAmount = props.amount.calculateNetAmount();
return new Transaction({
...props,
platformFee,
netAmount,
createdAt: props.createdAt ?? new Date(),
status: props.status ?? 'pending',
});
}
static rehydrate(props: {
id: string;
walletId: string;
type: TransactionType;
amount: Money;
platformFee: Money;
netAmount: Money;
status: TransactionStatus;
createdAt: Date;
completedAt?: Date;
description?: string;
metadata?: Record<string, unknown>;
}): Transaction {
return new Transaction({
id: TransactionId.create(props.id),
walletId: LeagueWalletId.create(props.walletId),
type: props.type,
amount: props.amount,
platformFee: props.platformFee,
netAmount: props.netAmount,
status: props.status,
createdAt: props.createdAt,
completedAt: props.completedAt,
description: props.description,
metadata: props.metadata,
});
}
private static validate(props: Omit<TransactionProps, 'createdAt' | 'status' | 'platformFee' | 'netAmount'>): void {
if (!props.id) {
throw new RacingDomainValidationError('Transaction ID is required');
}
if (!props.walletId) {
throw new RacingDomainValidationError('Transaction walletId is required');
}
if (!props.type) {
throw new RacingDomainValidationError('Transaction type is required');
}
if (!props.amount) {
throw new RacingDomainValidationError('Transaction amount is required');
}
if (props.amount.amount <= 0) {
throw new RacingDomainValidationError('Transaction amount must be greater than zero');
}
}
/**
* Mark transaction as completed
*/
complete(): Transaction {
if (this.status === 'completed') {
throw new RacingDomainInvariantError('Transaction is already completed');
}
if (this.status === 'failed' || this.status === 'cancelled') {
throw new RacingDomainInvariantError('Cannot complete a failed or cancelled transaction');
}
return new Transaction({
...this,
status: 'completed',
completedAt: new Date(),
});
}
/**
* Mark transaction as failed
*/
fail(): Transaction {
if (this.status === 'completed') {
throw new RacingDomainInvariantError('Cannot fail a completed transaction');
}
return new Transaction({
...this,
status: 'failed',
});
}
/**
* Cancel transaction
*/
cancel(): Transaction {
if (this.status === 'completed') {
throw new RacingDomainInvariantError('Cannot cancel a completed transaction');
}
return new Transaction({
...this,
status: 'cancelled',
});
}
/**
* Check if transaction is completed
*/
isCompleted(): boolean {
return this.status === 'completed';
}
/**
* Check if transaction is pending
*/
isPending(): boolean {
return this.status === 'pending';
}
}