# Value Object Candidates Audit This document lists domain concepts currently modeled as primitives or simple types that should be refactored into explicit value objects implementing `IValueObject`. Priority levels: - **High**: Cross-cutting identifiers, URLs, or settings with clear invariants and repeated usage. - **Medium**: Important within a single bounded context but less cross-cutting. - **Low**: Niche or rarely used concepts. --- ## Analytics ### Analytics/PageView - **Concept**: `PageViewId` ✅ Implemented - **Implementation**: [`PageViewId`](packages/analytics/domain/value-objects/PageViewId.ts), [`PageView`](packages/analytics/domain/entities/PageView.ts:14), [`PageViewId.test`](packages/analytics/domain/value-objects/PageViewId.test.ts) - **Notes**: Page view identifiers are now modeled as a VO and used internally by the `PageView` entity while repositories and use cases continue to work with primitive string IDs where appropriate. - **Priority**: High - **Concept**: `AnalyticsEntityId` (for analytics) ✅ Implemented - **Implementation**: [`AnalyticsEntityId`](packages/analytics/domain/value-objects/AnalyticsEntityId.ts), [`PageView`](packages/analytics/domain/entities/PageView.ts:16), [`AnalyticsSnapshot`](packages/analytics/domain/entities/AnalyticsSnapshot.ts:16), [`EngagementEvent`](packages/analytics/domain/entities/EngagementEvent.ts:15), [`AnalyticsEntityId.test`](packages/analytics/domain/value-objects/AnalyticsEntityId.test.ts) - **Notes**: Entity IDs within the analytics bounded context are now modeled as a VO and used internally in snapshots, engagement events, and page views; external DTOs still expose primitive strings. - **Priority**: High - **Concept**: `AnalyticsSessionId` ✅ Implemented - **Implementation**: [`AnalyticsSessionId`](packages/analytics/domain/value-objects/AnalyticsSessionId.ts), [`PageView`](packages/analytics/domain/entities/PageView.ts:18), [`EngagementEvent`](packages/analytics/domain/entities/EngagementEvent.ts:22), [`AnalyticsSessionId.test`](packages/analytics/domain/value-objects/AnalyticsSessionId.test.ts) - **Notes**: Session identifiers are now encapsulated in a VO and used internally across analytics entities while preserving primitive session IDs at the boundaries. - **Priority**: High - **Concept**: `ReferrerUrl` - **Location**: [`PageView.referrer`](packages/analytics/domain/entities/PageView.ts:18), [`PageViewProps.referrer`](packages/analytics/domain/types/PageView.ts:19) - **Why VO**: External URL with semantics around internal vs external (`isExternalReferral` method). Currently string with no URL parsing or normalization. - **Priority**: Medium - **Concept**: `CountryCode` - **Location**: [`PageView.country`](packages/analytics/domain/entities/PageView.ts:20), [`PageViewProps.country`](packages/analytics/domain/types/PageView.ts:21) - **Why VO**: ISO country codes or similar; currently unvalidated string. Could enforce standardized codes. - **Priority**: Medium - **Concept**: `SnapshotId` - **Location**: [`AnalyticsSnapshot.id`](packages/analytics/domain/entities/AnalyticsSnapshot.ts:16), [`AnalyticsSnapshotProps.id`](packages/analytics/domain/types/AnalyticsSnapshot.ts:27) - **Why VO**: Identity for time-bucketed analytics snapshots; currently primitive string with simple validation. - **Priority**: Medium - **Concept**: `SnapshotPeriod` (as VO vs string union) - **Location**: [`SnapshotPeriod`](packages/analytics/domain/types/AnalyticsSnapshot.ts:8), [`AnalyticsSnapshot.period`](packages/analytics/domain/entities/AnalyticsSnapshot.ts:20) - **Why VO**: Has semantics used in [`getPeriodLabel`](packages/analytics/domain/entities/AnalyticsSnapshot.ts:130); could encapsulate formatting logic and date range constraints. Currently a union type only. - **Priority**: Low (enum-like, acceptable as-is for now) ### Analytics/EngagementEvent - **Concept**: `EngagementEventId` - **Location**: [`EngagementEvent.id`](packages/analytics/domain/entities/EngagementEvent.ts:15), [`EngagementEventProps.id`](packages/analytics/domain/types/EngagementEvent.ts:28) - **Why VO**: Unique ID for engagement events; only non-empty validation today. Could unify ID semantics with other analytics IDs. - **Priority**: Medium - **Concept**: `ActorId` (analytics) - **Location**: [`EngagementEvent.actorId`](packages/analytics/domain/entities/EngagementEvent.ts:20), [`EngagementEventProps.actorId`](packages/analytics/domain/types/EngagementEvent.ts:32) - **Why VO**: Identifies the actor (anonymous / driver / sponsor) with a type discriminator; could be a specific `ActorId` VO constrained by `actorType`. - **Priority**: Low (usage seems optional and less central) --- ## Notifications ### Notification Entity - **Concept**: `NotificationId` ✅ Implemented - **Implementation**: [`NotificationId`](packages/notifications/domain/value-objects/NotificationId.ts), [`Notification`](packages/notifications/domain/entities/Notification.ts:89), [`NotificationId.test`](packages/notifications/domain/value-objects/NotificationId.test.ts), [`SendNotificationUseCase`](packages/notifications/application/use-cases/SendNotificationUseCase.ts:46) - **Notes**: Notification aggregate IDs are now modeled as a VO and used internally by the `Notification` entity; repositories and use cases still operate with primitive string IDs via entity factories and serialization. - **Priority**: High - **Concept**: `RecipientId` (NotificationRecipientId) - **Location**: [`NotificationProps.recipientId`](packages/notifications/domain/entities/Notification.ts:59), [`Notification.recipientId`](packages/notifications/domain/entities/Notification.ts:115) - **Why VO**: Identity of the driver who receives notifications; likely aligns with identity/user IDs and is important for routing. - **Priority**: High - **Concept**: `ActionUrl` - **Location**: [`NotificationProps.actionUrl`](packages/notifications/domain/entities/Notification.ts:75), [`Notification.actionUrl`](packages/notifications/domain/entities/Notification.ts:123) - **Why VO**: URL used for click-through actions in notifications; should be validated/normalized and may have internal vs external semantics. - **Priority**: High - **Concept**: `NotificationActionId` - **Location**: [`NotificationAction.actionId`](packages/notifications/domain/entities/Notification.ts:53), [`Notification.markAsResponded`](packages/notifications/domain/entities/Notification.ts:182) - **Why VO**: Identifies action button behavior; currently raw string used to record `responseActionId` in `data`. - **Priority**: Low ### NotificationPreference Entity - **Concept**: `NotificationPreferenceId` - **Location**: [`NotificationPreferenceProps.id`](packages/notifications/domain/entities/NotificationPreference.ts:25), [`NotificationPreference.id`](packages/notifications/domain/entities/NotificationPreference.ts:80) - **Why VO**: Aggregate ID; currently plain string tied to driver ID; could be constrained to match a `DriverId` or similar. - **Priority**: Medium - **Concept**: `PreferenceOwnerId` (driverId) - **Location**: [`NotificationPreferenceProps.driverId`](packages/notifications/domain/entities/NotificationPreference.ts:28), [`NotificationPreference.driverId`](packages/notifications/domain/entities/NotificationPreference.ts:81) - **Why VO**: Identifies the driver whose preferences these are; should align with identity/racing driver IDs. - **Priority**: High - **Concept**: `QuietHours` - **Location**: [`NotificationPreferenceProps.quietHoursStart`](packages/notifications/domain/entities/NotificationPreference.ts:38), [`NotificationPreferenceProps.quietHoursEnd`](packages/notifications/domain/entities/NotificationPreference.ts:40), [`NotificationPreference.isInQuietHours`](packages/notifications/domain/entities/NotificationPreference.ts:125) - **Why VO**: Encapsulates a time window invariant (0–23, wrap-around support, comparison with current hour); currently implemented as two numbers plus logic in the entity. Ideal VO candidate. - **Priority**: High - **Concept**: `DigestFrequency` - **Location**: [`NotificationPreferenceProps.digestFrequencyHours`](packages/notifications/domain/entities/NotificationPreference.ts:37), [`NotificationPreference.digestFrequencyHours`](packages/notifications/domain/entities/NotificationPreference.ts:87) - **Why VO**: Represents cadence for digest emails in hours; could enforce positive ranges and provide helper methods. - **Priority**: Medium --- ## Media ### AvatarGenerationRequest - **Concept**: `AvatarGenerationRequestId` - **Location**: [`AvatarGenerationRequest.id`](packages/media/domain/entities/AvatarGenerationRequest.ts:15), [`AvatarGenerationRequestProps.id`](packages/media/domain/types/AvatarGenerationRequest.ts:33) - **Why VO**: Aggregate ID for avatar generation request lifecycle; currently raw string with only non-empty checks. - **Priority**: Medium - **Concept**: `AvatarOwnerId` (userId) - **Location**: [`AvatarGenerationRequest.userId`](packages/media/domain/entities/AvatarGenerationRequest.ts:17), [`AvatarGenerationRequestProps.userId`](packages/media/domain/types/AvatarGenerationRequest.ts:34) - **Why VO**: Identity reference to user; could be tied to `UserId` VO or a dedicated `AvatarOwnerId`. - **Priority**: Medium - **Concept**: `FacePhotoUrl` - **Location**: [`AvatarGenerationRequest.facePhotoUrl`](packages/media/domain/entities/AvatarGenerationRequest.ts:18), [`AvatarGenerationRequestProps.facePhotoUrl`](packages/media/domain/types/AvatarGenerationRequest.ts:35) - **Why VO**: External URL to user-submitted media; should be validated, normalized, and potentially constrained to HTTPS or whitelisted hosts. - **Priority**: High - **Concept**: `GeneratedAvatarUrl` - **Location**: [`AvatarGenerationRequest._generatedAvatarUrls`](packages/media/domain/entities/AvatarGenerationRequest.ts:22), [`AvatarGenerationRequestProps.generatedAvatarUrls`](packages/media/domain/types/AvatarGenerationRequest.ts:39), [`AvatarGenerationRequest.selectedAvatarUrl`](packages/media/domain/entities/AvatarGenerationRequest.ts:86) - **Why VO**: Generated asset URLs with invariant that at least one must be present when completed; currently raw strings in an array. - **Priority**: High --- ## Identity ### SponsorAccount - **Concept**: `SponsorAccountId` - **Location**: [`SponsorAccountProps.id`](packages/identity/domain/entities/SponsorAccount.ts:12), [`SponsorAccount.getId`](packages/identity/domain/entities/SponsorAccount.ts:73) - **Status**: Already a VO (`UserId`) – no change needed. - **Priority**: N/A - **Concept**: `SponsorId` (link to racing domain) - **Location**: [`SponsorAccountProps.sponsorId`](packages/identity/domain/entities/SponsorAccount.ts:14), [`SponsorAccount.getSponsorId`](packages/identity/domain/entities/SponsorAccount.ts:77) - **Why VO**: Cross-bounded-context reference into racing `Sponsor` entity; currently a primitive string with only non-empty validation. - **Priority**: High - **Concept**: `SponsorAccountEmail` - **Location**: [`SponsorAccountProps.email`](packages/identity/domain/entities/SponsorAccount.ts:15), [`SponsorAccount.create` email validation](packages/identity/domain/entities/SponsorAccount.ts:60) - **Status**: Validation uses [`EmailAddress` VO utilities](packages/identity/domain/value-objects/EmailAddress.ts:15) but the entity still stores `email: string`. - **Why VO**: Entity should likely store `EmailAddress` instead of a plain string to guarantee invariants wherever it is used. - **Priority**: High - **Concept**: `CompanyName` - **Location**: [`SponsorAccountProps.companyName`](packages/identity/domain/entities/SponsorAccount.ts:17), [`SponsorAccount.getCompanyName`](packages/identity/domain/entities/SponsorAccount.ts:89) - **Why VO**: Represents sponsor company name with potential invariants (length, prohibited characters). Currently only checked for non-empty. - **Priority**: Low --- ## Racing ### League Entity - **Concept**: `LeagueId` - **Location**: [`League.id`](packages/racing/domain/entities/League.ts:83), `validate` ID check in [`League.validate`](packages/racing/domain/entities/League.ts:157) - **Why VO**: Aggregate root ID; central to many references (races, teams, sponsorships). Currently primitive string with non-empty validation only. - **Priority**: High - **Concept**: `LeagueOwnerId` - **Location**: [`League.ownerId`](packages/racing/domain/entities/League.ts:87), validation in [`League.validate`](packages/racing/domain/entities/League.ts:179) - **Why VO**: Identity of league owner; likely maps to a `UserId` or `DriverId` concept; should not remain a free-form string. - **Priority**: High - **Concept**: `LeagueSocialLinkUrl` (`DiscordUrl`, `YoutubeUrl`, `WebsiteUrl`) - **Location**: [`LeagueSocialLinks.discordUrl`](packages/racing/domain/entities/League.ts:77), [`LeagueSocialLinks.youtubeUrl`](packages/racing/domain/entities/League.ts:79), [`LeagueSocialLinks.websiteUrl`](packages/racing/domain/entities/League.ts:80) - **Why VO**: External URLs across multiple channels; should be validated and normalized; repeated semantics across UI and domain. - **Priority**: High ### Track Entity - **Concept**: `TrackId` - **Location**: [`Track.id`](packages/racing/domain/entities/Track.ts:14), validation in [`Track.validate`](packages/racing/domain/entities/Track.ts:92) - **Why VO**: Aggregate root ID for tracks; referenced from races and schedules; currently primitive string. - **Priority**: High - **Concept**: `TrackCountryCode` - **Location**: [`Track.country`](packages/racing/domain/entities/Track.ts:18), validation in [`Track.validate`](packages/racing/domain/entities/Track.ts:100) - **Why VO**: Represent country using standard codes; currently a free-form string. - **Priority**: Medium - **Concept**: `TrackImageUrl` - **Location**: [`Track.imageUrl`](packages/racing/domain/entities/Track.ts:23) - **Why VO**: Image asset URL; should be constrained and validated similarly to other URL concepts. - **Priority**: High - **Concept**: `GameId` - **Location**: [`Track.gameId`](packages/racing/domain/entities/Track.ts:24), validation in [`Track.validate`](packages/racing/domain/entities/Track.ts:112) - **Why VO**: Identifier for simulation/game platform; currently string with non-empty validation; may benefit from VO if multiple entities use it. - **Priority**: Medium ### Race Entity - **Concept**: `RaceId` - **Location**: [`Race.id`](packages/racing/domain/entities/Race.ts:14), validation in [`Race.validate`](packages/racing/domain/entities/Race.ts:101) - **Why VO**: Aggregate ID for races; central to many operations and references. - **Priority**: High - **Concept**: `RaceLeagueId` - **Location**: [`Race.leagueId`](packages/racing/domain/entities/Race.ts:16), validation in [`Race.validate`](packages/racing/domain/entities/Race.ts:105) - **Why VO**: Foreign key into `League`; should be modeled as `LeagueId` VO rather than raw string. - **Priority**: High - **Concept**: `RaceTrackId` / `RaceCarId` - **Location**: [`Race.trackId`](packages/racing/domain/entities/Race.ts:19), [`Race.carId`](packages/racing/domain/entities/Race.ts:21) - **Why VO**: Optional references to track and car entities; currently strings; could be typed IDs aligned with `TrackId` and car ID concepts. - **Priority**: Medium - **Concept**: `RaceName` / `TrackName` / `CarName` - **Location**: [`Race.track`](packages/racing/domain/entities/Race.ts:18), [`Race.car`](packages/racing/domain/entities/Race.ts:20) - **Why VO**: Displayable names with potential formatting rules; today treated as raw strings, which is acceptable for now. - **Priority**: Low ### Team Entity - **Concept**: `TeamId` - **Location**: [`Team.id`](packages/racing/domain/entities/Team.ts:12), validation in [`Team.validate`](packages/racing/domain/entities/Team.ts:108) - **Why VO**: Aggregate ID; referenced from standings, registrations, etc. Currently primitive. - **Priority**: High - **Concept**: `TeamOwnerId` - **Location**: [`Team.ownerId`](packages/racing/domain/entities/Team.ts:17), validation in [`Team.validate`](packages/racing/domain/entities/Team.ts:120) - **Why VO**: Identity of team owner; should map to `UserId` or `DriverId`, currently a simple string. - **Priority**: High - **Concept**: `TeamLeagueId` (for membership list) - **Location**: [`Team.leagues`](packages/racing/domain/entities/Team.ts:18), validation in [`Team.validate`](packages/racing/domain/entities/Team.ts:124) - **Why VO**: Array of league IDs; currently `string[]` with no per-item validation; could leverage `LeagueId` VO and a small collection abstraction. - **Priority**: Medium --- ## Summary of Highest-Impact Candidates (Not Yet Refactored) The following are **high-priority** candidates that have not been refactored in this pass but are strong future VO targets: - `LeagueId`, `RaceId`, `TeamId`, and their foreign key counterparts (`RaceLeagueId`, `RaceTrackId`, `RaceCarId`, `TeamLeagueId`). - Cross-bounded-context identifiers: `SponsorId` in identity linking to racing `Sponsor`, `PreferenceOwnerId` / `NotificationPreferenceId` in notifications, and remaining analytics/session identifiers where primitive usage persists across boundaries. - URL-related concepts beyond those refactored in this pass: `LeagueSocialLinkUrl` variants, `TrackImageUrl`, `ReferrerUrl`, `ActionUrl` in notifications, and avatar-related URLs in media (where not yet wrapped). - Time-window and scheduling primitives: `QuietHours` numeric start/end in notifications, and other time-related raw numbers in stewarding settings and session configuration where richer semantics may help. These should be considered for future VO-focused refactors once the impact on mappers, repositories, and application layers is planned and coordinated.