257 lines
18 KiB
Markdown
257 lines
18 KiB
Markdown
# 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<Props>`.
|
||
|
||
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. |