wip
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user