wip
This commit is contained in:
203
packages/racing/domain/entities/DriverLivery.ts
Normal file
203
packages/racing/domain/entities/DriverLivery.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Domain Entity: DriverLivery
|
||||
*
|
||||
* Represents a driver's custom livery for a specific car.
|
||||
* Includes user-placed decals and league-specific overrides.
|
||||
*/
|
||||
|
||||
import type { LiveryDecal } from '../value-objects/LiveryDecal';
|
||||
|
||||
export interface DecalOverride {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
decalId: string;
|
||||
newX: number;
|
||||
newY: number;
|
||||
}
|
||||
|
||||
export interface DriverLiveryProps {
|
||||
id: string;
|
||||
driverId: string;
|
||||
carId: string;
|
||||
uploadedImageUrl: string;
|
||||
userDecals: LiveryDecal[];
|
||||
leagueOverrides: DecalOverride[];
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
validatedAt?: Date;
|
||||
}
|
||||
|
||||
export class DriverLivery {
|
||||
readonly id: string;
|
||||
readonly driverId: string;
|
||||
readonly carId: string;
|
||||
readonly uploadedImageUrl: string;
|
||||
readonly userDecals: LiveryDecal[];
|
||||
readonly leagueOverrides: DecalOverride[];
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt?: Date;
|
||||
readonly validatedAt?: Date;
|
||||
|
||||
private constructor(props: DriverLiveryProps) {
|
||||
this.id = props.id;
|
||||
this.driverId = props.driverId;
|
||||
this.carId = props.carId;
|
||||
this.uploadedImageUrl = props.uploadedImageUrl;
|
||||
this.userDecals = props.userDecals;
|
||||
this.leagueOverrides = props.leagueOverrides;
|
||||
this.createdAt = props.createdAt;
|
||||
this.updatedAt = props.updatedAt;
|
||||
this.validatedAt = props.validatedAt;
|
||||
}
|
||||
|
||||
static create(props: Omit<DriverLiveryProps, 'createdAt' | 'userDecals' | 'leagueOverrides'> & {
|
||||
createdAt?: Date;
|
||||
userDecals?: LiveryDecal[];
|
||||
leagueOverrides?: DecalOverride[];
|
||||
}): DriverLivery {
|
||||
this.validate(props);
|
||||
|
||||
return new DriverLivery({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
userDecals: props.userDecals ?? [],
|
||||
leagueOverrides: props.leagueOverrides ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<DriverLiveryProps, 'createdAt' | 'userDecals' | 'leagueOverrides'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('DriverLivery ID is required');
|
||||
}
|
||||
|
||||
if (!props.driverId || props.driverId.trim().length === 0) {
|
||||
throw new Error('DriverLivery driverId is required');
|
||||
}
|
||||
|
||||
if (!props.carId || props.carId.trim().length === 0) {
|
||||
throw new Error('DriverLivery carId is required');
|
||||
}
|
||||
|
||||
if (!props.uploadedImageUrl || props.uploadedImageUrl.trim().length === 0) {
|
||||
throw new Error('DriverLivery uploadedImageUrl is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user decal
|
||||
*/
|
||||
addDecal(decal: LiveryDecal): DriverLivery {
|
||||
if (decal.type !== 'user') {
|
||||
throw new Error('Only user decals can be added to driver livery');
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
userDecals: [...this.userDecals, decal],
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user decal
|
||||
*/
|
||||
removeDecal(decalId: string): DriverLivery {
|
||||
const updatedDecals = this.userDecals.filter(d => d.id !== decalId);
|
||||
|
||||
if (updatedDecals.length === this.userDecals.length) {
|
||||
throw new Error('Decal not found in livery');
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
userDecals: updatedDecals,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user decal
|
||||
*/
|
||||
updateDecal(decalId: string, updatedDecal: LiveryDecal): DriverLivery {
|
||||
const index = this.userDecals.findIndex(d => d.id === decalId);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('Decal not found in livery');
|
||||
}
|
||||
|
||||
const updatedDecals = [...this.userDecals];
|
||||
updatedDecals[index] = updatedDecal;
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
userDecals: updatedDecals,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a league-specific decal override
|
||||
*/
|
||||
setLeagueOverride(leagueId: string, seasonId: string, decalId: string, newX: number, newY: number): DriverLivery {
|
||||
const existingIndex = this.leagueOverrides.findIndex(
|
||||
o => o.leagueId === leagueId && o.seasonId === seasonId && o.decalId === decalId
|
||||
);
|
||||
|
||||
const override: DecalOverride = { leagueId, seasonId, decalId, newX, newY };
|
||||
|
||||
let updatedOverrides: DecalOverride[];
|
||||
if (existingIndex >= 0) {
|
||||
updatedOverrides = [...this.leagueOverrides];
|
||||
updatedOverrides[existingIndex] = override;
|
||||
} else {
|
||||
updatedOverrides = [...this.leagueOverrides, override];
|
||||
}
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
leagueOverrides: updatedOverrides,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a league-specific override
|
||||
*/
|
||||
removeLeagueOverride(leagueId: string, seasonId: string, decalId: string): DriverLivery {
|
||||
const updatedOverrides = this.leagueOverrides.filter(
|
||||
o => !(o.leagueId === leagueId && o.seasonId === seasonId && o.decalId === decalId)
|
||||
);
|
||||
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
leagueOverrides: updatedOverrides,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overrides for a specific league/season
|
||||
*/
|
||||
getOverridesFor(leagueId: string, seasonId: string): DecalOverride[] {
|
||||
return this.leagueOverrides.filter(
|
||||
o => o.leagueId === leagueId && o.seasonId === seasonId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark livery as validated (no logos/text detected)
|
||||
*/
|
||||
markAsValidated(): DriverLivery {
|
||||
return new DriverLivery({
|
||||
...this,
|
||||
validatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if livery is validated
|
||||
*/
|
||||
isValidated(): boolean {
|
||||
return this.validatedAt !== undefined;
|
||||
}
|
||||
}
|
||||
123
packages/racing/domain/entities/LeagueWallet.ts
Normal file
123
packages/racing/domain/entities/LeagueWallet.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Domain Entity: LeagueWallet
|
||||
*
|
||||
* Represents a league's financial wallet.
|
||||
* Aggregate root for managing league finances and transactions.
|
||||
*/
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
import type { Transaction } from './Transaction';
|
||||
|
||||
export interface LeagueWalletProps {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
balance: Money;
|
||||
transactionIds: string[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class LeagueWallet {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly balance: Money;
|
||||
readonly transactionIds: string[];
|
||||
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: Omit<LeagueWalletProps, 'createdAt' | 'transactionIds'> & {
|
||||
createdAt?: Date;
|
||||
transactionIds?: string[];
|
||||
}): LeagueWallet {
|
||||
this.validate(props);
|
||||
|
||||
return new LeagueWallet({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
transactionIds: props.transactionIds ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<LeagueWalletProps, 'createdAt' | 'transactionIds'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('LeagueWallet ID is required');
|
||||
}
|
||||
|
||||
if (!props.leagueId || props.leagueId.trim().length === 0) {
|
||||
throw new Error('LeagueWallet leagueId is required');
|
||||
}
|
||||
|
||||
if (!props.balance) {
|
||||
throw new Error('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 Error('Cannot add funds with different currency');
|
||||
}
|
||||
|
||||
const newBalance = this.balance.add(netAmount);
|
||||
|
||||
return new LeagueWallet({
|
||||
...this,
|
||||
balance: newBalance,
|
||||
transactionIds: [...this.transactionIds, transactionId],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Error('Cannot withdraw funds with different currency');
|
||||
}
|
||||
|
||||
if (!this.balance.isGreaterThan(amount) && !this.balance.equals(amount)) {
|
||||
throw new Error('Insufficient balance for withdrawal');
|
||||
}
|
||||
|
||||
const newBalance = this.balance.subtract(amount);
|
||||
|
||||
return new LeagueWallet({
|
||||
...this,
|
||||
balance: newBalance,
|
||||
transactionIds: [...this.transactionIds, transactionId],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
142
packages/racing/domain/entities/LiveryTemplate.ts
Normal file
142
packages/racing/domain/entities/LiveryTemplate.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Domain Entity: LiveryTemplate
|
||||
*
|
||||
* Represents an admin-defined livery template for a specific car.
|
||||
* Contains base image and sponsor decal placements.
|
||||
*/
|
||||
|
||||
import type { LiveryDecal } from '../value-objects/LiveryDecal';
|
||||
|
||||
export interface LiveryTemplateProps {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
carId: string;
|
||||
baseImageUrl: string;
|
||||
adminDecals: LiveryDecal[];
|
||||
createdAt: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export class LiveryTemplate {
|
||||
readonly id: string;
|
||||
readonly leagueId: string;
|
||||
readonly seasonId: string;
|
||||
readonly carId: string;
|
||||
readonly baseImageUrl: string;
|
||||
readonly adminDecals: LiveryDecal[];
|
||||
readonly createdAt: Date;
|
||||
readonly updatedAt?: Date;
|
||||
|
||||
private constructor(props: LiveryTemplateProps) {
|
||||
this.id = props.id;
|
||||
this.leagueId = props.leagueId;
|
||||
this.seasonId = props.seasonId;
|
||||
this.carId = props.carId;
|
||||
this.baseImageUrl = props.baseImageUrl;
|
||||
this.adminDecals = props.adminDecals;
|
||||
this.createdAt = props.createdAt;
|
||||
this.updatedAt = props.updatedAt;
|
||||
}
|
||||
|
||||
static create(props: Omit<LiveryTemplateProps, 'createdAt' | 'adminDecals'> & {
|
||||
createdAt?: Date;
|
||||
adminDecals?: LiveryDecal[];
|
||||
}): LiveryTemplate {
|
||||
this.validate(props);
|
||||
|
||||
return new LiveryTemplate({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
adminDecals: props.adminDecals ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<LiveryTemplateProps, 'createdAt' | 'adminDecals'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('LiveryTemplate ID is required');
|
||||
}
|
||||
|
||||
if (!props.leagueId || props.leagueId.trim().length === 0) {
|
||||
throw new Error('LiveryTemplate leagueId is required');
|
||||
}
|
||||
|
||||
if (!props.seasonId || props.seasonId.trim().length === 0) {
|
||||
throw new Error('LiveryTemplate seasonId is required');
|
||||
}
|
||||
|
||||
if (!props.carId || props.carId.trim().length === 0) {
|
||||
throw new Error('LiveryTemplate carId is required');
|
||||
}
|
||||
|
||||
if (!props.baseImageUrl || props.baseImageUrl.trim().length === 0) {
|
||||
throw new Error('LiveryTemplate baseImageUrl is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a decal to the template
|
||||
*/
|
||||
addDecal(decal: LiveryDecal): LiveryTemplate {
|
||||
if (decal.type !== 'sponsor') {
|
||||
throw new Error('Only sponsor decals can be added to admin template');
|
||||
}
|
||||
|
||||
return new LiveryTemplate({
|
||||
...this,
|
||||
adminDecals: [...this.adminDecals, decal],
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a decal from the template
|
||||
*/
|
||||
removeDecal(decalId: string): LiveryTemplate {
|
||||
const updatedDecals = this.adminDecals.filter(d => d.id !== decalId);
|
||||
|
||||
if (updatedDecals.length === this.adminDecals.length) {
|
||||
throw new Error('Decal not found in template');
|
||||
}
|
||||
|
||||
return new LiveryTemplate({
|
||||
...this,
|
||||
adminDecals: updatedDecals,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a decal position
|
||||
*/
|
||||
updateDecal(decalId: string, updatedDecal: LiveryDecal): LiveryTemplate {
|
||||
const index = this.adminDecals.findIndex(d => d.id === decalId);
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error('Decal not found in template');
|
||||
}
|
||||
|
||||
const updatedDecals = [...this.adminDecals];
|
||||
updatedDecals[index] = updatedDecal;
|
||||
|
||||
return new LiveryTemplate({
|
||||
...this,
|
||||
adminDecals: updatedDecals,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sponsor decals
|
||||
*/
|
||||
getSponsorDecals(): LiveryDecal[] {
|
||||
return this.adminDecals.filter(d => d.type === 'sponsor');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if template has sponsor decals
|
||||
*/
|
||||
hasSponsorDecals(): boolean {
|
||||
return this.adminDecals.some(d => d.type === 'sponsor');
|
||||
}
|
||||
}
|
||||
157
packages/racing/domain/entities/Prize.ts
Normal file
157
packages/racing/domain/entities/Prize.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Domain Entity: Prize
|
||||
*
|
||||
* Represents a prize awarded to a driver for a specific position in a season.
|
||||
*/
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
|
||||
export type PrizeStatus = 'pending' | 'awarded' | 'paid' | 'cancelled';
|
||||
|
||||
export interface PrizeProps {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
position: number;
|
||||
amount: Money;
|
||||
driverId?: string;
|
||||
status: PrizeStatus;
|
||||
createdAt: Date;
|
||||
awardedAt?: Date;
|
||||
paidAt?: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class Prize {
|
||||
readonly id: string;
|
||||
readonly seasonId: string;
|
||||
readonly position: number;
|
||||
readonly amount: Money;
|
||||
readonly driverId?: string;
|
||||
readonly status: PrizeStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly awardedAt?: Date;
|
||||
readonly paidAt?: Date;
|
||||
readonly description?: string;
|
||||
|
||||
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'> & {
|
||||
createdAt?: Date;
|
||||
status?: PrizeStatus;
|
||||
}): Prize {
|
||||
this.validate(props);
|
||||
|
||||
return new Prize({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
status: props.status ?? 'pending',
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<PrizeProps, 'createdAt' | 'status'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('Prize ID is required');
|
||||
}
|
||||
|
||||
if (!props.seasonId || props.seasonId.trim().length === 0) {
|
||||
throw new Error('Prize seasonId is required');
|
||||
}
|
||||
|
||||
if (!Number.isInteger(props.position) || props.position < 1) {
|
||||
throw new Error('Prize position must be a positive integer');
|
||||
}
|
||||
|
||||
if (!props.amount) {
|
||||
throw new Error('Prize amount is required');
|
||||
}
|
||||
|
||||
if (props.amount.amount <= 0) {
|
||||
throw new Error('Prize amount must be greater than zero');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Award prize to a driver
|
||||
*/
|
||||
awardTo(driverId: string): Prize {
|
||||
if (!driverId || driverId.trim().length === 0) {
|
||||
throw new Error('Driver ID is required to award prize');
|
||||
}
|
||||
|
||||
if (this.status !== 'pending') {
|
||||
throw new Error('Only pending prizes can be awarded');
|
||||
}
|
||||
|
||||
return new Prize({
|
||||
...this,
|
||||
driverId,
|
||||
status: 'awarded',
|
||||
awardedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark prize as paid
|
||||
*/
|
||||
markAsPaid(): Prize {
|
||||
if (this.status !== 'awarded') {
|
||||
throw new Error('Only awarded prizes can be marked as paid');
|
||||
}
|
||||
|
||||
if (!this.driverId) {
|
||||
throw new Error('Prize must have a driver to be paid');
|
||||
}
|
||||
|
||||
return new Prize({
|
||||
...this,
|
||||
status: 'paid',
|
||||
paidAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel prize
|
||||
*/
|
||||
cancel(): Prize {
|
||||
if (this.status === 'paid') {
|
||||
throw new Error('Cannot cancel a paid prize');
|
||||
}
|
||||
|
||||
return new Prize({
|
||||
...this,
|
||||
status: 'cancelled',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prize is pending
|
||||
*/
|
||||
isPending(): boolean {
|
||||
return this.status === 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prize is awarded
|
||||
*/
|
||||
isAwarded(): boolean {
|
||||
return this.status === 'awarded';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prize is paid
|
||||
*/
|
||||
isPaid(): boolean {
|
||||
return this.status === 'paid';
|
||||
}
|
||||
}
|
||||
@@ -74,4 +74,25 @@ export class Season {
|
||||
endDate: props.endDate,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain rule: Wallet withdrawals are only allowed when season is completed
|
||||
*/
|
||||
canWithdrawFromWallet(): boolean {
|
||||
return this.status === 'completed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is active
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.status === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if season is completed
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return this.status === 'completed';
|
||||
}
|
||||
}
|
||||
140
packages/racing/domain/entities/SeasonSponsorship.ts
Normal file
140
packages/racing/domain/entities/SeasonSponsorship.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Domain Entity: SeasonSponsorship
|
||||
*
|
||||
* Represents a sponsorship relationship between a Sponsor and a Season.
|
||||
* Aggregate root for managing sponsorship slots and pricing.
|
||||
*/
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
|
||||
export type SponsorshipTier = 'main' | 'secondary';
|
||||
export type SponsorshipStatus = 'pending' | 'active' | 'cancelled';
|
||||
|
||||
export interface SeasonSponsorshipProps {
|
||||
id: string;
|
||||
seasonId: string;
|
||||
sponsorId: string;
|
||||
tier: SponsorshipTier;
|
||||
pricing: Money;
|
||||
status: SponsorshipStatus;
|
||||
createdAt: Date;
|
||||
activatedAt?: Date;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class SeasonSponsorship {
|
||||
readonly id: string;
|
||||
readonly seasonId: string;
|
||||
readonly sponsorId: string;
|
||||
readonly tier: SponsorshipTier;
|
||||
readonly pricing: Money;
|
||||
readonly status: SponsorshipStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly activatedAt?: Date;
|
||||
readonly description?: string;
|
||||
|
||||
private constructor(props: SeasonSponsorshipProps) {
|
||||
this.id = props.id;
|
||||
this.seasonId = props.seasonId;
|
||||
this.sponsorId = props.sponsorId;
|
||||
this.tier = props.tier;
|
||||
this.pricing = props.pricing;
|
||||
this.status = props.status;
|
||||
this.createdAt = props.createdAt;
|
||||
this.activatedAt = props.activatedAt;
|
||||
this.description = props.description;
|
||||
}
|
||||
|
||||
static create(props: Omit<SeasonSponsorshipProps, 'createdAt' | 'status'> & {
|
||||
createdAt?: Date;
|
||||
status?: SponsorshipStatus;
|
||||
}): SeasonSponsorship {
|
||||
this.validate(props);
|
||||
|
||||
return new SeasonSponsorship({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
status: props.status ?? 'pending',
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<SeasonSponsorshipProps, 'createdAt' | 'status'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('SeasonSponsorship ID is required');
|
||||
}
|
||||
|
||||
if (!props.seasonId || props.seasonId.trim().length === 0) {
|
||||
throw new Error('SeasonSponsorship seasonId is required');
|
||||
}
|
||||
|
||||
if (!props.sponsorId || props.sponsorId.trim().length === 0) {
|
||||
throw new Error('SeasonSponsorship sponsorId is required');
|
||||
}
|
||||
|
||||
if (!props.tier) {
|
||||
throw new Error('SeasonSponsorship tier is required');
|
||||
}
|
||||
|
||||
if (!props.pricing) {
|
||||
throw new Error('SeasonSponsorship pricing is required');
|
||||
}
|
||||
|
||||
if (props.pricing.amount <= 0) {
|
||||
throw new Error('SeasonSponsorship pricing must be greater than zero');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the sponsorship
|
||||
*/
|
||||
activate(): SeasonSponsorship {
|
||||
if (this.status === 'active') {
|
||||
throw new Error('SeasonSponsorship is already active');
|
||||
}
|
||||
|
||||
if (this.status === 'cancelled') {
|
||||
throw new Error('Cannot activate a cancelled SeasonSponsorship');
|
||||
}
|
||||
|
||||
return new SeasonSponsorship({
|
||||
...this,
|
||||
status: 'active',
|
||||
activatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the sponsorship
|
||||
*/
|
||||
cancel(): SeasonSponsorship {
|
||||
if (this.status === 'cancelled') {
|
||||
throw new Error('SeasonSponsorship is already cancelled');
|
||||
}
|
||||
|
||||
return new SeasonSponsorship({
|
||||
...this,
|
||||
status: 'cancelled',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sponsorship is active
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.status === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the platform fee for this sponsorship
|
||||
*/
|
||||
getPlatformFee(): Money {
|
||||
return this.pricing.calculatePlatformFee();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the net amount after platform fee
|
||||
*/
|
||||
getNetAmount(): Money {
|
||||
return this.pricing.calculateNetAmount();
|
||||
}
|
||||
}
|
||||
96
packages/racing/domain/entities/Sponsor.ts
Normal file
96
packages/racing/domain/entities/Sponsor.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Domain Entity: Sponsor
|
||||
*
|
||||
* Represents a sponsor that can sponsor leagues/seasons.
|
||||
* Aggregate root for sponsor information.
|
||||
*/
|
||||
|
||||
export interface SponsorProps {
|
||||
id: string;
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export class Sponsor {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly contactEmail: string;
|
||||
readonly logoUrl?: string;
|
||||
readonly websiteUrl?: string;
|
||||
readonly createdAt: Date;
|
||||
|
||||
private constructor(props: SponsorProps) {
|
||||
this.id = props.id;
|
||||
this.name = props.name;
|
||||
this.contactEmail = props.contactEmail;
|
||||
this.logoUrl = props.logoUrl;
|
||||
this.websiteUrl = props.websiteUrl;
|
||||
this.createdAt = props.createdAt;
|
||||
}
|
||||
|
||||
static create(props: Omit<SponsorProps, 'createdAt'> & { createdAt?: Date }): Sponsor {
|
||||
this.validate(props);
|
||||
|
||||
return new Sponsor({
|
||||
...props,
|
||||
createdAt: props.createdAt ?? new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
private static validate(props: Omit<SponsorProps, 'createdAt'>): void {
|
||||
if (!props.id || props.id.trim().length === 0) {
|
||||
throw new Error('Sponsor ID is required');
|
||||
}
|
||||
|
||||
if (!props.name || props.name.trim().length === 0) {
|
||||
throw new Error('Sponsor name is required');
|
||||
}
|
||||
|
||||
if (props.name.length > 100) {
|
||||
throw new Error('Sponsor name must be 100 characters or less');
|
||||
}
|
||||
|
||||
if (!props.contactEmail || props.contactEmail.trim().length === 0) {
|
||||
throw new Error('Sponsor contact email is required');
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(props.contactEmail)) {
|
||||
throw new Error('Invalid sponsor contact email format');
|
||||
}
|
||||
|
||||
if (props.websiteUrl && props.websiteUrl.trim().length > 0) {
|
||||
try {
|
||||
new URL(props.websiteUrl);
|
||||
} catch {
|
||||
throw new Error('Invalid sponsor website URL');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sponsor information
|
||||
*/
|
||||
update(props: Partial<{
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
logoUrl: string | undefined;
|
||||
websiteUrl: string | undefined;
|
||||
}>): Sponsor {
|
||||
const updated = {
|
||||
id: this.id,
|
||||
name: props.name ?? this.name,
|
||||
contactEmail: props.contactEmail ?? this.contactEmail,
|
||||
logoUrl: props.logoUrl !== undefined ? props.logoUrl : this.logoUrl,
|
||||
websiteUrl: props.websiteUrl !== undefined ? props.websiteUrl : this.websiteUrl,
|
||||
createdAt: this.createdAt,
|
||||
};
|
||||
|
||||
Sponsor.validate(updated);
|
||||
return new Sponsor(updated);
|
||||
}
|
||||
}
|
||||
159
packages/racing/domain/entities/Transaction.ts
Normal file
159
packages/racing/domain/entities/Transaction.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Domain Entity: Transaction
|
||||
*
|
||||
* Represents a financial transaction in the league wallet system.
|
||||
*/
|
||||
|
||||
import type { Money } from '../value-objects/Money';
|
||||
|
||||
export type TransactionType =
|
||||
| 'sponsorship_payment'
|
||||
| 'membership_payment'
|
||||
| 'prize_payout'
|
||||
| 'withdrawal'
|
||||
| 'refund';
|
||||
|
||||
export type TransactionStatus = 'pending' | 'completed' | 'failed' | 'cancelled';
|
||||
|
||||
export interface TransactionProps {
|
||||
id: string;
|
||||
walletId: string;
|
||||
type: TransactionType;
|
||||
amount: Money;
|
||||
platformFee: Money;
|
||||
netAmount: Money;
|
||||
status: TransactionStatus;
|
||||
createdAt: Date;
|
||||
completedAt?: Date;
|
||||
description?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class Transaction {
|
||||
readonly id: string;
|
||||
readonly walletId: string;
|
||||
readonly type: TransactionType;
|
||||
readonly amount: Money;
|
||||
readonly platformFee: Money;
|
||||
readonly netAmount: Money;
|
||||
readonly status: TransactionStatus;
|
||||
readonly createdAt: Date;
|
||||
readonly completedAt?: Date;
|
||||
readonly description?: string;
|
||||
readonly metadata?: Record<string, unknown>;
|
||||
|
||||
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 || props.id.trim().length === 0) {
|
||||
throw new Error('Transaction ID is required');
|
||||
}
|
||||
|
||||
if (!props.walletId || props.walletId.trim().length === 0) {
|
||||
throw new Error('Transaction walletId is required');
|
||||
}
|
||||
|
||||
if (!props.type) {
|
||||
throw new Error('Transaction type is required');
|
||||
}
|
||||
|
||||
if (!props.amount) {
|
||||
throw new Error('Transaction amount is required');
|
||||
}
|
||||
|
||||
if (props.amount.amount <= 0) {
|
||||
throw new Error('Transaction amount must be greater than zero');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark transaction as completed
|
||||
*/
|
||||
complete(): Transaction {
|
||||
if (this.status === 'completed') {
|
||||
throw new Error('Transaction is already completed');
|
||||
}
|
||||
|
||||
if (this.status === 'failed' || this.status === 'cancelled') {
|
||||
throw new Error('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 Error('Cannot fail a completed transaction');
|
||||
}
|
||||
|
||||
return new Transaction({
|
||||
...this,
|
||||
status: 'failed',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel transaction
|
||||
*/
|
||||
cancel(): Transaction {
|
||||
if (this.status === 'completed') {
|
||||
throw new Error('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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user