inmemory to postgres

This commit is contained in:
2025-12-29 18:34:12 +01:00
parent 9e17d0752a
commit f5639a367f
176 changed files with 10175 additions and 468 deletions

View File

@@ -52,6 +52,37 @@ export class User {
return new User(props);
}
public static rehydrate(props: {
id: string;
displayName: string;
email?: string;
passwordHash?: PasswordHash;
iracingCustomerId?: string;
primaryDriverId?: string;
avatarUrl?: string;
}): User {
const email =
props.email !== undefined
? (() => {
const result: EmailValidationResult = validateEmail(props.email);
if (!result.success) {
throw new Error(result.error);
}
return result.email;
})()
: undefined;
return new User({
id: UserId.fromString(props.id),
displayName: props.displayName,
...(email !== undefined ? { email } : {}),
...(props.passwordHash !== undefined ? { passwordHash: props.passwordHash } : {}),
...(props.iracingCustomerId !== undefined ? { iracingCustomerId: props.iracingCustomerId } : {}),
...(props.primaryDriverId !== undefined ? { primaryDriverId: props.primaryDriverId } : {}),
...(props.avatarUrl !== undefined ? { avatarUrl: props.avatarUrl } : {}),
});
}
public static fromStored(stored: StoredUser): User {
const passwordHash = stored.passwordHash
? PasswordHash.fromHash(stored.passwordHash)

View File

@@ -18,4 +18,10 @@ export interface MemberPayment {
status: MemberPaymentStatus;
dueDate: Date;
paidAt?: Date;
}
}
export const MemberPayment = {
rehydrate(props: MemberPayment): MemberPayment {
return { ...props };
},
};

View File

@@ -17,4 +17,10 @@ export interface MembershipFee {
enabled: boolean;
createdAt: Date;
updatedAt: Date;
}
}
export const MembershipFee = {
rehydrate(props: MembershipFee): MembershipFee {
return { ...props };
},
};

View File

@@ -32,4 +32,10 @@ export interface Payment {
status: PaymentStatus;
createdAt: Date;
completedAt?: Date;
}
}
export const Payment = {
rehydrate(props: Payment): Payment {
return { ...props };
},
};

View File

@@ -21,4 +21,10 @@ export interface Prize {
awardedTo?: string;
awardedAt?: Date;
createdAt: Date;
}
}
export const Prize = {
rehydrate(props: Prize): Prize {
return { ...props };
},
};

View File

@@ -13,6 +13,12 @@ export interface Wallet {
createdAt: Date;
}
export const Wallet = {
rehydrate(props: Wallet): Wallet {
return { ...props };
},
};
export enum TransactionType {
DEPOSIT = 'deposit',
WITHDRAWAL = 'withdrawal',
@@ -34,4 +40,10 @@ export interface Transaction {
referenceId?: string;
referenceType?: ReferenceType;
createdAt: Date;
}
}
export const Transaction = {
rehydrate(props: Transaction): Transaction {
return { ...props };
},
};

View File

@@ -62,6 +62,24 @@ export class Driver implements IEntity<string> {
});
}
static rehydrate(props: {
id: string;
iracingId: string;
name: string;
country: string;
bio?: string;
joinedAt: Date;
}): Driver {
return new Driver({
id: props.id,
iracingId: IRacingId.create(props.iracingId),
name: DriverName.create(props.name),
country: CountryCode.create(props.country),
...(props.bio !== undefined ? { bio: DriverBio.create(props.bio) } : {}),
joinedAt: JoinedAt.create(props.joinedAt),
});
}
/**
* Create a copy with updated properties
*/

View File

@@ -20,4 +20,11 @@ export class Game implements IEntity<GameId> {
name,
});
}
static rehydrate(props: { id: string; name: string }): Game {
return new Game({
id: GameId.create(props.id),
name: GameName.create(props.name),
});
}
}

View File

