Files
gridpilot.gg/packages/shared/docs/ValueObjectCandidates.md
2025-12-11 13:50:38 +01:00

257 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (023, 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.