refactor
This commit is contained in:
148
core/racing/domain/entities/league-wallet/LeagueWallet.test.ts
Normal file
148
core/racing/domain/entities/league-wallet/LeagueWallet.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueWallet } from './LeagueWallet';
|
||||
import { Money } from '../value-objects/Money';
|
||||
|
||||
describe('LeagueWallet', () => {
|
||||
it('should create a league wallet', () => {
|
||||
const balance = Money.create(10000, 'USD'); // $100
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
expect(wallet.id.toString()).toBe('wallet1');
|
||||
expect(wallet.leagueId.toString()).toBe('league1');
|
||||
expect(wallet.getBalance().equals(balance)).toBe(true);
|
||||
expect(wallet.getTransactionIds()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw on invalid id', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
expect(() => LeagueWallet.create({
|
||||
id: '',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
})).toThrow('LeagueWallet ID is required');
|
||||
});
|
||||
|
||||
it('should throw on invalid leagueId', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
expect(() => LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: '',
|
||||
balance,
|
||||
})).toThrow('LeagueWallet leagueId is required');
|
||||
});
|
||||
|
||||
|
||||
it('should add funds', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
const netAmount = Money.create(5000, 'USD'); // $50
|
||||
const updated = wallet.addFunds(netAmount, 'tx1');
|
||||
|
||||
expect(updated.getBalance().amount).toBe(15000);
|
||||
expect(updated.getTransactionIds()).toEqual(['tx1']);
|
||||
});
|
||||
|
||||
it('should throw on add funds with different currency', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
const netAmount = Money.create(5000, 'EUR');
|
||||
expect(() => wallet.addFunds(netAmount, 'tx1')).toThrow('Cannot add funds with different currency');
|
||||
});
|
||||
|
||||
it('should withdraw funds', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
const amount = Money.create(5000, 'USD');
|
||||
const updated = wallet.withdrawFunds(amount, 'tx1');
|
||||
|
||||
expect(updated.getBalance().amount).toBe(5000);
|
||||
expect(updated.getTransactionIds()).toEqual(['tx1']);
|
||||
});
|
||||
|
||||
it('should throw on withdraw with insufficient balance', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
const amount = Money.create(15000, 'USD');
|
||||
expect(() => wallet.withdrawFunds(amount, 'tx1')).toThrow('Insufficient balance for withdrawal');
|
||||
});
|
||||
|
||||
it('should throw on withdraw with different currency', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
const amount = Money.create(5000, 'EUR');
|
||||
expect(() => wallet.withdrawFunds(amount, 'tx1')).toThrow('Cannot withdraw funds with different currency');
|
||||
});
|
||||
|
||||
it('should check if can withdraw', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
const amount = Money.create(5000, 'USD');
|
||||
expect(wallet.canWithdraw(amount)).toBe(true);
|
||||
|
||||
const largeAmount = Money.create(15000, 'USD');
|
||||
expect(wallet.canWithdraw(largeAmount)).toBe(false);
|
||||
|
||||
const exactAmount = Money.create(10000, 'USD');
|
||||
expect(wallet.canWithdraw(exactAmount)).toBe(true);
|
||||
|
||||
const differentCurrency = Money.create(5000, 'EUR');
|
||||
expect(wallet.canWithdraw(differentCurrency)).toBe(false);
|
||||
});
|
||||
|
||||
it('should get balance', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
});
|
||||
|
||||
expect(wallet.getBalance().equals(balance)).toBe(true);
|
||||
});
|
||||
|
||||
it('should get transaction ids', () => {
|
||||
const balance = Money.create(10000, 'USD');
|
||||
const wallet = LeagueWallet.create({
|
||||
id: 'wallet1',
|
||||
leagueId: 'league1',
|
||||
balance,
|
||||
transactionIds: ['tx1', 'tx2'],
|
||||
});
|
||||
|
||||
expect(wallet.getTransactionIds()).toEqual(['tx1', 'tx2']);
|
||||
});
|
||||
});
|
||||
143
core/racing/domain/entities/league-wallet/LeagueWallet.ts
Normal file
143
core/racing/domain/entities/league-wallet/LeagueWallet.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Domain Entity: LeagueWallet
|
||||
*
|
||||
* Represents a league's financial wallet.
|
||||
* Aggregate root for managing league finances and transactions.
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../errors/RacingDomainError';
|
||||
import type { IEntity } from '@core/shared/domain';
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
import { LeagueWalletId } from './LeagueWalletId';
|
||||
import { LeagueId } from './LeagueId';
|
||||
import { TransactionId } from './TransactionId';
|
||||
|
||||
export interface LeagueWalletProps {
|
||||
id: LeagueWalletId;
|
||||
leagueId: LeagueId;
|
||||
balance: Money;
|
||||
transactionIds: TransactionId[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class LeagueWallet implements IEntity<LeagueWalletId> {
|
||||
readonly id: LeagueWalletId;
|
||||
readonly leagueId: LeagueId;
|
||||
readonly balance: Money;
|
||||
readonly transactionIds: TransactionId[];
|
||||
readonly createdAt: Date;
|
||||
|
||||
private constructor(props: LeagueWalletProps) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
this.balance = props.balance;
|
||||
this.transactionIds = props.transactionIds;
|
||||
this.createdAt = props.createdAt;
|
||||
}
|
||||
|
||||
static create(props: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
balance: Money;
|
||||
createdAt?: Date;
|
||||
transactionIds?: string[];
|
||||
}): LeagueWallet {
|
||||
this.validate(props);
|
||||
|
||||
const id = LeagueWalletId.create(props.id);
|
||||
const leagueId = LeagueId.create(props.leagueId);
|
||||
const transactionIds = props.transactionIds?.map(tid => TransactionId.create(tid)) ?? [];
|
||||
|
||||
return new LeagueWallet({
|
||||
id,
|
||||
leagueId,
|
||||
balance: props.balance,
|
||||
transactionIds,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
balance: Money;
|
||||
}): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('LeagueWallet ID is required');
|
||||
}
|
||||
|
||||
if (!props.leagueId || props.leagueId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('LeagueWallet leagueId is required');
|
||||
}
|
||||
|
||||
if (!props.balance) {
|
||||
throw new RacingDomainValidationError('LeagueWallet balance is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add funds to wallet (from sponsorship or membership payments)
|
||||
*/
|
||||
addFunds(netAmount: Money, transactionId: string): LeagueWallet {
|
||||
if (this.balance.currency !== netAmount.currency) {
|
||||
throw new RacingDomainInvariantError('Cannot add funds with different currency');
|
||||
}
|
||||
|
||||
const newBalance = this.balance.add(netAmount);
|
||||
const newTransactionId = TransactionId.create(transactionId);
|
||||
|
||||
return new LeagueWallet({
|
||||
...this,
|
||||
balance: newBalance,
|
||||
transactionIds: [...this.transactionIds, newTransactionId],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw funds from wallet
|
||||
* Domain rule: Cannot withdraw if insufficient balance
|
||||
*/
|
||||
withdrawFunds(amount: Money, transactionId: string): LeagueWallet {
|
||||
if (this.balance.currency !== amount.currency) {
|
||||
throw new RacingDomainInvariantError('Cannot withdraw funds with different currency');
|
||||
}
|
||||
|
||||
if (!this.balance.isGreaterThan(amount) && !this.balance.equals(amount)) {
|
||||
throw new RacingDomainInvariantError('Insufficient balance for withdrawal');
|
||||
}
|
||||
|
||||
const newBalance = this.balance.subtract(amount);
|
||||
const newTransactionId = TransactionId.create(transactionId);
|
||||
|
||||
return new LeagueWallet({
|
||||
...this,
|
||||
balance: newBalance,
|
||||
transactionIds: [...this.transactionIds, newTransactionId],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if wallet can withdraw a specific amount
|
||||
*/
|
||||
canWithdraw(amount: Money): boolean {
|
||||
if (this.balance.currency !== amount.currency) {
|
||||
return false;
|
||||
}
|
||||
return this.balance.isGreaterThan(amount) || this.balance.equals(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current balance
|
||||
*/
|
||||
getBalance(): Money {
|
||||
return this.balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all transaction IDs
|
||||
*/
|
||||
getTransactionIds(): string[] {
|
||||
return this.transactionIds.map(tid => tid.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { LeagueWalletId } from './LeagueWalletId';
|
||||
|
||||
describe('LeagueWalletId', () => {
|
||||
it('should create a league wallet id', () => {
|
||||
const id = LeagueWalletId.create('wallet1');
|
||||
expect(id.toString()).toBe('wallet1');
|
||||
});
|
||||
|
||||
it('should throw on empty id', () => {
|
||||
expect(() => LeagueWalletId.create('')).toThrow('LeagueWallet ID cannot be empty');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
const id = LeagueWalletId.create(' wallet1 ');
|
||||
expect(id.toString()).toBe('wallet1');
|
||||
});
|
||||
|
||||
it('should check equality', () => {
|
||||
const id1 = LeagueWalletId.create('wallet1');
|
||||
const id2 = LeagueWalletId.create('wallet1');
|
||||
const id3 = LeagueWalletId.create('wallet2');
|
||||
|
||||
expect(id1.equals(id2)).toBe(true);
|
||||
expect(id1.equals(id3)).toBe(false);
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/entities/league-wallet/LeagueWalletId.ts
Normal file
20
core/racing/domain/entities/league-wallet/LeagueWalletId.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
export class LeagueWalletId {
|
||||
private constructor(private readonly value: string) {}
|
||||
|
||||
static create(value: string): LeagueWalletId {
|
||||
if (!value || value.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('LeagueWallet ID cannot be empty');
|
||||
}
|
||||
return new LeagueWalletId(value.trim());
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: LeagueWalletId): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
137
core/racing/domain/entities/league-wallet/Transaction.test.ts
Normal file
137
core/racing/domain/entities/league-wallet/Transaction.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Transaction } from './Transaction';
|
||||
import { TransactionId } from './TransactionId';
|
||||
import { LeagueWalletId } from './LeagueWalletId';
|
||||
import { Money } from '../../value-objects/Money';
|
||||
|
||||
describe('Transaction', () => {
|
||||
const validId = TransactionId.create('tx1');
|
||||
const validWalletId = LeagueWalletId.create('wallet1');
|
||||
const validAmount = Money.create(10000, 'USD'); // $100.00
|
||||
|
||||
const validProps = {
|
||||
id: validId,
|
||||
walletId: validWalletId,
|
||||
type: 'sponsorship_payment' as const,
|
||||
amount: validAmount,
|
||||
completedAt: undefined,
|
||||
description: 'Test transaction',
|
||||
metadata: undefined,
|
||||
};
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a transaction with default values', () => {
|
||||
const transaction = Transaction.create(validProps);
|
||||
|
||||
expect(transaction.id).toBe(validId);
|
||||
expect(transaction.walletId).toBe(validWalletId);
|
||||
expect(transaction.type).toBe('sponsorship_payment');
|
||||
expect(transaction.amount).toBe(validAmount);
|
||||
expect(transaction.status).toBe('pending');
|
||||
expect(transaction.createdAt).toBeInstanceOf(Date);
|
||||
expect(transaction.completedAt).toBeUndefined();
|
||||
expect(transaction.description).toBe('Test transaction');
|
||||
expect(transaction.platformFee.amount).toBe(1000); // 10% of 10000
|
||||
expect(transaction.netAmount.amount).toBe(9000); // 10000 - 1000
|
||||
});
|
||||
|
||||
it('should create a transaction with custom createdAt and status', () => {
|
||||
const customDate = new Date('2023-01-01');
|
||||
const transaction = Transaction.create({
|
||||
...validProps,
|
||||
createdAt: customDate,
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
expect(transaction.createdAt).toBe(customDate);
|
||||
expect(transaction.status).toBe('completed');
|
||||
});
|
||||
|
||||
|
||||
it('should throw on zero amount', () => {
|
||||
const zeroAmount = Money.create(0, 'USD');
|
||||
expect(() => Transaction.create({ ...validProps, amount: zeroAmount })).toThrow('Transaction amount must be greater than zero');
|
||||
});
|
||||
|
||||
it('should throw on negative amount', () => {
|
||||
expect(() => Money.create(-100, 'USD')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('complete', () => {
|
||||
it('should complete a pending transaction', () => {
|
||||
const transaction = Transaction.create(validProps);
|
||||
const completed = transaction.complete();
|
||||
|
||||
expect(completed.status).toBe('completed');
|
||||
expect(completed.completedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should throw on completing already completed transaction', () => {
|
||||
const transaction = Transaction.create({ ...validProps, status: 'completed' });
|
||||
expect(() => transaction.complete()).toThrow('Transaction is already completed');
|
||||
});
|
||||
|
||||
it('should throw on completing failed transaction', () => {
|
||||
const transaction = Transaction.create({ ...validProps, status: 'failed' });
|
||||
expect(() => transaction.complete()).toThrow('Cannot complete a failed or cancelled transaction');
|
||||
});
|
||||
|
||||
it('should throw on completing cancelled transaction', () => {
|
||||
const transaction = Transaction.create({ ...validProps, status: 'cancelled' });
|
||||
expect(() => transaction.complete()).toThrow('Cannot complete a failed or cancelled transaction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fail', () => {
|
||||
it('should fail a pending transaction', () => {
|
||||
const transaction = Transaction.create(validProps);
|
||||
const failed = transaction.fail();
|
||||
|
||||
expect(failed.status).toBe('failed');
|
||||
});
|
||||
|
||||
it('should throw on failing completed transaction', () => {
|
||||
const transaction = Transaction.create({ ...validProps, status: 'completed' });
|
||||
expect(() => transaction.fail()).toThrow('Cannot fail a completed transaction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', () => {
|
||||
it('should cancel a pending transaction', () => {
|
||||
const transaction = Transaction.create(validProps);
|
||||
const cancelled = transaction.cancel();
|
||||
|
||||
expect(cancelled.status).toBe('cancelled');
|
||||
});
|
||||
|
||||
it('should throw on cancelling completed transaction', () => {
|
||||
const transaction = Transaction.create({ ...validProps, status: 'completed' });
|
||||
expect(() => transaction.cancel()).toThrow('Cannot cancel a completed transaction');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCompleted', () => {
|
||||
it('should return true for completed transaction', () => {
|
||||
const transaction = Transaction.create({ ...validProps, status: 'completed' });
|
||||
expect(transaction.isCompleted()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-completed transaction', () => {
|
||||
const transaction = Transaction.create(validProps);
|
||||
expect(transaction.isCompleted()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPending', () => {
|
||||
it('should return true for pending transaction', () => {
|
||||
const transaction = Transaction.create(validProps);
|
||||
expect(transaction.isPending()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-pending transaction', () => {
|
||||
const transaction = Transaction.create({ ...validProps, status: 'completed' });
|
||||
expect(transaction.isPending()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
164
core/racing/domain/entities/league-wallet/Transaction.ts
Normal file
164
core/racing/domain/entities/league-wallet/Transaction.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Domain Entity: Transaction
|
||||
*
|
||||
* Represents a financial transaction in the league wallet system.
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../../errors/RacingDomainError';
|
||||
|
||||
import type { Money } from '../../value-objects/Money';
|
||||
import type { IEntity } from '@core/shared/domain';
|
||||
import type { TransactionId } from './TransactionId';
|
||||
import type { LeagueWalletId } from './LeagueWalletId';
|
||||
|
||||
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 implements IEntity<TransactionId> {
|
||||
readonly id: 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) {
|
||||
this.id = 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',
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TransactionId } from './TransactionId';
|
||||
|
||||
describe('TransactionId', () => {
|
||||
it('should create a transaction id', () => {
|
||||
const id = TransactionId.create('tx1');
|
||||
expect(id.toString()).toBe('tx1');
|
||||
});
|
||||
|
||||
it('should throw on empty id', () => {
|
||||
expect(() => TransactionId.create('')).toThrow('Transaction ID cannot be empty');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
const id = TransactionId.create(' tx1 ');
|
||||
expect(id.toString()).toBe('tx1');
|
||||
});
|
||||
|
||||
it('should check equality', () => {
|
||||
const id1 = TransactionId.create('tx1');
|
||||
const id2 = TransactionId.create('tx1');
|
||||
const id3 = TransactionId.create('tx2');
|
||||
|
||||
expect(id1.equals(id2)).toBe(true);
|
||||
expect(id1.equals(id3)).toBe(false);
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/entities/league-wallet/TransactionId.ts
Normal file
20
core/racing/domain/entities/league-wallet/TransactionId.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
export class TransactionId {
|
||||
private constructor(private readonly value: string) {}
|
||||
|
||||
static create(value: string): TransactionId {
|
||||
if (!value || value.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Transaction ID cannot be empty');
|
||||
}
|
||||
return new TransactionId(value.trim());
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: TransactionId): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user