# Domain Models Specification - Change Delta This document captures modifications to the base `domain-models` specification for the `implement-domain-models` change. ## Base Specification Reference: `openspec/specs/domain-models/spec.md` ## ADDED Requirements ### Requirement: JdeDateConverter helper class The system SHALL provide a static helper class for converting JDE date/time formats to .NET DateTime. #### Methods | Method | Signature | Description | |--------|-----------|-------------| | ToDateTime | `DateTime? ToDateTime(int jdeDate, int jdeTime = 0)` | Converts JDE CYYDDD date and HHMMSS time to DateTime | #### Business Rules - Returns `null` for zero or invalid date values (not `1900-01-01`) - CYYDDD format: C = century (0=1900s, 1=2000s), YY = year, DDD = day of year - HHMMSS format: HH = hours (0-23), MM = minutes (0-59), SS = seconds (0-59) - Invalid time values are ignored (date portion still returned) - Parse errors return `null` rather than throwing exceptions #### Scenario: Convert valid JDE date - **WHEN** JdeDateConverter.ToDateTime(124365, 143052) is called - **THEN** returns DateTime 2024-12-30 14:30:52 #### Scenario: Handle zero date - **WHEN** JdeDateConverter.ToDateTime(0, 0) is called - **THEN** returns null #### Scenario: Handle invalid day of year - **WHEN** JdeDateConverter.ToDateTime(124400, 0) is called (day 400 is invalid) - **THEN** returns null --- ### Requirement: Extension method file organization The system SHALL organize ToViewModel extension methods in separate files by entity type. #### File Structure | File | Extension Methods | |------|-------------------| | WorkOrderExtensions.cs | WorkOrder.ToViewModel() | | LotExtensions.cs | Lot.ToViewModel() | | ItemExtensions.cs | Item.ToViewModel() | | WorkCenterExtensions.cs | WorkCenter.ToViewModel() | | ProfitCenterExtensions.cs | ProfitCenter.ToViewModel() | | JdeUserExtensions.cs | JdeUser.ToViewModel() | #### Business Rules - Extension methods in `JdeScoping.Core.Extensions` namespace - Each file contains a single static class with extension methods for one entity type - Extension methods are the only way entities project to ViewModels (no methods on entities) #### Scenario: Use extension method for projection - **WHEN** a WorkOrder entity calls ToViewModel() extension method - **THEN** a WorkOrderViewModel is returned with WorkOrderNumber and ItemNumber --- ### Requirement: ViewModel file organization The system SHALL organize ViewModel classes in a dedicated ViewModels folder. #### File Structure | File | ViewModel Class | |------|-----------------| | WorkOrderViewModel.cs | WorkOrderViewModel record | | LotViewModel.cs | LotViewModel record | | ItemViewModel.cs | ItemViewModel record | | WorkCenterViewModel.cs | WorkCenterViewModel record | | ProfitCenterViewModel.cs | ProfitCenterViewModel record | | JdeUserViewModel.cs | JdeUserViewModel record | | PartOperationViewModel.cs | PartOperationViewModel record | #### Business Rules - ViewModels in `JdeScoping.Core.ViewModels` namespace - ViewModels are immutable DTOs (prefer `record` type) - ViewModels contain only serializable properties (no computed properties) #### Scenario: ViewModel serialization - **WHEN** a WorkOrderViewModel is serialized to JSON - **THEN** all properties are included in the output --- ### Requirement: Enum file organization The system SHALL organize enum types in a dedicated Enums folder. #### File Structure | File | Enum Type | |------|-----------| | SearchStatus.cs | SearchStatus enum | | UpdateTypes.cs | UpdateTypes enum | #### Business Rules - Enums in `JdeScoping.Core.Models.Enums` namespace - All enums that may be serialized MUST have `[JsonConverter(typeof(JsonStringEnumConverter))]` #### Scenario: Enum JSON serialization - **WHEN** SearchStatus.Ended is serialized to JSON - **THEN** the output is "Ended" (string), not 3 (integer) --- ## MODIFIED Requirements ### Requirement: Search entity The system SHALL store user search requests containing filter criteria and resulting Excel output, with lazy deserialization of criteria from JSON. #### Properties | Property | Type | Description | |----------|------|-------------| | ID | int | Primary key identifier | | UserName | string | Username of user who created search | | Name | string | User-friendly name for the search | | Status | SearchStatus | Current search status (enum) | | SubmitDT | DateTime? | Timestamp when search was submitted | | StartDT | DateTime? | Timestamp when search processing started | | EndDT | DateTime? | Timestamp when search completed | | CriteriaJSON | string | JSON-serialized search criteria | | Criteria | SearchCriteria | Deserialized search criteria object | | Results | byte[]? | Excel file output (VARBINARY), nullable when not yet generated | #### Business Rules - Status MUST be serialized as string using `[JsonConverter(typeof(JsonStringEnumConverter))]` - Criteria is stored as JSON in `CriteriaJSON` for database persistence - `Criteria` property getter deserializes from `CriteriaJSON` using System.Text.Json - Setter serializes to `CriteriaJSON` - If `CriteriaJSON` is null or empty, `Criteria` returns a new empty `SearchCriteria` - Deserialization errors return empty `SearchCriteria` (fail gracefully) - Results contains binary Excel file data only when Status = Ended - Results property MUST be annotated as `byte[]?` since it is null until processing completes #### Scenario: Lazy deserialization of Criteria - **WHEN** Search.CriteriaJSON = '{"MinimumDT":"2024-01-01"}' and Criteria is accessed - **THEN** Criteria.MinimumDT = 2024-01-01 #### Scenario: Handle empty CriteriaJSON - **WHEN** Search.CriteriaJSON = null and Criteria is accessed - **THEN** Criteria returns new SearchCriteria() with all empty lists --- ### Requirement: SearchUpdate entity The system SHALL provide a real-time status update message for ASP.NET Core SignalR broadcast with factory method construction. #### Properties | Property | Type | Description | |----------|------|-------------| | ID | int | Search primary key | | UserName | string | Username of search submitter | | Name | string | Search name | | Status | SearchStatus | Current status | | SubmitDT | DateTime? | Submit timestamp | | StartDT | DateTime? | Start timestamp | | EndDT | DateTime? | End timestamp | | Timestamp | DateTime | When update was generated | | HasResults | bool | Indicates if search has Results | #### Business Rules - Primary constructor: `SearchUpdate(Search search)` copies all fields and sets Timestamp - Timestamp MUST be set to `DateTime.UtcNow` when update is created - Status MUST be serialized as string for JSON via `[JsonConverter(typeof(JsonStringEnumConverter))]` - `HasResults` is computed: `Status == SearchStatus.Ended && search.Results != null` #### Scenario: Create SearchUpdate from Search - **WHEN** SearchUpdate is created from a Search with ID=1, Status=Ended - **THEN** SearchUpdate.ID = 1, SearchUpdate.Status = Ended, SearchUpdate.Timestamp = current UTC time #### Scenario: HasResults computation - **WHEN** SearchUpdate is created from Search with Status=Ended and Results is not null - **THEN** HasResults = true --- ### Requirement: UserInfo entity The system SHALL provide authenticated user information with computed display name for ASP.NET Core Identity integration. #### Properties | Property | Type | Description | |----------|------|-------------| | Username | string | User's login identifier | | FirstName | string? | User's first name (nullable) | | LastName | string? | User's last name (nullable) | | DisplayName | string | Computed display name | | Title | string? | Organization title (nullable) | | EmailAddress | string? | Email address (nullable) | #### Business Rules - DisplayName computation: 1. If FirstName and LastName both have values: `$"{FirstName} {LastName}".Trim()` 2. If only FirstName has value: `FirstName.Trim()` 3. If only LastName has value: `LastName.Trim()` 4. Otherwise: `Username` - "Has value" means not null and not whitespace-only - Used for authentication context, populated from ASP.NET Core Identity claims or LDAP provider - DN (Distinguished Name) property removed; use ClaimsPrincipal for identity information #### Scenario: Compute display name from both names - **WHEN** UserInfo has FirstName = "John", LastName = "Doe" and DisplayName is accessed - **THEN** DisplayName = "John Doe" #### Scenario: Compute display name from first name only - **WHEN** UserInfo has FirstName = "John", LastName = null and DisplayName is accessed - **THEN** DisplayName = "John" #### Scenario: Fallback to username when names empty - **WHEN** UserInfo has FirstName = null, LastName = null, Username = "jdoe" and DisplayName is accessed - **THEN** DisplayName = "jdoe" --- ## CLARIFICATIONS ### Private JDE Date Fields Entities with JDE date fields use this pattern: ```csharp public class SomeEntity { // These are mapped by Dapper from database columns // but not exposed publicly private int LastUpdateDate { get; set; } private int LastUpdateTime { get; set; } // Public computed property public DateTime? LastUpdateDT => JdeDateConverter.ToDateTime(LastUpdateDate, LastUpdateTime); } ``` The private setters allow Dapper to populate the values during query mapping, while the computed property provides the converted DateTime. ### Collection Initialization All collection properties use C# 12 collection expression syntax: ```csharp public List Items { get; set; } = []; ``` This ensures collections are never null and always initialized to empty. ### Namespace Organization | Folder | Namespace | |--------|-----------| | Models/ | JdeScoping.Core.Models | | Models/Enums/ | JdeScoping.Core.Models.Enums | | ViewModels/ | JdeScoping.Core.ViewModels | | Extensions/ | JdeScoping.Core.Extensions | | Interfaces/ | JdeScoping.Core.Interfaces | | Helpers/ | JdeScoping.Core.Helpers |