@@ -49,6 +49,22 @@ export class JoinRequest implements IEntity<string> {
});
}
static rehydrate(props: {
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message?: string;
}): JoinRequest {
return new JoinRequest({
id: props.id,
leagueId: props.leagueId,
driverId: props.driverId,
requestedAt: props.requestedAt,
...(props.message !== undefined && { message: props.message }),
});
}
private static validate(props: JoinRequestProps): void {
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new RacingDomainValidationError('League ID is required');

View File

@@ -70,6 +70,24 @@ export class LeagueMembership implements IEntity<string> {
});
}
static rehydrate(props: {
id: string;
leagueId: string;
driverId: string;
role: MembershipRoleValue;
status: MembershipStatusValue;
joinedAt: Date;
}): LeagueMembership {
return new LeagueMembership({
id: props.id,
leagueId: LeagueId.create(props.leagueId),
driverId: DriverId.create(props.driverId),
role: MembershipRole.create(props.role),
status: MembershipStatus.create(props.status),
joinedAt: JoinedAt.create(props.joinedAt),
});
}
private static validate(props: LeagueMembershipProps): void {
if (!props.leagueId || props.leagueId.trim().length === 0) {
throw new RacingDomainValidationError('League ID is required');

View File

@@ -116,6 +116,61 @@ export class Protest implements IEntity<string> {
return new Protest(protestProps);
}
static rehydrate(props: {
id: string;
raceId: string;
protestingDriverId: string;
accusedDriverId: string;
incident: { lap: number; description: string; timeInRace?: number };
comment?: string;
proofVideoUrl?: string;
status: string;
reviewedBy?: string;
decisionNotes?: string;
filedAt: Date;
reviewedAt?: Date;
defense?: { statement: string; videoUrl?: string; submittedAt: Date };
defenseRequestedAt?: Date;
defenseRequestedBy?: string;
}): Protest {
const id = ProtestId.create(props.id);
const raceId = RaceId.create(props.raceId);
const protestingDriverId = DriverId.create(props.protestingDriverId);
const accusedDriverId = DriverId.create(props.accusedDriverId);
const incident = ProtestIncident.create(props.incident.lap, props.incident.description, props.incident.timeInRace);
const comment = props.comment ? ProtestComment.create(props.comment) : undefined;
const proofVideoUrl = props.proofVideoUrl ? VideoUrl.create(props.proofVideoUrl) : undefined;
const status = ProtestStatus.create(props.status);
const reviewedBy = props.reviewedBy ? StewardId.create(props.reviewedBy) : undefined;
const decisionNotes = props.decisionNotes ? DecisionNotes.create(props.decisionNotes) : undefined;
const filedAt = FiledAt.create(props.filedAt);
const reviewedAt = props.reviewedAt ? ReviewedAt.create(props.reviewedAt) : undefined;
const defense = props.defense ? ProtestDefense.create(props.defense.statement, props.defense.submittedAt, props.defense.videoUrl) : undefined;
const defenseRequestedAt = props.defenseRequestedAt ? DefenseRequestedAt.create(props.defenseRequestedAt) : undefined;
const defenseRequestedBy = props.defenseRequestedBy ? StewardId.create(props.defenseRequestedBy) : undefined;
const protestProps: ProtestProps = {
id,
raceId,
protestingDriverId,
accusedDriverId,
incident,
status,
filedAt,
};
if (comment !== undefined) protestProps.comment = comment;
if (proofVideoUrl !== undefined) protestProps.proofVideoUrl = proofVideoUrl;
if (reviewedBy !== undefined) protestProps.reviewedBy = reviewedBy;
if (decisionNotes !== undefined) protestProps.decisionNotes = decisionNotes;
if (reviewedAt !== undefined) protestProps.reviewedAt = reviewedAt;
if (defense !== undefined) protestProps.defense = defense;
if (defenseRequestedAt !== undefined) protestProps.defenseRequestedAt = defenseRequestedAt;
if (defenseRequestedBy !== undefined) protestProps.defenseRequestedBy = defenseRequestedBy;
return new Protest(protestProps);
}
get id(): string { return this.props.id.toString(); }
get raceId(): string { return this.props.raceId.toString(); }
get protestingDriverId(): string { return this.props.protestingDriverId.toString(); }

View File

@@ -55,6 +55,15 @@ export class RaceRegistration implements IEntity<string> {
});
}
static rehydrate(props: { id: string; raceId: string; driverId: string; registeredAt: Date }): RaceRegistration {
return new RaceRegistration({
id: props.id,
raceId: RaceId.create(props.raceId),
driverId: DriverId.create(props.driverId),
registeredAt: RegisteredAt.create(props.registeredAt),
});
}
private static validate(props: RaceRegistrationProps): void {
if (!props.raceId || props.raceId.trim().length === 0) {
throw new RacingDomainValidationError('Race ID is required');

View File

@@ -71,6 +71,10 @@ export class SponsorshipRequest implements IEntity<string> {
});
}
static rehydrate(props: SponsorshipRequestProps): SponsorshipRequest {
return new SponsorshipRequest(props);
}
private static validate(props: Omit<SponsorshipRequestProps, 'createdAt' | 'status'>): void {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('SponsorshipRequest ID is required');

View File

@@ -74,6 +74,30 @@ export class Standing implements IEntity<string> {
});
}
static rehydrate(props: {
id: string;
leagueId: string;
driverId: string;
points: number;
wins: number;
position: number;
racesCompleted: number;
}): Standing {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Standing ID is required');
}
return new Standing({
id: props.id,
leagueId: LeagueId.create(props.leagueId),
driverId: DriverId.create(props.driverId),
points: Points.create(props.points),
wins: props.wins,
position: Position.create(props.position),
racesCompleted: props.racesCompleted,
});
}
/**
* Domain validation logic
*/

