Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
This commit is contained in:
+270
@@ -0,0 +1,270 @@
|
||||
# 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<string> 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 |
|
||||
Reference in New Issue
Block a user