refactor
This commit is contained in:
303
core/racing/domain/entities/prize/Prize.test.ts
Normal file
303
core/racing/domain/entities/prize/Prize.test.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { Prize } from './Prize';
|
||||
import { Money } from '../../value-objects/Money';
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../../errors/RacingDomainError';
|
||||
|
||||
describe('Prize', () => {
|
||||
describe('create', () => {
|
||||
it('should create a prize with required fields', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
});
|
||||
|
||||
expect(prize.id.toString()).toBe('prize-1');
|
||||
expect(prize.seasonId.toString()).toBe('season-1');
|
||||
expect(prize.position.toNumber()).toBe(1);
|
||||
expect(prize.amount).toEqual(amount);
|
||||
expect(prize.driverId).toBeUndefined();
|
||||
expect(prize.status.toString()).toBe('pending');
|
||||
expect(prize.awardedAt).toBeUndefined();
|
||||
expect(prize.paidAt).toBeUndefined();
|
||||
expect(prize.description).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create a prize with all fields', () => {
|
||||
const amount = Money.create(2000, 'USD');
|
||||
const createdAt = new Date('2023-01-01');
|
||||
const awardedAt = new Date('2023-01-02');
|
||||
const paidAt = new Date('2023-01-03');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 2,
|
||||
amount,
|
||||
driverId: 'driver-1',
|
||||
status: 'paid',
|
||||
createdAt,
|
||||
awardedAt,
|
||||
paidAt,
|
||||
description: 'First place prize',
|
||||
});
|
||||
|
||||
expect(prize.id.toString()).toBe('prize-1');
|
||||
expect(prize.seasonId.toString()).toBe('season-1');
|
||||
expect(prize.position.toNumber()).toBe(2);
|
||||
expect(prize.amount).toEqual(amount);
|
||||
expect(prize.driverId!.toString()).toBe('driver-1');
|
||||
expect(prize.status.toString()).toBe('paid');
|
||||
expect(prize.createdAt).toEqual(createdAt);
|
||||
expect(prize.awardedAt).toEqual(awardedAt);
|
||||
expect(prize.paidAt).toEqual(paidAt);
|
||||
expect(prize.description).toBe('First place prize');
|
||||
});
|
||||
|
||||
it('should throw error for invalid id', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
expect(() => Prize.create({
|
||||
id: '',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
})).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for invalid seasonId', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
expect(() => Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: '',
|
||||
position: 1,
|
||||
amount,
|
||||
})).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for invalid position', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
expect(() => Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 0,
|
||||
amount,
|
||||
})).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for non-integer position', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
expect(() => Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1.5,
|
||||
amount,
|
||||
})).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for zero amount', () => {
|
||||
const amount = Money.create(0, 'USD');
|
||||
expect(() => Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
})).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awardTo', () => {
|
||||
it('should award prize to a driver', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
});
|
||||
const awarded = prize.awardTo('driver-1');
|
||||
expect(awarded.driverId!.toString()).toBe('driver-1');
|
||||
expect(awarded.status.toString()).toBe('awarded');
|
||||
expect(awarded.awardedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should throw error for empty driverId', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
});
|
||||
expect(() => prize.awardTo('')).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error if not pending', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
status: 'awarded',
|
||||
});
|
||||
expect(() => prize.awardTo('driver-1')).toThrow(RacingDomainInvariantError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markAsPaid', () => {
|
||||
it('should mark prize as paid', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
driverId: 'driver-1',
|
||||
status: 'awarded',
|
||||
});
|
||||
const paid = prize.markAsPaid();
|
||||
expect(paid.status.toString()).toBe('paid');
|
||||
expect(paid.paidAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should throw error if not awarded', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
});
|
||||
expect(() => prize.markAsPaid()).toThrow(RacingDomainInvariantError);
|
||||
});
|
||||
|
||||
it('should throw error if no driverId', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
status: 'awarded',
|
||||
});
|
||||
expect(() => prize.markAsPaid()).toThrow(RacingDomainInvariantError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancel', () => {
|
||||
it('should cancel pending prize', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
});
|
||||
const cancelled = prize.cancel();
|
||||
expect(cancelled.status.toString()).toBe('cancelled');
|
||||
});
|
||||
|
||||
it('should cancel awarded prize', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
driverId: 'driver-1',
|
||||
status: 'awarded',
|
||||
});
|
||||
const cancelled = prize.cancel();
|
||||
expect(cancelled.status.toString()).toBe('cancelled');
|
||||
});
|
||||
|
||||
it('should throw error if paid', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
driverId: 'driver-1',
|
||||
status: 'paid',
|
||||
});
|
||||
expect(() => prize.cancel()).toThrow(RacingDomainInvariantError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPending', () => {
|
||||
it('should return true for pending status', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
});
|
||||
expect(prize.isPending()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for awarded status', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
status: 'awarded',
|
||||
});
|
||||
expect(prize.isPending()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAwarded', () => {
|
||||
it('should return true for awarded status', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
status: 'awarded',
|
||||
});
|
||||
expect(prize.isAwarded()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for pending status', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
});
|
||||
expect(prize.isAwarded()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPaid', () => {
|
||||
it('should return true for paid status', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
status: 'paid',
|
||||
});
|
||||
expect(prize.isPaid()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for awarded status', () => {
|
||||
const amount = Money.create(1000, 'USD');
|
||||
const prize = Prize.create({
|
||||
id: 'prize-1',
|
||||
seasonId: 'season-1',
|
||||
position: 1,
|
||||
amount,
|
||||
status: 'awarded',
|
||||
});
|
||||
expect(prize.isPaid()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
169
core/racing/domain/entities/prize/Prize.ts
Normal file
169
core/racing/domain/entities/prize/Prize.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Domain Entity: Prize
|
||||
*
|
||||
* Represents a prize awarded to a driver for a specific position in a season.
|
||||
*/
|
||||
|
||||
import { RacingDomainValidationError, RacingDomainInvariantError } from '../../errors/RacingDomainError';
|
||||
import type { IEntity } from '@core/shared/domain';
|
||||
|
||||
import type { Money } from '../../value-objects/Money';
|
||||
import { Position } from '../championship/Position';
|
||||
import { PrizeId } from './PrizeId';
|
||||
import { PrizeStatus } from './PrizeStatus';
|
||||
import { SeasonId } from '../SeasonId';
|
||||
import { DriverId } from '../DriverId';
|
||||
|
||||
export interface PrizeProps {
|
||||
id: PrizeId;
|
||||
seasonId: SeasonId;
|
||||
position: Position;
|
||||
amount: Money;
|
||||
driverId: DriverId | undefined;
|
||||
status: PrizeStatus;
|
||||
createdAt: Date;
|
||||
awardedAt: Date | undefined;
|
||||
paidAt: Date | undefined;
|
||||
description: string | undefined;
|
||||
}
|
||||
|
||||
export class Prize implements IEntity<PrizeId> {
|
||||
readonly id: PrizeId;
|
||||
readonly seasonId: SeasonId;
|
||||
readonly position: Position;
|
||||
readonly amount: Money;
|
||||
readonly driverId: DriverId | undefined;
|
||||
readonly status: PrizeStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly awardedAt: Date | undefined;
|
||||
readonly paidAt: Date | undefined;
|
||||
readonly description: string | undefined;
|
||||
|
||||
private constructor(props: PrizeProps) {
|
||||
this.id = props.id;
|
||||
this.seasonId = props.seasonId;
|
||||
this.position = props.position;
|
||||
this.amount = props.amount;
|
||||
this.driverId = props.driverId;
|
||||
this.status = props.status;
|
||||
this.createdAt = props.createdAt;
|
||||
this.awardedAt = props.awardedAt;
|
||||
this.paidAt = props.paidAt;
|
||||
this.description = props.description;
|
||||
}
|
||||
|
||||
static create(props: Omit<PrizeProps, 'createdAt' | 'status' | 'driverId' | 'awardedAt' | 'paidAt' | 'description' | 'id' | 'seasonId' | 'position'> & {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
position: number;
|
||||
createdAt?: Date;
|
||||
status?: string;
|
||||
driverId?: string;
|
||||
awardedAt?: Date;
|
||||
paidAt?: Date;
|
||||
description?: string;
|
||||
}): Prize {
|
||||
const fullProps: Omit<PrizeProps, 'createdAt' | 'status'> = {
|
||||
id: PrizeId.create(props.id),
|
||||
seasonId: SeasonId.create(props.seasonId),
|
||||
position: Position.create(props.position),
|
||||
amount: props.amount,
|
||||
driverId: props.driverId ? DriverId.create(props.driverId) : undefined,
|
||||
awardedAt: props.awardedAt,
|
||||
paidAt: props.paidAt,
|
||||
description: props.description,
|
||||
};
|
||||
|
||||
this.validate(fullProps);
|
||||
|
||||
return new Prize({
|
||||
...fullProps,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
status: PrizeStatus.create(props.status ?? 'pending'),
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<PrizeProps, 'createdAt' | 'status'>): void {
|
||||
if (!props.amount) {
|
||||
throw new RacingDomainValidationError('Prize amount is required');
|
||||
}
|
||||
|
||||
if (props.amount.amount <= 0) {
|
||||
throw new RacingDomainValidationError('Prize amount must be greater than zero');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Award prize to a driver
|
||||
*/
|
||||
awardTo(driverId: string): Prize {
|
||||
if (!driverId || driverId.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Driver ID is required to award prize');
|
||||
}
|
||||
|
||||
if (this.status.toString() !== 'pending') {
|
||||
throw new RacingDomainInvariantError('Only pending prizes can be awarded');
|
||||
}
|
||||
|
||||
return new Prize({
|
||||
...this,
|
||||
driverId: DriverId.create(driverId),
|
||||
status: PrizeStatus.create('awarded'),
|
||||
awardedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark prize as paid
|
||||
*/
|
||||
markAsPaid(): Prize {
|
||||
if (this.status.toString() !== 'awarded') {
|
||||
throw new RacingDomainInvariantError('Only awarded prizes can be marked as paid');
|
||||
}
|
||||
|
||||
if (!this.driverId) {
|
||||
throw new RacingDomainInvariantError('Prize must have a driver to be paid');
|
||||
}
|
||||
|
||||
return new Prize({
|
||||
...this,
|
||||
status: PrizeStatus.create('paid'),
|
||||
paidAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel prize
|
||||
*/
|
||||
cancel(): Prize {
|
||||
if (this.status.toString() === 'paid') {
|
||||
throw new RacingDomainInvariantError('Cannot cancel a paid prize');
|
||||
}
|
||||
|
||||
return new Prize({
|
||||
...this,
|
||||
status: PrizeStatus.create('cancelled'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prize is pending
|
||||
*/
|
||||
isPending(): boolean {
|
||||
return this.status.toString() === 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prize is awarded
|
||||
*/
|
||||
isAwarded(): boolean {
|
||||
return this.status.toString() === 'awarded';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prize is paid
|
||||
*/
|
||||
isPaid(): boolean {
|
||||
return this.status.toString() === 'paid';
|
||||
}
|
||||
}
|
||||
38
core/racing/domain/entities/prize/PrizeId.test.ts
Normal file
38
core/racing/domain/entities/prize/PrizeId.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { PrizeId } from './PrizeId';
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
describe('PrizeId', () => {
|
||||
describe('create', () => {
|
||||
it('should create a PrizeId with valid value', () => {
|
||||
const id = PrizeId.create('prize-123');
|
||||
expect(id.toString()).toBe('prize-123');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
const id = PrizeId.create(' prize-123 ');
|
||||
expect(id.toString()).toBe('prize-123');
|
||||
});
|
||||
|
||||
it('should throw error for empty string', () => {
|
||||
expect(() => PrizeId.create('')).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for whitespace only', () => {
|
||||
expect(() => PrizeId.create(' ')).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equals', () => {
|
||||
it('should return true for equal ids', () => {
|
||||
const id1 = PrizeId.create('prize-123');
|
||||
const id2 = PrizeId.create('prize-123');
|
||||
expect(id1.equals(id2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different ids', () => {
|
||||
const id1 = PrizeId.create('prize-123');
|
||||
const id2 = PrizeId.create('prize-456');
|
||||
expect(id1.equals(id2)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
core/racing/domain/entities/prize/PrizeId.ts
Normal file
20
core/racing/domain/entities/prize/PrizeId.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
export class PrizeId {
|
||||
private constructor(private readonly value: string) {}
|
||||
|
||||
static create(value: string): PrizeId {
|
||||
if (!value || value.trim().length === 0) {
|
||||
throw new RacingDomainValidationError('Prize ID cannot be empty');
|
||||
}
|
||||
return new PrizeId(value.trim());
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: PrizeId): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
42
core/racing/domain/entities/prize/PrizeStatus.test.ts
Normal file
42
core/racing/domain/entities/prize/PrizeStatus.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { PrizeStatus } from './PrizeStatus';
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
describe('PrizeStatus', () => {
|
||||
describe('create', () => {
|
||||
it('should create a PrizeStatus with valid value', () => {
|
||||
const status = PrizeStatus.create('pending');
|
||||
expect(status.toString()).toBe('pending');
|
||||
});
|
||||
|
||||
it('should create all valid statuses', () => {
|
||||
const validStatuses = ['pending', 'awarded', 'paid', 'cancelled'];
|
||||
|
||||
validStatuses.forEach(statusValue => {
|
||||
const status = PrizeStatus.create(statusValue);
|
||||
expect(status.toString()).toBe(statusValue);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error for invalid status', () => {
|
||||
expect(() => PrizeStatus.create('invalid_status')).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
|
||||
it('should throw error for empty string', () => {
|
||||
expect(() => PrizeStatus.create('')).toThrow(RacingDomainValidationError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equals', () => {
|
||||
it('should return true for equal statuses', () => {
|
||||
const status1 = PrizeStatus.create('pending');
|
||||
const status2 = PrizeStatus.create('pending');
|
||||
expect(status1.equals(status2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different statuses', () => {
|
||||
const status1 = PrizeStatus.create('pending');
|
||||
const status2 = PrizeStatus.create('awarded');
|
||||
expect(status1.equals(status2)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
25
core/racing/domain/entities/prize/PrizeStatus.ts
Normal file
25
core/racing/domain/entities/prize/PrizeStatus.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { RacingDomainValidationError } from '../../errors/RacingDomainError';
|
||||
|
||||
export type PrizeStatusValue = 'pending' | 'awarded' | 'paid' | 'cancelled';
|
||||
|
||||
export class PrizeStatus {
|
||||
private constructor(private readonly value: PrizeStatusValue) {}
|
||||
|
||||
static create(value: string): PrizeStatus {
|
||||
const validStatuses: PrizeStatusValue[] = ['pending', 'awarded', 'paid', 'cancelled'];
|
||||
|
||||
if (!validStatuses.includes(value as PrizeStatusValue)) {
|
||||
throw new RacingDomainValidationError(`Invalid prize status: ${value}`);
|
||||
}
|
||||
|
||||
return new PrizeStatus(value as PrizeStatusValue);
|
||||
}
|
||||
|
||||
toString(): PrizeStatusValue {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: PrizeStatus): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user