View File

@@ -73,6 +73,34 @@ export class Team implements IEntity<string> {
});
}
static rehydrate(props: {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt: Date;
}): Team {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Team ID is required');
}
if (!Array.isArray(props.leagues)) {
throw new RacingDomainValidationError('Team leagues must be an array');
}
return new Team({
id: props.id,
name: TeamName.create(props.name),
tag: TeamTag.create(props.tag),
description: TeamDescription.create(props.description),
ownerId: DriverId.create(props.ownerId),
leagues: props.leagues.map(leagueId => LeagueId.create(leagueId)),
createdAt: TeamCreatedAt.create(props.createdAt),
});
}
/**
* Create a copy with updated properties.
*/

View File

@@ -58,6 +58,26 @@ export class LeagueWallet implements IEntity<LeagueWalletId> {
});
}
static rehydrate(props: {
id: string;
leagueId: string;
balance: Money;
transactionIds: string[];
createdAt: Date;
}): LeagueWallet {
const id = LeagueWalletId.create(props.id);
const leagueId = LeagueId.create(props.leagueId);
const transactionIds = props.transactionIds.map(tid => TransactionId.create(tid));
return new LeagueWallet({
id,
leagueId,
balance: props.balance,
transactionIds,
createdAt: props.createdAt,
});
}
private static validate(props: {
id: string;
leagueId: string;

View File

@@ -8,8 +8,8 @@ import { RacingDomainValidationError, RacingDomainInvariantError } from '../../e
import type { Money } from '../../value-objects/Money';
import type { IEntity } from '@core/shared/domain';
import type { TransactionId } from './TransactionId';
import type { LeagueWalletId } from './LeagueWalletId';
import { TransactionId } from './TransactionId';
import { LeagueWalletId } from './LeagueWalletId';
export type TransactionType =
| 'sponsorship_payment'
@@ -79,6 +79,34 @@ export class Transaction implements IEntity<TransactionId> {
});
}
static rehydrate(props: {
id: string;
walletId: string;
type: TransactionType;
amount: Money;
platformFee: Money;
netAmount: Money;
status: TransactionStatus;
createdAt: Date;
completedAt?: Date;
description?: string;
metadata?: Record<string, unknown>;
}): Transaction {
return new Transaction({
id: TransactionId.create(props.id),
walletId: LeagueWalletId.create(props.walletId),
type: props.type,
amount: props.amount,
platformFee: props.platformFee,
netAmount: props.netAmount,
status: props.status,
createdAt: props.createdAt,
completedAt: props.completedAt,
description: props.description,
metadata: props.metadata,
});
}
private static validate(props: Omit<TransactionProps, 'createdAt' | 'status' | 'platformFee' | 'netAmount'>): void {
if (!props.id) {
throw new RacingDomainValidationError('Transaction ID is required');

View File

@@ -114,6 +114,46 @@ export class Penalty implements IEntity<string> {
return new Penalty(penaltyProps);
}
static rehydrate(props: {
id: string;
leagueId: string;
raceId: string;
driverId: string;
type: string;
value?: number;
reason: string;
protestId?: string;
issuedBy: string;
status: string;
issuedAt: Date;
appliedAt?: Date;
notes?: string;
}): Penalty {
const penaltyType = PenaltyType.create(props.type);
if (penaltyTypeRequiresValue(penaltyType.toString())) {
if (props.value === undefined || props.value <= 0) {
throw new RacingDomainValidationError(`${penaltyType.toString()} requires a positive value`);
}
}
return new Penalty({
id: PenaltyId.create(props.id),
leagueId: LeagueId.create(props.leagueId),
raceId: RaceId.create(props.raceId),
driverId: DriverId.create(props.driverId),
type: penaltyType,
...(props.value !== undefined ? { value: PenaltyValue.create(props.value) } : {}),
reason: PenaltyReason.create(props.reason),
...(props.protestId !== undefined ? { protestId: ProtestId.create(props.protestId) } : {}),
issuedBy: StewardId.create(props.issuedBy),
status: PenaltyStatus.create(props.status),
issuedAt: IssuedAt.create(props.issuedAt),
...(props.appliedAt !== undefined ? { appliedAt: AppliedAt.create(props.appliedAt) } : {}),
...(props.notes !== undefined ? { notes: PenaltyNotes.create(props.notes) } : {}),
});
}
get id(): string { return this.props.id.toString(); }
get leagueId(): string { return this.props.leagueId.toString(); }
get raceId(): string { return this.props.raceId.toString(); }

View File

@@ -72,6 +72,30 @@ export class Result implements IEntity<string> {
});
}
static rehydrate(props: {
id: string;
raceId: string;
driverId: string;
position: number;
fastestLap: number;
incidents: number;
startPosition: number;
}): Result {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('Result ID is required');
}
return new Result({
id: props.id,
raceId: RaceId.create(props.raceId),
driverId: DriverId.create(props.driverId),
position: Position.create(props.position),
fastestLap: LapTime.create(props.fastestLap),
incidents: IncidentCount.create(props.incidents),
startPosition: Position.create(props.startPosition),
});
}
/**
* Domain validation logic
*/

