wip
This commit is contained in:
46
packages/racing/application/ports/ILiveryCompositor.ts
Normal file
46
packages/racing/application/ports/ILiveryCompositor.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Application Port: ILiveryCompositor
|
||||||
|
*
|
||||||
|
* Defines interface for livery image composition.
|
||||||
|
* Infrastructure will provide image processing implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { LiveryDecal } from '../../domain/value-objects/LiveryDecal';
|
||||||
|
|
||||||
|
export interface CompositionResult {
|
||||||
|
success: boolean;
|
||||||
|
composedImageUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILiveryCompositor {
|
||||||
|
/**
|
||||||
|
* Composite a livery by layering decals on base image
|
||||||
|
*/
|
||||||
|
composeLivery(
|
||||||
|
baseImageUrl: string,
|
||||||
|
decals: LiveryDecal[]
|
||||||
|
): Promise<CompositionResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a livery pack (.zip) for all drivers in a season
|
||||||
|
*/
|
||||||
|
generateLiveryPack(
|
||||||
|
seasonId: string,
|
||||||
|
liveryData: Array<{
|
||||||
|
driverId: string;
|
||||||
|
driverName: string;
|
||||||
|
carId: string;
|
||||||
|
composedImageUrl: string;
|
||||||
|
}>
|
||||||
|
): Promise<Buffer>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate livery image (check for logos/text)
|
||||||
|
*/
|
||||||
|
validateLiveryImage(imageUrl: string): Promise<{
|
||||||
|
isValid: boolean;
|
||||||
|
violations?: string[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
39
packages/racing/application/ports/ILiveryStorage.ts
Normal file
39
packages/racing/application/ports/ILiveryStorage.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Application Port: ILiveryStorage
|
||||||
|
*
|
||||||
|
* Defines interface for livery image storage.
|
||||||
|
* Infrastructure will provide cloud storage adapter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UploadResult {
|
||||||
|
success: boolean;
|
||||||
|
imageUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILiveryStorage {
|
||||||
|
/**
|
||||||
|
* Upload a livery image
|
||||||
|
*/
|
||||||
|
upload(
|
||||||
|
imageData: Buffer | string,
|
||||||
|
fileName: string,
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
): Promise<UploadResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a livery image
|
||||||
|
*/
|
||||||
|
download(imageUrl: string): Promise<Buffer>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a livery image
|
||||||
|
*/
|
||||||
|
delete(imageUrl: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a signed URL for temporary access
|
||||||
|
*/
|
||||||
|
generateSignedUrl(imageUrl: string, expiresInSeconds: number): Promise<string>;
|
||||||
|
}
|
||||||
48
packages/racing/application/ports/IPaymentGateway.ts
Normal file
48
packages/racing/application/ports/IPaymentGateway.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Application Port: IPaymentGateway
|
||||||
|
*
|
||||||
|
* Defines interface for payment processing.
|
||||||
|
* Infrastructure will provide mock or real implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Money } from '../../domain/value-objects/Money';
|
||||||
|
|
||||||
|
export interface PaymentResult {
|
||||||
|
success: boolean;
|
||||||
|
transactionId?: string;
|
||||||
|
error?: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefundResult {
|
||||||
|
success: boolean;
|
||||||
|
refundId?: string;
|
||||||
|
error?: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPaymentGateway {
|
||||||
|
/**
|
||||||
|
* Process a payment
|
||||||
|
*/
|
||||||
|
processPayment(
|
||||||
|
amount: Money,
|
||||||
|
payerId: string,
|
||||||
|
description: string,
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
): Promise<PaymentResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refund a payment
|
||||||
|
*/
|
||||||
|
refund(
|
||||||
|
originalTransactionId: string,
|
||||||
|
amount: Money,
|
||||||
|
reason: string
|
||||||
|
): Promise<RefundResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify payment status
|
||||||
|
*/
|
||||||
|
verifyPayment(transactionId: string): Promise<PaymentResult>;
|
||||||
|
}
|
||||||
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,
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Repository Interface: ILeagueWalletRepository
|
||||||
|
*
|
||||||
|
* Defines operations for LeagueWallet aggregate persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { LeagueWallet } from '../entities/LeagueWallet';
|
||||||
|
|
||||||
|
export interface ILeagueWalletRepository {
|
||||||
|
findById(id: string): Promise<LeagueWallet | null>;
|
||||||
|
findByLeagueId(leagueId: string): Promise<LeagueWallet | null>;
|
||||||
|
create(wallet: LeagueWallet): Promise<LeagueWallet>;
|
||||||
|
update(wallet: LeagueWallet): Promise<LeagueWallet>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
exists(id: string): Promise<boolean>;
|
||||||
|
}
|
||||||
26
packages/racing/domain/repositories/ILiveryRepository.ts
Normal file
26
packages/racing/domain/repositories/ILiveryRepository.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Repository Interface: ILiveryRepository
|
||||||
|
*
|
||||||
|
* Defines operations for livery-related entities
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DriverLivery } from '../entities/DriverLivery';
|
||||||
|
import type { LiveryTemplate } from '../entities/LiveryTemplate';
|
||||||
|
|
||||||
|
export interface ILiveryRepository {
|
||||||
|
// DriverLivery operations
|
||||||
|
findDriverLiveryById(id: string): Promise<DriverLivery | null>;
|
||||||
|
findDriverLiveriesByDriverId(driverId: string): Promise<DriverLivery[]>;
|
||||||
|
findDriverLiveryByDriverAndCar(driverId: string, carId: string): Promise<DriverLivery | null>;
|
||||||
|
createDriverLivery(livery: DriverLivery): Promise<DriverLivery>;
|
||||||
|
updateDriverLivery(livery: DriverLivery): Promise<DriverLivery>;
|
||||||
|
deleteDriverLivery(id: string): Promise<void>;
|
||||||
|
|
||||||
|
// LiveryTemplate operations
|
||||||
|
findTemplateById(id: string): Promise<LiveryTemplate | null>;
|
||||||
|
findTemplatesBySeasonId(seasonId: string): Promise<LiveryTemplate[]>;
|
||||||
|
findTemplateBySeasonAndCar(seasonId: string, carId: string): Promise<LiveryTemplate | null>;
|
||||||
|
createTemplate(template: LiveryTemplate): Promise<LiveryTemplate>;
|
||||||
|
updateTemplate(template: LiveryTemplate): Promise<LiveryTemplate>;
|
||||||
|
deleteTemplate(id: string): Promise<void>;
|
||||||
|
}
|
||||||
19
packages/racing/domain/repositories/IPrizeRepository.ts
Normal file
19
packages/racing/domain/repositories/IPrizeRepository.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Repository Interface: IPrizeRepository
|
||||||
|
*
|
||||||
|
* Defines operations for Prize entity persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Prize, PrizeStatus } from '../entities/Prize';
|
||||||
|
|
||||||
|
export interface IPrizeRepository {
|
||||||
|
findById(id: string): Promise<Prize | null>;
|
||||||
|
findBySeasonId(seasonId: string): Promise<Prize[]>;
|
||||||
|
findByDriverId(driverId: string): Promise<Prize[]>;
|
||||||
|
findByStatus(status: PrizeStatus): Promise<Prize[]>;
|
||||||
|
findBySeasonAndPosition(seasonId: string, position: number): Promise<Prize | null>;
|
||||||
|
create(prize: Prize): Promise<Prize>;
|
||||||
|
update(prize: Prize): Promise<Prize>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
exists(id: string): Promise<boolean>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Repository Interface: ISeasonSponsorshipRepository
|
||||||
|
*
|
||||||
|
* Defines operations for SeasonSponsorship aggregate persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SeasonSponsorship, SponsorshipTier } from '../entities/SeasonSponsorship';
|
||||||
|
|
||||||
|
export interface ISeasonSponsorshipRepository {
|
||||||
|
findById(id: string): Promise<SeasonSponsorship | null>;
|
||||||
|
findBySeasonId(seasonId: string): Promise<SeasonSponsorship[]>;
|
||||||
|
findBySponsorId(sponsorId: string): Promise<SeasonSponsorship[]>;
|
||||||
|
findBySeasonAndTier(seasonId: string, tier: SponsorshipTier): Promise<SeasonSponsorship[]>;
|
||||||
|
create(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship>;
|
||||||
|
update(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
exists(id: string): Promise<boolean>;
|
||||||
|
}
|
||||||
17
packages/racing/domain/repositories/ISponsorRepository.ts
Normal file
17
packages/racing/domain/repositories/ISponsorRepository.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Repository Interface: ISponsorRepository
|
||||||
|
*
|
||||||
|
* Defines operations for Sponsor aggregate persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Sponsor } from '../entities/Sponsor';
|
||||||
|
|
||||||
|
export interface ISponsorRepository {
|
||||||
|
findById(id: string): Promise<Sponsor | null>;
|
||||||
|
findAll(): Promise<Sponsor[]>;
|
||||||
|
findByEmail(email: string): Promise<Sponsor | null>;
|
||||||
|
create(sponsor: Sponsor): Promise<Sponsor>;
|
||||||
|
update(sponsor: Sponsor): Promise<Sponsor>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
exists(id: string): Promise<boolean>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Repository Interface: ITransactionRepository
|
||||||
|
*
|
||||||
|
* Defines operations for Transaction entity persistence
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Transaction, TransactionType } from '../entities/Transaction';
|
||||||
|
|
||||||
|
export interface ITransactionRepository {
|
||||||
|
findById(id: string): Promise<Transaction | null>;
|
||||||
|
findByWalletId(walletId: string): Promise<Transaction[]>;
|
||||||
|
findByType(type: TransactionType): Promise<Transaction[]>;
|
||||||
|
create(transaction: Transaction): Promise<Transaction>;
|
||||||
|
update(transaction: Transaction): Promise<Transaction>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
exists(id: string): Promise<boolean>;
|
||||||
|
}
|
||||||
127
packages/racing/domain/value-objects/LiveryDecal.ts
Normal file
127
packages/racing/domain/value-objects/LiveryDecal.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Value Object: LiveryDecal
|
||||||
|
* Represents a decal/logo placed on a livery
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type DecalType = 'sponsor' | 'user';
|
||||||
|
|
||||||
|
export interface LiveryDecalProps {
|
||||||
|
id: string;
|
||||||
|
imageUrl: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
zIndex: number;
|
||||||
|
type: DecalType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LiveryDecal {
|
||||||
|
readonly id: string;
|
||||||
|
readonly imageUrl: string;
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
readonly zIndex: number;
|
||||||
|
readonly type: DecalType;
|
||||||
|
|
||||||
|
private constructor(props: LiveryDecalProps) {
|
||||||
|
this.id = props.id;
|
||||||
|
this.imageUrl = props.imageUrl;
|
||||||
|
this.x = props.x;
|
||||||
|
this.y = props.y;
|
||||||
|
this.width = props.width;
|
||||||
|
this.height = props.height;
|
||||||
|
this.zIndex = props.zIndex;
|
||||||
|
this.type = props.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(props: LiveryDecalProps): LiveryDecal {
|
||||||
|
this.validate(props);
|
||||||
|
return new LiveryDecal(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static validate(props: LiveryDecalProps): void {
|
||||||
|
if (!props.id || props.id.trim().length === 0) {
|
||||||
|
throw new Error('LiveryDecal ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.imageUrl || props.imageUrl.trim().length === 0) {
|
||||||
|
throw new Error('LiveryDecal imageUrl is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.x < 0 || props.x > 1) {
|
||||||
|
throw new Error('LiveryDecal x coordinate must be between 0 and 1 (normalized)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.y < 0 || props.y > 1) {
|
||||||
|
throw new Error('LiveryDecal y coordinate must be between 0 and 1 (normalized)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.width <= 0 || props.width > 1) {
|
||||||
|
throw new Error('LiveryDecal width must be between 0 and 1 (normalized)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.height <= 0 || props.height > 1) {
|
||||||
|
throw new Error('LiveryDecal height must be between 0 and 1 (normalized)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(props.zIndex) || props.zIndex < 0) {
|
||||||
|
throw new Error('LiveryDecal zIndex must be a non-negative integer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.type) {
|
||||||
|
throw new Error('LiveryDecal type is required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move decal to new position
|
||||||
|
*/
|
||||||
|
moveTo(x: number, y: number): LiveryDecal {
|
||||||
|
return LiveryDecal.create({
|
||||||
|
...this,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize decal
|
||||||
|
*/
|
||||||
|
resize(width: number, height: number): LiveryDecal {
|
||||||
|
return LiveryDecal.create({
|
||||||
|
...this,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change z-index
|
||||||
|
*/
|
||||||
|
setZIndex(zIndex: number): LiveryDecal {
|
||||||
|
return LiveryDecal.create({
|
||||||
|
...this,
|
||||||
|
zIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this decal overlaps with another
|
||||||
|
*/
|
||||||
|
overlapsWith(other: LiveryDecal): boolean {
|
||||||
|
const thisRight = this.x + this.width;
|
||||||
|
const thisBottom = this.y + this.height;
|
||||||
|
const otherRight = other.x + other.width;
|
||||||
|
const otherBottom = other.y + other.height;
|
||||||
|
|
||||||
|
return !(
|
||||||
|
thisRight <= other.x ||
|
||||||
|
this.x >= otherRight ||
|
||||||
|
thisBottom <= other.y ||
|
||||||
|
this.y >= otherBottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
packages/racing/domain/value-objects/MembershipFee.ts
Normal file
74
packages/racing/domain/value-objects/MembershipFee.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Value Object: MembershipFee
|
||||||
|
* Represents membership fee configuration for league drivers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Money } from './Money';
|
||||||
|
|
||||||
|
export type MembershipFeeType = 'season' | 'monthly' | 'per_race';
|
||||||
|
|
||||||
|
export interface MembershipFeeProps {
|
||||||
|
type: MembershipFeeType;
|
||||||
|
amount: Money;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MembershipFee {
|
||||||
|
readonly type: MembershipFeeType;
|
||||||
|
readonly amount: Money;
|
||||||
|
|
||||||
|
private constructor(props: MembershipFeeProps) {
|
||||||
|
this.type = props.type;
|
||||||
|
this.amount = props.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(type: MembershipFeeType, amount: Money): MembershipFee {
|
||||||
|
if (!type) {
|
||||||
|
throw new Error('MembershipFee type is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amount) {
|
||||||
|
throw new Error('MembershipFee amount is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount.amount < 0) {
|
||||||
|
throw new Error('MembershipFee amount cannot be negative');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MembershipFee({ type, amount });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get platform fee for this membership fee
|
||||||
|
*/
|
||||||
|
getPlatformFee(): Money {
|
||||||
|
return this.amount.calculatePlatformFee();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get net amount after platform fee
|
||||||
|
*/
|
||||||
|
getNetAmount(): Money {
|
||||||
|
return this.amount.calculateNetAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is a recurring fee
|
||||||
|
*/
|
||||||
|
isRecurring(): boolean {
|
||||||
|
return this.type === 'monthly';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get display name for fee type
|
||||||
|
*/
|
||||||
|
getDisplayName(): string {
|
||||||
|
switch (this.type) {
|
||||||
|
case 'season':
|
||||||
|
return 'Season Fee';
|
||||||
|
case 'monthly':
|
||||||
|
return 'Monthly Subscription';
|
||||||
|
case 'per_race':
|
||||||
|
return 'Per-Race Fee';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
98
packages/racing/domain/value-objects/Money.ts
Normal file
98
packages/racing/domain/value-objects/Money.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Value Object: Money
|
||||||
|
* Represents a monetary amount with currency and platform fee calculation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Currency = 'USD' | 'EUR' | 'GBP';
|
||||||
|
|
||||||
|
export class Money {
|
||||||
|
private static readonly PLATFORM_FEE_PERCENTAGE = 0.10;
|
||||||
|
|
||||||
|
readonly amount: number;
|
||||||
|
readonly currency: Currency;
|
||||||
|
|
||||||
|
private constructor(amount: number, currency: Currency) {
|
||||||
|
this.amount = amount;
|
||||||
|
this.currency = currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(amount: number, currency: Currency = 'USD'): Money {
|
||||||
|
if (amount < 0) {
|
||||||
|
throw new Error('Money amount cannot be negative');
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(amount)) {
|
||||||
|
throw new Error('Money amount must be a finite number');
|
||||||
|
}
|
||||||
|
return new Money(amount, currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate platform fee (10%)
|
||||||
|
*/
|
||||||
|
calculatePlatformFee(): Money {
|
||||||
|
const feeAmount = this.amount * Money.PLATFORM_FEE_PERCENTAGE;
|
||||||
|
return new Money(feeAmount, this.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate net amount after platform fee
|
||||||
|
*/
|
||||||
|
calculateNetAmount(): Money {
|
||||||
|
const platformFee = this.calculatePlatformFee();
|
||||||
|
return new Money(this.amount - platformFee.amount, this.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add two money amounts
|
||||||
|
*/
|
||||||
|
add(other: Money): Money {
|
||||||
|
if (this.currency !== other.currency) {
|
||||||
|
throw new Error('Cannot add money with different currencies');
|
||||||
|
}
|
||||||
|
return new Money(this.amount + other.amount, this.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtract two money amounts
|
||||||
|
*/
|
||||||
|
subtract(other: Money): Money {
|
||||||
|
if (this.currency !== other.currency) {
|
||||||
|
throw new Error('Cannot subtract money with different currencies');
|
||||||
|
}
|
||||||
|
const result = this.amount - other.amount;
|
||||||
|
if (result < 0) {
|
||||||
|
throw new Error('Subtraction would result in negative amount');
|
||||||
|
}
|
||||||
|
return new Money(result, this.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this money is greater than another
|
||||||
|
*/
|
||||||
|
isGreaterThan(other: Money): boolean {
|
||||||
|
if (this.currency !== other.currency) {
|
||||||
|
throw new Error('Cannot compare money with different currencies');
|
||||||
|
}
|
||||||
|
return this.amount > other.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this money equals another
|
||||||
|
*/
|
||||||
|
equals(other: Money): boolean {
|
||||||
|
return this.amount === other.amount && this.currency === other.currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format money for display
|
||||||
|
*/
|
||||||
|
format(): string {
|
||||||
|
const formatter = new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: this.currency,
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
});
|
||||||
|
return formatter.format(this.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: ILeagueWalletRepository
|
||||||
|
*
|
||||||
|
* Mock repository for testing and development
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { LeagueWallet } from '../../domain/entities/LeagueWallet';
|
||||||
|
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
||||||
|
|
||||||
|
export class InMemoryLeagueWalletRepository implements ILeagueWalletRepository {
|
||||||
|
private wallets: Map<string, LeagueWallet> = new Map();
|
||||||
|
|
||||||
|
async findById(id: string): Promise<LeagueWallet | null> {
|
||||||
|
return this.wallets.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByLeagueId(leagueId: string): Promise<LeagueWallet | null> {
|
||||||
|
for (const wallet of this.wallets.values()) {
|
||||||
|
if (wallet.leagueId === leagueId) {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(wallet: LeagueWallet): Promise<LeagueWallet> {
|
||||||
|
if (this.wallets.has(wallet.id)) {
|
||||||
|
throw new Error('LeagueWallet with this ID already exists');
|
||||||
|
}
|
||||||
|
this.wallets.set(wallet.id, wallet);
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(wallet: LeagueWallet): Promise<LeagueWallet> {
|
||||||
|
if (!this.wallets.has(wallet.id)) {
|
||||||
|
throw new Error('LeagueWallet not found');
|
||||||
|
}
|
||||||
|
this.wallets.set(wallet.id, wallet);
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
this.wallets.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(id: string): Promise<boolean> {
|
||||||
|
return this.wallets.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test helper
|
||||||
|
clear(): void {
|
||||||
|
this.wallets.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: ILiveryRepository
|
||||||
|
*
|
||||||
|
* Mock repository for testing and development
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DriverLivery } from '../../domain/entities/DriverLivery';
|
||||||
|
import type { LiveryTemplate } from '../../domain/entities/LiveryTemplate';
|
||||||
|
import type { ILiveryRepository } from '../../domain/repositories/ILiveryRepository';
|
||||||
|
|
||||||
|
export class InMemoryLiveryRepository implements ILiveryRepository {
|
||||||
|
private driverLiveries: Map<string, DriverLivery> = new Map();
|
||||||
|
private templates: Map<string, LiveryTemplate> = new Map();
|
||||||
|
|
||||||
|
// DriverLivery operations
|
||||||
|
async findDriverLiveryById(id: string): Promise<DriverLivery | null> {
|
||||||
|
return this.driverLiveries.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findDriverLiveriesByDriverId(driverId: string): Promise<DriverLivery[]> {
|
||||||
|
return Array.from(this.driverLiveries.values()).filter(l => l.driverId === driverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findDriverLiveryByDriverAndCar(driverId: string, carId: string): Promise<DriverLivery | null> {
|
||||||
|
for (const livery of this.driverLiveries.values()) {
|
||||||
|
if (livery.driverId === driverId && livery.carId === carId) {
|
||||||
|
return livery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDriverLivery(livery: DriverLivery): Promise<DriverLivery> {
|
||||||
|
if (this.driverLiveries.has(livery.id)) {
|
||||||
|
throw new Error('DriverLivery with this ID already exists');
|
||||||
|
}
|
||||||
|
this.driverLiveries.set(livery.id, livery);
|
||||||
|
return livery;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDriverLivery(livery: DriverLivery): Promise<DriverLivery> {
|
||||||
|
if (!this.driverLiveries.has(livery.id)) {
|
||||||
|
throw new Error('DriverLivery not found');
|
||||||
|
}
|
||||||
|
this.driverLiveries.set(livery.id, livery);
|
||||||
|
return livery;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDriverLivery(id: string): Promise<void> {
|
||||||
|
this.driverLiveries.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LiveryTemplate operations
|
||||||
|
async findTemplateById(id: string): Promise<LiveryTemplate | null> {
|
||||||
|
return this.templates.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTemplatesBySeasonId(seasonId: string): Promise<LiveryTemplate[]> {
|
||||||
|
return Array.from(this.templates.values()).filter(t => t.seasonId === seasonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTemplateBySeasonAndCar(seasonId: string, carId: string): Promise<LiveryTemplate | null> {
|
||||||
|
for (const template of this.templates.values()) {
|
||||||
|
if (template.seasonId === seasonId && template.carId === carId) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTemplate(template: LiveryTemplate): Promise<LiveryTemplate> {
|
||||||
|
if (this.templates.has(template.id)) {
|
||||||
|
throw new Error('LiveryTemplate with this ID already exists');
|
||||||
|
}
|
||||||
|
this.templates.set(template.id, template);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTemplate(template: LiveryTemplate): Promise<LiveryTemplate> {
|
||||||
|
if (!this.templates.has(template.id)) {
|
||||||
|
throw new Error('LiveryTemplate not found');
|
||||||
|
}
|
||||||
|
this.templates.set(template.id, template);
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTemplate(id: string): Promise<void> {
|
||||||
|
this.templates.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test helpers
|
||||||
|
clearDriverLiveries(): void {
|
||||||
|
this.driverLiveries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTemplates(): void {
|
||||||
|
this.templates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.driverLiveries.clear();
|
||||||
|
this.templates.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: ISeasonSponsorshipRepository
|
||||||
|
*
|
||||||
|
* Mock repository for testing and development
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SeasonSponsorship, SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||||
|
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
||||||
|
|
||||||
|
export class InMemorySeasonSponsorshipRepository implements ISeasonSponsorshipRepository {
|
||||||
|
private sponsorships: Map<string, SeasonSponsorship> = new Map();
|
||||||
|
|
||||||
|
async findById(id: string): Promise<SeasonSponsorship | null> {
|
||||||
|
return this.sponsorships.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findBySeasonId(seasonId: string): Promise<SeasonSponsorship[]> {
|
||||||
|
return Array.from(this.sponsorships.values()).filter(s => s.seasonId === seasonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findBySponsorId(sponsorId: string): Promise<SeasonSponsorship[]> {
|
||||||
|
return Array.from(this.sponsorships.values()).filter(s => s.sponsorId === sponsorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findBySeasonAndTier(seasonId: string, tier: SponsorshipTier): Promise<SeasonSponsorship[]> {
|
||||||
|
return Array.from(this.sponsorships.values()).filter(
|
||||||
|
s => s.seasonId === seasonId && s.tier === tier
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship> {
|
||||||
|
if (this.sponsorships.has(sponsorship.id)) {
|
||||||
|
throw new Error('SeasonSponsorship with this ID already exists');
|
||||||
|
}
|
||||||
|
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||||
|
return sponsorship;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(sponsorship: SeasonSponsorship): Promise<SeasonSponsorship> {
|
||||||
|
if (!this.sponsorships.has(sponsorship.id)) {
|
||||||
|
throw new Error('SeasonSponsorship not found');
|
||||||
|
}
|
||||||
|
this.sponsorships.set(sponsorship.id, sponsorship);
|
||||||
|
return sponsorship;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
this.sponsorships.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(id: string): Promise<boolean> {
|
||||||
|
return this.sponsorships.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test helper
|
||||||
|
clear(): void {
|
||||||
|
this.sponsorships.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: ISponsorRepository
|
||||||
|
*
|
||||||
|
* Mock repository for testing and development
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Sponsor } from '../../domain/entities/Sponsor';
|
||||||
|
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||||
|
|
||||||
|
export class InMemorySponsorRepository implements ISponsorRepository {
|
||||||
|
private sponsors: Map<string, Sponsor> = new Map();
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Sponsor | null> {
|
||||||
|
return this.sponsors.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Sponsor[]> {
|
||||||
|
return Array.from(this.sponsors.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(email: string): Promise<Sponsor | null> {
|
||||||
|
for (const sponsor of this.sponsors.values()) {
|
||||||
|
if (sponsor.contactEmail === email) {
|
||||||
|
return sponsor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(sponsor: Sponsor): Promise<Sponsor> {
|
||||||
|
if (this.sponsors.has(sponsor.id)) {
|
||||||
|
throw new Error('Sponsor with this ID already exists');
|
||||||
|
}
|
||||||
|
this.sponsors.set(sponsor.id, sponsor);
|
||||||
|
return sponsor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(sponsor: Sponsor): Promise<Sponsor> {
|
||||||
|
if (!this.sponsors.has(sponsor.id)) {
|
||||||
|
throw new Error('Sponsor not found');
|
||||||
|
}
|
||||||
|
this.sponsors.set(sponsor.id, sponsor);
|
||||||
|
return sponsor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
this.sponsors.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(id: string): Promise<boolean> {
|
||||||
|
return this.sponsors.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test helper
|
||||||
|
clear(): void {
|
||||||
|
this.sponsors.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* In-Memory Implementation: ITransactionRepository
|
||||||
|
*
|
||||||
|
* Mock repository for testing and development
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Transaction, TransactionType } from '../../domain/entities/Transaction';
|
||||||
|
import type { ITransactionRepository } from '../../domain/repositories/ITransactionRepository';
|
||||||
|
|
||||||
|
export class InMemoryTransactionRepository implements ITransactionRepository {
|
||||||
|
private transactions: Map<string, Transaction> = new Map();
|
||||||
|
|
||||||
|
async findById(id: string): Promise<Transaction | null> {
|
||||||
|
return this.transactions.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByWalletId(walletId: string): Promise<Transaction[]> {
|
||||||
|
return Array.from(this.transactions.values()).filter(t => t.walletId === walletId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByType(type: TransactionType): Promise<Transaction[]> {
|
||||||
|
return Array.from(this.transactions.values()).filter(t => t.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(transaction: Transaction): Promise<Transaction> {
|
||||||
|
if (this.transactions.has(transaction.id)) {
|
||||||
|
throw new Error('Transaction with this ID already exists');
|
||||||
|
}
|
||||||
|
this.transactions.set(transaction.id, transaction);
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(transaction: Transaction): Promise<Transaction> {
|
||||||
|
if (!this.transactions.has(transaction.id)) {
|
||||||
|
throw new Error('Transaction not found');
|
||||||
|
}
|
||||||
|
this.transactions.set(transaction.id, transaction);
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
this.transactions.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(id: string): Promise<boolean> {
|
||||||
|
return this.transactions.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test helper
|
||||||
|
clear(): void {
|
||||||
|
this.transactions.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user