View File

@@ -83,6 +83,10 @@ export class SeasonSponsorship implements IEntity<string> {
});
}
static rehydrate(props: SeasonSponsorshipProps): SeasonSponsorship {
return new SeasonSponsorship(props);
}
private static validate(props: Omit<SeasonSponsorshipProps, 'createdAt' | 'status'>): void {
if (!props.id || props.id.trim().length === 0) {
throw new RacingDomainValidationError('SeasonSponsorship ID is required');

View File

@@ -62,6 +62,31 @@ export class Sponsor implements IEntity<SponsorId> {
});
}
static rehydrate(props: {
id: string;
name: string;
contactEmail: string;
logoUrl?: string;
websiteUrl?: string;
createdAt: Date;
}): Sponsor {
const id = SponsorId.create(props.id);
const name = SponsorName.create(props.name);
const contactEmail = SponsorEmail.create(props.contactEmail);
const logoUrl = props.logoUrl ? Url.create(props.logoUrl) : undefined;
const websiteUrl = props.websiteUrl ? Url.create(props.websiteUrl) : undefined;
const createdAt = SponsorCreatedAt.create(props.createdAt);
return new Sponsor({
id,
name,
contactEmail,
createdAt,
...(logoUrl !== undefined ? { logoUrl } : {}),
...(websiteUrl !== undefined ? { websiteUrl } : {}),
});
}
/**
* Update sponsor information
*/