Created
February 16, 2026 21:18
-
-
Save achal7/e6182027934298d460773d54baa01c7c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Medhavi.Domain.Material.BillOfMaterials | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Common.ResultCE | |
| type BomItem = | |
| { | |
| ComponentProductId: ProductId | |
| Quantity: Quantity | |
| UnitOfMeasure: UnitOfMeasureId | |
| StockingPointId: StockingPointId | |
| SequenceNumber: int | |
| IsPhantom: bool | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| } | |
| type BillOfMaterials = | |
| { | |
| Id: BomId | |
| ProductId: ProductId | |
| Version: string | |
| Items: BomItem list | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineBomCmd = | |
| { | |
| Id: string | |
| ProductId: ProductId | |
| Version: string | |
| Items: BomItem list | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| } | |
| type ReviseBomCmd = | |
| { | |
| Id: BomId | |
| Items: BomItem list | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type ActivateBomCmd = | |
| { | |
| Id: BomId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type DeactivateBomCmd = | |
| { | |
| Id: BomId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type BomCommand = | |
| | DefineBom of DefineBomCmd | |
| | ReviseBom of ReviseBomCmd | |
| | ActivateBom of ActivateBomCmd | |
| | DeactivateBom of DeactivateBomCmd | |
| // Events | |
| type BomDefinedEvt = | |
| { | |
| Id: BomId | |
| ProductId: ProductId | |
| Version: string | |
| Items: BomItem list | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| CreatedDate: DateTimeOffset | |
| } | |
| type BomRevisedEvt = | |
| { | |
| Id: BomId | |
| Items: BomItem list | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type BomActivatedEvt = | |
| { | |
| Id: BomId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type BomDeactivatedEvt = | |
| { | |
| Id: BomId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type BomEvent = | |
| | BomDefined of BomDefinedEvt | |
| | BomRevised of BomRevisedEvt | |
| | BomActivated of BomActivatedEvt | |
| | BomDeactivated of BomDeactivatedEvt | |
| // Signatures | |
| type DecideBom = BillOfMaterials option -> BomCommand -> Result<BomEvent list, DomainError> | |
| type EvolveBom = Evolve<BillOfMaterials, BomEvent> | |
| // Validation functions (includes business rules) | |
| let validateDefine (cmd: DefineBomCmd) : Result<unit, DomainError> = | |
| result { | |
| let! _ = required "BOM Id" cmd.Id | |
| let! _ = required "BOM Version" cmd.Version | |
| // Validate that we have at least one item | |
| let! _ = | |
| if List.isEmpty cmd.Items then | |
| Error(DomainError.validation "BOM must have at least one component") | |
| else | |
| Ok() | |
| return () | |
| } | |
| let validateRevise (cmd: ReviseBomCmd) : Result<unit, DomainError> = | |
| result { | |
| // Validate that we have at least one item | |
| let! _ = | |
| if List.isEmpty cmd.Items then | |
| Error(DomainError.validation "BOM must have at least one component") | |
| else | |
| Ok() | |
| return () | |
| } | |
| let validateActivate (_cmd: ActivateBomCmd) : Result<unit, DomainError> = | |
| // Activation is always allowed | |
| Ok() | |
| let validateDeactivate (_cmd: DeactivateBomCmd) : Result<unit, DomainError> = | |
| // Deactivation is always allowed | |
| Ok() | |
| // State evolution functions (pure state transitions) | |
| let applyDefined (evt: BomDefinedEvt) : BillOfMaterials = | |
| { | |
| Id = evt.Id | |
| ProductId = evt.ProductId | |
| Version = evt.Version | |
| Items = evt.Items | |
| EffectiveStart = evt.EffectiveStart | |
| EffectiveEnd = evt.EffectiveEnd | |
| IsActive = evt.IsActive | |
| CreatedDate = evt.CreatedDate | |
| ModifiedDate = evt.CreatedDate | |
| } | |
| let applyRevised (evt: BomRevisedEvt) (state: BillOfMaterials) : BillOfMaterials = | |
| { state with | |
| Items = evt.Items | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let applyActivated (evt: BomActivatedEvt) (state: BillOfMaterials) : BillOfMaterials = | |
| { state with | |
| IsActive = true | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let applyDeactivated (evt: BomDeactivatedEvt) (state: BillOfMaterials) : BillOfMaterials = | |
| { state with | |
| IsActive = false | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let evolve (state: BillOfMaterials option) (event: BomEvent) : BillOfMaterials option = | |
| match event, state with | |
| | BomDefined e, _ -> Some(applyDefined e) | |
| | BomRevised e, Some s -> Some(applyRevised e s) | |
| | BomActivated e, Some s -> Some(applyActivated e s) | |
| | BomDeactivated e, Some s -> Some(applyDeactivated e s) | |
| //| BomDefined _, Some _ -> state // Idempotent - BOM already exists | |
| | _, None -> None // Can't apply updates to non-existent BOM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Medhavi.Domain.Material.Forecast | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Common.ResultCE | |
| type ForecastId = string | |
| type Forecast = | |
| { | |
| Id: ForecastId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| NeedDate: DateTimeOffset | |
| IsIntermediate: bool | |
| } | |
| // Commands | |
| type DefineForecastCmd = | |
| { | |
| Id: ForecastId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| NeedDate: DateTimeOffset | |
| } | |
| type UpdateForecastCmd = | |
| { | |
| Id: ForecastId | |
| Quantity: Quantity | |
| NeedDate: DateTimeOffset | |
| } | |
| type RetireForecastCmd = | |
| { | |
| Id: ForecastId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type ConsumptionResult = | |
| { | |
| Consumed: Forecast list | |
| Residual: Forecast list | |
| PeggedRequirements: MaterialRequirementId list | |
| } | |
| type ConsumeForecastCmd = { Id: ForecastId; Quantity: decimal } | |
| type PegForecastCmd = { Id: ForecastId; Quantity: decimal } | |
| type ForecastCommand = | |
| | DefineForecast of DefineForecastCmd | |
| | UpdateForecast of UpdateForecastCmd | |
| | RetireForecast of RetireForecastCmd | |
| | ConsumeForecast of ConsumeForecastCmd | |
| | PegForecast of PegForecastCmd | |
| // Events | |
| type ForecastDefinedEvt = | |
| { | |
| Id: ForecastId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| NeedDate: DateTimeOffset | |
| } | |
| type ForecastUpdatedEvt = | |
| { | |
| Id: ForecastId | |
| Quantity: Quantity | |
| NeedDate: DateTimeOffset | |
| } | |
| type ForecastConsumedEvt = { Id: ForecastId; Quantity: Quantity } | |
| type ForecastPeggedEvt = { Id: ForecastId; Quantity: Quantity } | |
| type ForecastRetiredEvt = | |
| { | |
| Id: ForecastId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type ForecastEvent = | |
| | ForecastDefined of ForecastDefinedEvt | |
| | ForecastUpdated of ForecastUpdatedEvt | |
| | ForecastConsumed of ForecastConsumedEvt | |
| | ForecastPegged of ForecastPeggedEvt | |
| | ForecastRetired of ForecastRetiredEvt | |
| // Signatures | |
| type DecideForecast = Forecast option -> ForecastCommand -> Result<ForecastEvent list, DomainError> | |
| type EvolveForecast = Evolve<Forecast, ForecastEvent> | |
| // Validation functions (includes business rules) | |
| let validateDefine (cmd: DefineForecastCmd) : Result<unit, DomainError> = | |
| result { | |
| let! _ = required "Forecast Id" cmd.Id | |
| let! _ = | |
| if cmd.Quantity <= Quantity.zero then | |
| Error(DomainError.validation "Forecast quantity must be greater than zero") | |
| else | |
| Ok() | |
| return () | |
| } | |
| let validateUpdate (cmd: UpdateForecastCmd) : Result<unit, DomainError> = | |
| result { | |
| let! _ = | |
| if cmd.Quantity <= Quantity.zero then | |
| Error(DomainError.validation "Forecast quantity must be greater than zero") | |
| else | |
| Ok() | |
| return () | |
| } | |
| let validateRetire (_cmd: RetireForecastCmd) : Result<unit, DomainError> = | |
| // Retirement is always allowed | |
| Ok() | |
| // State evolution functions (pure state transitions) | |
| let applyDefined (evt: ForecastDefinedEvt) : Forecast = | |
| { | |
| Id = evt.Id | |
| ProductId = evt.ProductId | |
| StockingPointId = evt.StockingPointId | |
| Quantity = evt.Quantity | |
| NeedDate = evt.NeedDate | |
| IsIntermediate = false // Default to false, can be set later if needed | |
| } | |
| let applyUpdated (evt: ForecastUpdatedEvt) (state: Forecast) : Forecast = | |
| { state with | |
| Quantity = evt.Quantity | |
| NeedDate = evt.NeedDate | |
| } | |
| let applyRetired (_evt: ForecastRetiredEvt) (_state: Forecast) : Forecast option = None // Remove forecast when retired | |
| let evolve (state: Forecast option) (event: ForecastEvent) : Forecast option = | |
| match event, state with | |
| | ForecastDefined e, None -> Some(applyDefined e) | |
| | ForecastUpdated e, Some s -> Some(applyUpdated e s) | |
| | ForecastRetired e, Some s -> applyRetired e s | |
| | ForecastDefined _, Some _ -> state // Idempotent - forecast already exists | |
| | _, None -> None // Can't apply updates to non-existent forecast | |
| | _ -> state // Other events not handled in ingest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Medhavi.Domain.Material.Inventory | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| /// Domain aggregate for on-hand inventory snapshot. | |
| type Inventory = | |
| { | |
| Id: InventoryId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| UnitOfMeasure: UnitOfMeasureId | |
| InTransitInbound: Quantity | |
| InTransitOutbound: Quantity | |
| QualityHold: Quantity | |
| Damaged: Quantity | |
| AvailableToPromise: Quantity | |
| LastUpdated: DateTimeOffset | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Commands | |
| type CreateInventoryCmd = | |
| { | |
| Id: string | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| UnitOfMeasure: UnitOfMeasureId | |
| LastUpdated: DateTimeOffset option | |
| } | |
| type RemoveInventoryCmd = { Id: InventoryId } | |
| type InventoryCommand = | |
| | CreateInventory of CreateInventoryCmd | |
| | RemoveInventory of RemoveInventoryCmd | |
| // Events | |
| type InventoryCreatedEvt = | |
| { | |
| Id: InventoryId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| UnitOfMeasure: UnitOfMeasureId | |
| LastUpdated: DateTimeOffset | |
| Created: DateTimeOffset | |
| } | |
| type InventoryRemovedEvt = { Id: InventoryId } | |
| type InventoryEvent = | |
| | InventoryCreated of InventoryCreatedEvt | |
| | InventoryRemoved of InventoryRemovedEvt | |
| // Signatures | |
| type DecideInventory = Inventory option -> InventoryCommand -> Result<InventoryEvent list, DomainError> | |
| type EvolveInventory = Medhavi.Domain.Evolve<Inventory, InventoryEvent> | |
| let validateCreate (cmd: CreateInventoryCmd) : Result<unit, DomainError> = | |
| required "Inventory Id" (string cmd.Id) | |
| |> Result.bind (fun _ -> required "UoM" (UnitOfMeasureId.value cmd.UnitOfMeasure)) | |
| |> Result.map (fun _ -> ()) | |
| let applyCreated (evt: InventoryCreatedEvt) : Inventory = | |
| { | |
| Id = evt.Id | |
| ProductId = evt.ProductId | |
| StockingPointId = evt.StockingPointId | |
| Quantity = evt.Quantity | |
| UnitOfMeasure = evt.UnitOfMeasure | |
| InTransitInbound = Quantity.zero | |
| InTransitOutbound = Quantity.zero | |
| QualityHold = Quantity.zero | |
| Damaged = Quantity.zero | |
| AvailableToPromise = Quantity.zero | |
| LastUpdated = evt.LastUpdated | |
| Created = evt.Created | |
| Modified = evt.Created | |
| } | |
| let evolve (state: Inventory option) (event: InventoryEvent) : Inventory option = | |
| match event with | |
| | InventoryCreated e -> Some(applyCreated e) | |
| | InventoryRemoved(_) -> failwith "Not Implemented" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Medhavi.Domain.Material.InventoryTarget | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Common.ResultCE | |
| type ReplenishmentPolicy = | |
| { | |
| Safety: Quantity | |
| MinQty: Quantity option | |
| MaxQty: Quantity option | |
| CoverDays: float option | |
| LotSize: Quantity option | |
| Expedite: bool | |
| } | |
| /// Seasonal adjustment factor | |
| type SeasonalAdjustment = | |
| { | |
| PeriodStart: DateTimeOffset | |
| PeriodEnd: DateTimeOffset | |
| AdjustmentFactor: decimal // e.g., 1.2 = 20% increase | |
| } | |
| type InventoryTarget = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReplenishmentPolicy: ReplenishmentPolicy option | |
| SafetyStockQty: Quantity option | |
| MinQty: Quantity option | |
| MaxQty: Quantity option | |
| TargetServiceLevel: float option | |
| CoverDays: float option | |
| SeasonalAdjustments: SeasonalAdjustment list // TODO-014: Fixed - Seasonal adjustments | |
| EffectiveStart: DateTimeOffset option | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineInventoryTargetCmd = | |
| { | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReplenishmentPolicy: ReplenishmentPolicy option | |
| SafetyStockQty: decimal option | |
| MinQty: decimal option | |
| MaxQty: decimal option | |
| TargetServiceLevel: float option | |
| CoverDays: float option | |
| SeasonalAdjustments: SeasonalAdjustment list // TODO-014: Fixed - Seasonal adjustments | |
| EffectiveStart: DateTimeOffset option | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| } | |
| type UpdateInventoryTargetCmd = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReplenishmentPolicy: ReplenishmentPolicy option | |
| SafetyStockQty: decimal option | |
| MinQty: decimal option | |
| MaxQty: decimal option | |
| TargetServiceLevel: float option | |
| CoverDays: float option | |
| SeasonalAdjustments: SeasonalAdjustment list option // TODO-014: Fixed - Seasonal adjustments | |
| EffectiveStart: DateTimeOffset option | |
| EffectiveEnd: DateTimeOffset option | |
| } | |
| type ActivateInventoryTargetCmd = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type DeactivateInventoryTargetCmd = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type InventoryTargetCommand = | |
| | DefineInventoryTarget of DefineInventoryTargetCmd | |
| | UpdateInventoryTarget of UpdateInventoryTargetCmd | |
| | ActivateInventoryTarget of ActivateInventoryTargetCmd | |
| | DeactivateInventoryTarget of DeactivateInventoryTargetCmd | |
| // Events | |
| type InventoryTargetDefinedEvt = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReplenishmentPolicy: ReplenishmentPolicy option | |
| SafetyStockQty: Quantity option | |
| MinQty: Quantity option | |
| MaxQty: Quantity option | |
| TargetServiceLevel: float option | |
| CoverDays: float option | |
| SeasonalAdjustments: SeasonalAdjustment list | |
| EffectiveStart: DateTimeOffset option | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| CreatedDate: DateTimeOffset | |
| } | |
| type InventoryTargetUpdatedEvt = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReplenishmentPolicy: ReplenishmentPolicy option | |
| SafetyStockQty: Quantity option | |
| MinQty: Quantity option | |
| MaxQty: Quantity option | |
| TargetServiceLevel: float option | |
| CoverDays: float option | |
| SeasonalAdjustments: SeasonalAdjustment list option | |
| EffectiveStart: DateTimeOffset option | |
| EffectiveEnd: DateTimeOffset option | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type InventoryTargetActivatedEvt = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type InventoryTargetDeactivatedEvt = | |
| { | |
| Id: InventoryTargetId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type InventoryTargetEvent = | |
| | InventoryTargetDefined of InventoryTargetDefinedEvt | |
| | InventoryTargetUpdated of InventoryTargetUpdatedEvt | |
| | InventoryTargetActivated of InventoryTargetActivatedEvt | |
| | InventoryTargetDeactivated of InventoryTargetDeactivatedEvt | |
| // Signatures | |
| type DecideInventoryTarget = | |
| InventoryTarget option -> InventoryTargetCommand -> Result<InventoryTargetEvent list, DomainError> | |
| type EvolveInventoryTarget = Medhavi.Domain.Evolve<InventoryTarget, InventoryTargetEvent> | |
| // Validation functions (includes business rules) | |
| let validateDefine (cmd: DefineInventoryTargetCmd) : Result<unit, DomainError> = | |
| result { | |
| let! _ = required "ProductId" (ProductId.value cmd.ProductId) | |
| let! _ = required "StockingPointId" (StockingPointId.value cmd.StockingPointId) | |
| // Business rule: If MaxQty is specified, it must be >= MinQty | |
| match cmd.MinQty, cmd.MaxQty with | |
| | Some min, Some max when max < min -> | |
| return! Error(DomainError.validation "MaxQty must be greater than or equal to MinQty") | |
| | _ -> return () | |
| } | |
| let validateUpdate (cmd: UpdateInventoryTargetCmd) : Result<unit, DomainError> = | |
| result { | |
| let! _ = required "ProductId" (ProductId.value cmd.ProductId) | |
| let! _ = required "StockingPointId" (StockingPointId.value cmd.StockingPointId) | |
| // Business rule: If MaxQty is specified, it must be >= MinQty | |
| match cmd.MinQty, cmd.MaxQty with | |
| | Some min, Some max when max < min -> | |
| return! Error(DomainError.validation "MaxQty must be greater than or equal to MinQty") | |
| | _ -> return () | |
| } | |
| let validateActivate (_cmd: ActivateInventoryTargetCmd) : Result<unit, DomainError> = | |
| // Activation is always allowed | |
| Ok() | |
| let validateDeactivate (_cmd: DeactivateInventoryTargetCmd) : Result<unit, DomainError> = | |
| // Deactivation is always allowed | |
| Ok() | |
| // State evolution functions (pure state transitions) | |
| let applyDefinedEvent (evt: InventoryTargetDefinedEvt) : InventoryTarget = | |
| { | |
| Id = evt.Id | |
| ProductId = evt.ProductId | |
| StockingPointId = evt.StockingPointId | |
| ReplenishmentPolicy = evt.ReplenishmentPolicy | |
| SafetyStockQty = evt.SafetyStockQty | |
| MinQty = evt.MinQty | |
| MaxQty = evt.MaxQty | |
| TargetServiceLevel = evt.TargetServiceLevel | |
| CoverDays = evt.CoverDays | |
| SeasonalAdjustments = evt.SeasonalAdjustments | |
| EffectiveStart = evt.EffectiveStart | |
| EffectiveEnd = evt.EffectiveEnd | |
| IsActive = evt.IsActive | |
| CreatedDate = evt.CreatedDate | |
| ModifiedDate = evt.CreatedDate | |
| } | |
| let applyUpdatedEvent (existing: InventoryTarget) (evt: InventoryTargetUpdatedEvt) : InventoryTarget = | |
| { existing with | |
| ReplenishmentPolicy = evt.ReplenishmentPolicy | |
| SafetyStockQty = evt.SafetyStockQty | |
| MinQty = evt.MinQty | |
| MaxQty = evt.MaxQty | |
| TargetServiceLevel = evt.TargetServiceLevel | |
| CoverDays = evt.CoverDays | |
| SeasonalAdjustments = | |
| evt.SeasonalAdjustments | |
| |> Option.defaultValue existing.SeasonalAdjustments | |
| EffectiveStart = evt.EffectiveStart | |
| EffectiveEnd = evt.EffectiveEnd | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let applyActivatedEvent (existing: InventoryTarget) (evt: InventoryTargetActivatedEvt) : InventoryTarget = | |
| { existing with | |
| IsActive = true | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let applyDeactivatedEvent (existing: InventoryTarget) (evt: InventoryTargetDeactivatedEvt) : InventoryTarget = | |
| { existing with | |
| IsActive = false | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let evolve (state: InventoryTarget option) (event: InventoryTargetEvent) : InventoryTarget option = | |
| match event, state with | |
| | InventoryTargetDefined e, None -> Some(applyDefinedEvent e) | |
| | InventoryTargetUpdated e, Some s -> Some(applyUpdatedEvent s e) | |
| | InventoryTargetActivated e, Some s -> Some(applyActivatedEvent s e) | |
| | InventoryTargetDeactivated e, Some s -> Some(applyDeactivatedEvent s e) | |
| | InventoryTargetDefined _, Some _ -> state // Idempotent - target already exists | |
| | _, None -> None // Can't apply updates to non-existent target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| namespace Medhavi.Domain.Material | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain.UnitOfMeasure | |
| open Medhavi.Domain.Material.SupplyOrder | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Domain | |
| type RequirementSource = | |
| | FromCustomerOrder of CustomerOrderId | |
| | FromSupplyOrder of SupplyOrderId | |
| | FromInventoryTarget of ProductId * StockingPointId | |
| | FromSafetyStock of ProductId * StockingPointId | |
| | FromDemandForecast of ProductId * StockingPointId * DateTimeOffset | |
| type MaterialRequirement = | |
| { | |
| Id: MaterialRequirementId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| RequiredQuantity: Quantity | |
| RequiredDate: DateTimeOffset | |
| Source: RequirementSource | |
| GrossRequirement: Quantity | |
| NetRequirement: Quantity | |
| OnHandQuantity: Quantity | |
| ReservedQuantity: Quantity | |
| InTransitQuantity: Quantity | |
| WorkInProgressQuantity: Quantity | |
| SafetyStockQuantity: Quantity | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineMaterialRequirementCmd = | |
| { | |
| Id: string | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| RequiredQuantity: Quantity | |
| RequiredDate: DateTimeOffset | |
| Source: RequirementSource | |
| GrossRequirement: Quantity | |
| NetRequirement: Quantity | |
| OnHandQuantity: Quantity | |
| ReservedQuantity: Quantity | |
| InTransitQuantity: Quantity | |
| WorkInProgressQuantity: Quantity | |
| SafetyStockQuantity: Quantity | |
| } | |
| type UpdateMaterialRequirementCmd = | |
| { | |
| Id: MaterialRequirementId | |
| NetRequirement: Quantity | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type MaterialRequirementCommand = | |
| | DefineMaterialRequirement of DefineMaterialRequirementCmd | |
| | UpdateMaterialRequirement of UpdateMaterialRequirementCmd | |
| // Events | |
| type MaterialRequirementDefinedEvt = DefineMaterialRequirementCmd | |
| type MaterialRequirementUpdatedEvt = | |
| { | |
| Id: MaterialRequirementId | |
| NetRequirement: Quantity | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type MaterialRequirementEvent = | |
| | MaterialRequirementDefined of MaterialRequirementDefinedEvt | |
| | MaterialRequirementUpdated of MaterialRequirementUpdatedEvt | |
| // Signatures | |
| type DecideMaterialRequirement = | |
| MaterialRequirement option -> MaterialRequirementCommand -> Result<MaterialRequirementEvent list, DomainError> | |
| type EvolveMaterialRequirement = MaterialRequirement option -> MaterialRequirementEvent -> MaterialRequirement |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| namespace Medhavi.Domain.Material | |
| open System | |
| open Medhavi.Domain.Ids | |
| open System.Text.Json.Serialization | |
| open Medhavi.Domain | |
| [<JsonFSharpConverter>] | |
| type ReservationStatus = | |
| | Tentative | |
| | Confirmed | |
| | Released | |
| | Expired | |
| | Reduced | |
| /// Indicates whether a reservation consumes on-hand inventory or a planned supply. | |
| type ReservationOrigin = | |
| | OnHand of InventoryId option | |
| | PlannedSupply of SupplyOrderId | |
| type MaterialReservation = | |
| { | |
| Id: MaterialReservationId | |
| IdempotencyKey: string option | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReservedQuantity: Quantity | |
| Status: ReservationStatus | |
| Origin: ReservationOrigin | |
| WindowStart: DateTimeOffset | |
| WindowEnd: DateTimeOffset | |
| Ttl: TimeSpan option | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type CreateMaterialReservationCmd = | |
| { | |
| Id: string | |
| IdempotencyKey: string option | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReservedQuantity: Quantity | |
| Origin: ReservationOrigin | |
| WindowStart: DateTimeOffset | |
| WindowEnd: DateTimeOffset | |
| Ttl: TimeSpan option | |
| CreatedDate: DateTimeOffset | |
| } | |
| type ConfirmMaterialReservationCmd = | |
| { | |
| Id: MaterialReservationId | |
| ConfirmedDate: DateTimeOffset | |
| } | |
| type ReleaseMaterialReservationCmd = | |
| { | |
| Id: MaterialReservationId | |
| ReleasedDate: DateTimeOffset | |
| } | |
| type ReduceMaterialReservationCmd = | |
| { | |
| Id: MaterialReservationId | |
| NewQuantity: decimal | |
| NewWindowEnd: DateTimeOffset option | |
| ReducedDate: DateTimeOffset | |
| } | |
| type ExpireMaterialReservationCmd = | |
| { | |
| Id: MaterialReservationId | |
| ExpiredDate: DateTimeOffset | |
| } | |
| type MaterialReservationCommand = | |
| | CreateMaterialReservation of CreateMaterialReservationCmd | |
| | ConfirmMaterialReservation of ConfirmMaterialReservationCmd | |
| | ReleaseMaterialReservation of ReleaseMaterialReservationCmd | |
| | ReduceMaterialReservation of ReduceMaterialReservationCmd | |
| | ExpireMaterialReservation of ExpireMaterialReservationCmd | |
| // Events | |
| type MaterialReservationCreatedEvt = | |
| { | |
| Id: MaterialReservationId | |
| IdempotencyKey: string option | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ReservedQuantity: Quantity | |
| Origin: ReservationOrigin | |
| WindowStart: DateTimeOffset | |
| WindowEnd: DateTimeOffset | |
| Ttl: TimeSpan option | |
| CreatedDate: DateTimeOffset | |
| } | |
| type MaterialReservationConfirmedEvt = | |
| { | |
| Id: MaterialReservationId | |
| ConfirmedDate: DateTimeOffset | |
| } | |
| type MaterialReservationReleasedEvt = | |
| { | |
| Id: MaterialReservationId | |
| ReleasedDate: DateTimeOffset | |
| } | |
| type MaterialReservationReducedEvt = | |
| { | |
| Id: MaterialReservationId | |
| NewQuantity: Quantity | |
| NewWindowEnd: DateTimeOffset option | |
| ReducedDate: DateTimeOffset | |
| } | |
| type MaterialReservationExpiredEvt = | |
| { | |
| Id: MaterialReservationId | |
| ExpiredDate: DateTimeOffset | |
| } | |
| type MaterialReservationEvent = | |
| | MaterialReservationCreated of MaterialReservationCreatedEvt | |
| | MaterialReservationConfirmed of MaterialReservationConfirmedEvt | |
| | MaterialReservationReleased of MaterialReservationReleasedEvt | |
| | MaterialReservationReduced of MaterialReservationReducedEvt | |
| | MaterialReservationExpired of MaterialReservationExpiredEvt | |
| // Decide | |
| type DecideMaterialReservation = | |
| Decide<MaterialReservation, MaterialReservationCommand, MaterialReservationEvent, DomainError> | |
| // Evolve | |
| type EvolveMaterialReservation = Evolve<MaterialReservation, MaterialReservationEvent> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| namespace Medhavi.Domain.Material | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain.UnitOfMeasure | |
| open Medhavi.Domain.Material.SupplyOrder | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Domain | |
| type RecommendationStatus = | |
| | Suggested | |
| | Approved | |
| | Rejected | |
| | ConvertedToPurchaseOrder | |
| type PurchaseOrderRecommendation = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| SupplierId: string | |
| RecommendedQuantity: decimal | |
| RecommendedDeliveryDate: DateTimeOffset | |
| EconomicOrderQuantity: decimal option | |
| Status: RecommendationStatus | |
| MaterialRequirementIds: MaterialRequirementId list | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type RecommendPurchaseOrderCmd = | |
| { | |
| Id: string | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| SupplierId: string | |
| RecommendedQuantity: decimal | |
| RecommendedDeliveryDate: DateTimeOffset | |
| EconomicOrderQuantity: decimal option | |
| MaterialRequirementIds: MaterialRequirementId list | |
| } | |
| type ApprovePurchaseOrderRecommendationCmd = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| ApprovedDate: DateTimeOffset | |
| } | |
| type RejectPurchaseOrderRecommendationCmd = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| RejectedDate: DateTimeOffset | |
| } | |
| type ConvertPurchaseOrderRecommendationCmd = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| ConvertedDate: DateTimeOffset | |
| } | |
| type PurchaseOrderRecommendationCommand = | |
| | RecommendPurchaseOrder of RecommendPurchaseOrderCmd | |
| | ApprovePurchaseOrderRecommendation of ApprovePurchaseOrderRecommendationCmd | |
| | RejectPurchaseOrderRecommendation of RejectPurchaseOrderRecommendationCmd | |
| | ConvertPurchaseOrderRecommendation of ConvertPurchaseOrderRecommendationCmd | |
| // Events | |
| type PurchaseOrderRecommendedEvt = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| SupplierId: string | |
| RecommendedQuantity: decimal | |
| RecommendedDeliveryDate: DateTimeOffset | |
| EconomicOrderQuantity: decimal option | |
| MaterialRequirementIds: MaterialRequirementId list | |
| CreatedDate: DateTimeOffset | |
| } | |
| type PurchaseOrderRecommendationApprovedEvt = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| ApprovedDate: DateTimeOffset | |
| } | |
| type PurchaseOrderRecommendationRejectedEvt = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| RejectedDate: DateTimeOffset | |
| } | |
| type PurchaseOrderRecommendationConvertedEvt = | |
| { | |
| Id: PurchaseOrderRecommendationId | |
| ConvertedDate: DateTimeOffset | |
| } | |
| type PurchaseOrderRecommendationEvent = | |
| | PurchaseOrderRecommended of PurchaseOrderRecommendedEvt | |
| | PurchaseOrderRecommendationApproved of PurchaseOrderRecommendationApprovedEvt | |
| | PurchaseOrderRecommendationRejected of PurchaseOrderRecommendationRejectedEvt | |
| | PurchaseOrderRecommendationConverted of PurchaseOrderRecommendationConvertedEvt | |
| // Signatures | |
| type DecidePurchaseOrderRecommendation = | |
| PurchaseOrderRecommendation option | |
| -> PurchaseOrderRecommendationCommand | |
| -> Result<PurchaseOrderRecommendationEvent list, DomainError> | |
| type EvolvePurchaseOrderRecommendation = | |
| PurchaseOrderRecommendation option -> PurchaseOrderRecommendationEvent -> PurchaseOrderRecommendation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Medhavi.Domain.Material.Supplier | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| // Supplier classification/type | |
| type SupplierType = | |
| | Strategic // Key strategic supplier | |
| | Preferred // Preferred supplier | |
| | Standard // Standard supplier | |
| // Geographic region served by supplier | |
| type RegionId = | RegionId of string | |
| // Core Supplier aggregate | |
| type Supplier = | |
| { | |
| Id: SupplierId | |
| Code: string | |
| Name: string | |
| SupplierType: SupplierType | |
| Regions: RegionId list | |
| IsActive: bool // Simple active/inactive status | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // ================================================================================================= | |
| // SUPPLIER COMMANDS | |
| // ================================================================================================= | |
| // Create a new supplier | |
| type CreateSupplierCmd = | |
| { | |
| Id: SupplierId | |
| Code: string | |
| Name: string | |
| SupplierType: SupplierType | |
| Regions: RegionId list | |
| } | |
| // Update supplier basic information | |
| type UpdateSupplierInfoCmd = | |
| { | |
| Id: SupplierId | |
| Name: string option | |
| SupplierType: SupplierType option | |
| Modified: DateTimeOffset | |
| } | |
| // Activate/Deactivate supplier | |
| type ChangeSupplierStatusCmd = | |
| { | |
| Id: SupplierId | |
| IsActive: bool | |
| Reason: string option | |
| Modified: DateTimeOffset | |
| } | |
| // Add geographic region to supplier | |
| type AddSupplierRegionCmd = | |
| { | |
| Id: SupplierId | |
| RegionId: RegionId | |
| Modified: DateTimeOffset | |
| } | |
| // Remove geographic region from supplier | |
| type RemoveSupplierRegionCmd = | |
| { | |
| Id: SupplierId | |
| RegionId: RegionId | |
| Modified: DateTimeOffset | |
| } | |
| // Discriminated union of all supplier commands | |
| type SupplierCommand = | |
| | CreateSupplier of CreateSupplierCmd | |
| | UpdateSupplierInfo of UpdateSupplierInfoCmd | |
| | ChangeSupplierStatus of ChangeSupplierStatusCmd | |
| | AddSupplierRegion of AddSupplierRegionCmd | |
| | RemoveSupplierRegion of RemoveSupplierRegionCmd | |
| // ================================================================================================= | |
| // SUPPLIER EVENTS | |
| // ================================================================================================= | |
| // Supplier created event | |
| type SupplierCreatedEvt = { Result: Supplier } | |
| // Supplier information updated | |
| type SupplierInfoUpdatedEvt = | |
| { | |
| Id: SupplierId | |
| Name: string option | |
| SupplierType: SupplierType option | |
| Modified: DateTimeOffset | |
| } | |
| // Supplier status changed | |
| type SupplierStatusChangedEvt = | |
| { | |
| Id: SupplierId | |
| IsActive: bool | |
| Reason: string option | |
| Modified: DateTimeOffset | |
| } | |
| // Supplier region added | |
| type SupplierRegionAddedEvt = | |
| { | |
| Id: SupplierId | |
| RegionId: RegionId | |
| Modified: DateTimeOffset | |
| } | |
| // Supplier region removed | |
| type SupplierRegionRemovedEvt = | |
| { | |
| Id: SupplierId | |
| RegionId: RegionId | |
| Modified: DateTimeOffset | |
| } | |
| // Discriminated union of all supplier events | |
| type SupplierEvent = | |
| | SupplierCreated of SupplierCreatedEvt | |
| | SupplierInfoUpdated of SupplierInfoUpdatedEvt | |
| | SupplierStatusChanged of SupplierStatusChangedEvt | |
| | SupplierRegionAdded of SupplierRegionAddedEvt | |
| | SupplierRegionRemoved of SupplierRegionRemovedEvt | |
| // ================================================================================================= | |
| // SUPPLIER DOMAIN LOGIC SIGNATURES | |
| // ================================================================================================= | |
| // Decision function signature | |
| type DecideSupplier = Supplier option -> SupplierCommand -> Result<SupplierEvent list, DomainError> | |
| // Evolution function signature | |
| type EvolveSupplier = Medhavi.Domain.Evolve<Supplier, SupplierEvent> | |
| // ================================================================================================= | |
| // LEGACY SUPPLIER OFFER (TO BE KEPT FOR COMPATIBILITY) | |
| // ================================================================================================= | |
| /// Supplier offer model (scaffold for Phase 7 – Supplier Management). | |
| /// TODO: add events/commands/projection for supplier offers (MOQ, lot size, lead times, price tiers, reliability, incoterms). | |
| type SupplierOffer = | |
| { | |
| SupplierId: string | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId option | |
| Moq: decimal option | |
| LotSize: decimal option | |
| LeadTimeP50: TimeSpan option | |
| LeadTimeP95: TimeSpan option | |
| Price: decimal option | |
| Currency: string option | |
| Reliability: float option | |
| Incoterm: string option | |
| PriceTier: string option | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Validation functions (includes business rules) | |
| let validateCreate (cmd: CreateSupplierCmd) : Result<unit, DomainError> = | |
| // Basic input validation | |
| if String.IsNullOrWhiteSpace cmd.Code then | |
| Error(DomainError.validation "Supplier code cannot be empty") | |
| elif String.IsNullOrWhiteSpace cmd.Name then | |
| Error(DomainError.validation "Supplier name cannot be empty") | |
| elif cmd.Regions.Length = 0 then | |
| Error(DomainError.validation "Supplier must have at least one region") | |
| else | |
| Ok() | |
| let validateUpdateInfo (cmd: UpdateSupplierInfoCmd) : Result<unit, DomainError> = | |
| // Validate that at least one field is being updated | |
| match cmd.Name, cmd.SupplierType with | |
| | None, None -> Error(DomainError.validation "At least one field must be updated") | |
| | Some name, _ when String.IsNullOrWhiteSpace name -> Error(DomainError.validation "Supplier name cannot be empty") | |
| | _ -> Ok() | |
| let validateChangeStatus (_cmd: ChangeSupplierStatusCmd) : Result<unit, DomainError> = | |
| // Status changes are always allowed | |
| Ok() | |
| let validateAddRegion (_cmd: AddSupplierRegionCmd) : Result<unit, DomainError> = | |
| // Basic validation - region format could be added here if needed | |
| Ok() | |
| let validateRemoveRegion (_cmd: RemoveSupplierRegionCmd) : Result<unit, DomainError> = | |
| // Basic validation - could check if supplier has at least one region remaining | |
| Ok() | |
| // State evolution functions (pure state transitions) | |
| // Note: ID is validated in Application layer before event creation | |
| let applyCreated (evt: SupplierCreatedEvt) : Supplier = | |
| let result = evt.Result | |
| { | |
| Id = result.Id | |
| Code = result.Code | |
| Name = result.Name | |
| SupplierType = result.SupplierType | |
| Regions = result.Regions | |
| IsActive = true // New suppliers start as active | |
| Created = result.Created | |
| Modified = result.Modified | |
| } | |
| let applyInfoUpdated (evt: SupplierInfoUpdatedEvt) (state: Supplier) : Supplier = | |
| { state with | |
| Name = evt.Name |> Option.defaultValue state.Name | |
| SupplierType = | |
| evt.SupplierType | |
| |> Option.defaultValue state.SupplierType | |
| Modified = evt.Modified | |
| } | |
| let applyStatusChanged (evt: SupplierStatusChangedEvt) (state: Supplier) : Supplier = | |
| { state with | |
| IsActive = evt.IsActive | |
| Modified = evt.Modified | |
| } | |
| let applyRegionAdded (evt: SupplierRegionAddedEvt) (state: Supplier) : Supplier = | |
| let newRegions = state.Regions @ [ evt.RegionId ] | |
| { state with | |
| Regions = newRegions | |
| Modified = evt.Modified | |
| } | |
| let applyRegionRemoved (evt: SupplierRegionRemovedEvt) (state: Supplier) : Supplier = | |
| let newRegions = | |
| state.Regions | |
| |> List.filter (fun r -> r <> evt.RegionId) | |
| { state with | |
| Regions = newRegions | |
| Modified = evt.Modified | |
| } | |
| let evolve (state: Supplier option) (event: SupplierEvent) : Supplier option = | |
| match event, state with | |
| | SupplierCreated e, None -> Some(applyCreated e) | |
| | SupplierInfoUpdated e, Some s -> Some(applyInfoUpdated e s) | |
| | SupplierStatusChanged e, Some s -> Some(applyStatusChanged e s) | |
| | SupplierRegionAdded e, Some s -> Some(applyRegionAdded e s) | |
| | SupplierRegionRemoved e, Some s -> Some(applyRegionRemoved e s) | |
| | SupplierCreated _, Some _ -> state // Idempotent - supplier already exists | |
| | _, None -> None // Can't apply updates to non-existent supplier |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Medhavi.Domain.Material.SupplierOffer | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open System.Text.Json.Serialization | |
| /// Price tier for supplier offers | |
| [<JsonFSharpConverter>] | |
| type PriceTier = | |
| { | |
| TierNumber: int | |
| MinQuantity: decimal | |
| MaxQuantity: decimal option | |
| PricePerUnit: decimal | |
| Currency: string | |
| } | |
| /// Incoterms for supplier offers | |
| [<JsonFSharpConverter>] | |
| type Incoterm = | |
| | FOB // Free On Board | |
| | CIF // Cost, Insurance, Freight | |
| | EXW // Ex Works | |
| | DDP // Delivered Duty Paid | |
| | Other of string | |
| /// Supplier capacity window | |
| [<JsonFSharpConverter>] | |
| type SupplierCapacityWindow = | |
| { | |
| WindowId: string | |
| StartDate: DateTimeOffset | |
| EndDate: DateTimeOffset | |
| MaxQuantity: decimal | |
| AvailableQuantity: decimal | |
| } | |
| /// Supplier Offer aggregate | |
| /// Represents a supplier's offer for a specific product/stocking point combination | |
| type SupplierOffer = | |
| { | |
| Id: SupplierOfferId | |
| SupplierId: SupplierId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId option | |
| Moq: decimal option // Minimum Order Quantity | |
| LotSize: decimal option | |
| LeadTimeP50: TimeSpan option // Median lead time | |
| LeadTimeP95: TimeSpan option // 95th percentile lead time | |
| PriceTiers: PriceTier list | |
| Reliability: float option // 0.0-1.0 | |
| Incoterm: Incoterm option | |
| CapacityWindows: SupplierCapacityWindow list | |
| IsActive: bool | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // ================================================================================================= | |
| // SUPPLIER OFFER COMMANDS | |
| // ================================================================================================= | |
| /// Create a new supplier offer | |
| type CreateSupplierOfferCmd = | |
| { | |
| Id: string | |
| SupplierId: SupplierId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId option | |
| Moq: decimal option | |
| LotSize: decimal option | |
| LeadTimeP50: TimeSpan option | |
| LeadTimeP95: TimeSpan option | |
| PriceTiers: PriceTier list | |
| Reliability: float option | |
| Incoterm: Incoterm option | |
| CapacityWindows: SupplierCapacityWindow list | |
| CreatedDate: DateTimeOffset | |
| } | |
| /// Update supplier offer | |
| type UpdateSupplierOfferCmd = | |
| { | |
| Id: SupplierOfferId | |
| Moq: decimal option | |
| LotSize: decimal option | |
| LeadTimeP50: TimeSpan option | |
| LeadTimeP95: TimeSpan option | |
| PriceTiers: PriceTier list option | |
| Reliability: float option | |
| Incoterm: Incoterm option | |
| CapacityWindows: SupplierCapacityWindow list option | |
| ModifiedDate: DateTimeOffset | |
| } | |
| /// Delete supplier offer | |
| type DeleteSupplierOfferCmd = | |
| { | |
| Id: SupplierOfferId | |
| DeletedDate: DateTimeOffset | |
| } | |
| /// Activate/Deactivate supplier offer | |
| type ChangeSupplierOfferStatusCmd = | |
| { | |
| Id: SupplierOfferId | |
| IsActive: bool | |
| ModifiedDate: DateTimeOffset | |
| } | |
| /// Discriminated union of all supplier offer commands | |
| type SupplierOfferCommand = | |
| | CreateSupplierOffer of CreateSupplierOfferCmd | |
| | UpdateSupplierOffer of UpdateSupplierOfferCmd | |
| | DeleteSupplierOffer of DeleteSupplierOfferCmd | |
| | ChangeSupplierOfferStatus of ChangeSupplierOfferStatusCmd | |
| // ================================================================================================= | |
| // SUPPLIER OFFER EVENTS | |
| // ================================================================================================= | |
| /// Supplier offer created event | |
| type SupplierOfferCreatedEvt = | |
| { | |
| Id: SupplierOfferId | |
| SupplierId: SupplierId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId option | |
| Moq: decimal option | |
| LotSize: decimal option | |
| LeadTimeP50: TimeSpan option | |
| LeadTimeP95: TimeSpan option | |
| PriceTiers: PriceTier list | |
| Reliability: float option | |
| Incoterm: Incoterm option | |
| CapacityWindows: SupplierCapacityWindow list | |
| CreatedDate: DateTimeOffset | |
| } | |
| /// Supplier offer updated event | |
| type SupplierOfferUpdatedEvt = | |
| { | |
| Id: SupplierOfferId | |
| Moq: decimal option | |
| LotSize: decimal option | |
| LeadTimeP50: TimeSpan option | |
| LeadTimeP95: TimeSpan option | |
| PriceTiers: PriceTier list option | |
| Reliability: float option | |
| Incoterm: Incoterm option | |
| CapacityWindows: SupplierCapacityWindow list option | |
| ModifiedDate: DateTimeOffset | |
| } | |
| /// Supplier offer deleted event | |
| type SupplierOfferDeletedEvt = | |
| { | |
| Id: SupplierOfferId | |
| DeletedDate: DateTimeOffset | |
| } | |
| /// Supplier offer status changed event | |
| type SupplierOfferStatusChangedEvt = | |
| { | |
| Id: SupplierOfferId | |
| IsActive: bool | |
| ModifiedDate: DateTimeOffset | |
| } | |
| /// Discriminated union of all supplier offer events | |
| type SupplierOfferEvent = | |
| | SupplierOfferCreated of SupplierOfferCreatedEvt | |
| | SupplierOfferUpdated of SupplierOfferUpdatedEvt | |
| | SupplierOfferDeleted of SupplierOfferDeletedEvt | |
| | SupplierOfferStatusChanged of SupplierOfferStatusChangedEvt | |
| // ================================================================================================= | |
| // SUPPLIER OFFER DOMAIN LOGIC SIGNATURES | |
| // ================================================================================================= | |
| /// Decision function signature | |
| type DecideSupplierOffer = SupplierOffer option -> SupplierOfferCommand -> Result<SupplierOfferEvent list, DomainError> | |
| /// Evolution function signature | |
| type EvolveSupplierOffer = Medhavi.Domain.Evolve<SupplierOffer, SupplierOfferEvent> | |
| // ================================================================================================= | |
| // VALIDATION FUNCTIONS | |
| // ================================================================================================= | |
| /// Validate create supplier offer command | |
| let validateCreate (cmd: CreateSupplierOfferCmd) : Result<unit, DomainError> = | |
| // Basic input validation | |
| if String.IsNullOrWhiteSpace cmd.Id then | |
| Error(DomainError.validation "SupplierOffer ID cannot be empty") | |
| elif cmd.PriceTiers.IsEmpty then | |
| Error(DomainError.validation "SupplierOffer must have at least one price tier") | |
| elif | |
| cmd.PriceTiers | |
| |> List.exists (fun tier -> tier.MinQuantity < 0m) | |
| then | |
| Error(DomainError.validation "Price tier MinQuantity cannot be negative") | |
| elif | |
| cmd.PriceTiers | |
| |> List.exists (fun tier -> tier.PricePerUnit < 0m) | |
| then | |
| Error(DomainError.validation "Price tier PricePerUnit cannot be negative") | |
| elif | |
| cmd.Reliability.IsSome | |
| && (cmd.Reliability.Value < 0.0 | |
| || cmd.Reliability.Value > 1.0) | |
| then | |
| Error(DomainError.validation "Reliability must be between 0.0 and 1.0") | |
| else | |
| Ok() | |
| /// Validate update supplier offer command | |
| let validateUpdate (cmd: UpdateSupplierOfferCmd) : Result<unit, DomainError> = | |
| // Validate price tiers if provided | |
| match cmd.PriceTiers with | |
| | Some tiers when tiers.IsEmpty -> Error(DomainError.validation "Price tiers cannot be empty") | |
| | Some tiers when | |
| tiers | |
| |> List.exists (fun tier -> tier.MinQuantity < 0m) | |
| -> | |
| Error(DomainError.validation "Price tier MinQuantity cannot be negative") | |
| | Some tiers when | |
| tiers | |
| |> List.exists (fun tier -> tier.PricePerUnit < 0m) | |
| -> | |
| Error(DomainError.validation "Price tier PricePerUnit cannot be negative") | |
| | _ -> Ok() | |
| /// Validate change status command | |
| let validateChangeStatus (_cmd: ChangeSupplierOfferStatusCmd) : Result<unit, DomainError> = | |
| // Status changes are always allowed | |
| Ok() | |
| // ================================================================================================= | |
| // STATE EVOLUTION FUNCTIONS | |
| // ================================================================================================= | |
| /// Apply supplier offer created event | |
| let applyCreated (evt: SupplierOfferCreatedEvt) : SupplierOffer = | |
| { | |
| Id = evt.Id | |
| SupplierId = evt.SupplierId | |
| ProductId = evt.ProductId | |
| StockingPointId = evt.StockingPointId | |
| Moq = evt.Moq | |
| LotSize = evt.LotSize | |
| LeadTimeP50 = evt.LeadTimeP50 | |
| LeadTimeP95 = evt.LeadTimeP95 | |
| PriceTiers = evt.PriceTiers | |
| Reliability = evt.Reliability | |
| Incoterm = evt.Incoterm | |
| CapacityWindows = evt.CapacityWindows | |
| IsActive = true // New offers start as active | |
| CreatedDate = evt.CreatedDate | |
| ModifiedDate = evt.CreatedDate | |
| } | |
| /// Apply supplier offer updated event | |
| let applyUpdated (evt: SupplierOfferUpdatedEvt) (state: SupplierOffer) : SupplierOffer = | |
| { state with | |
| Moq = evt.Moq |> Option.orElse state.Moq | |
| LotSize = evt.LotSize |> Option.orElse state.LotSize | |
| LeadTimeP50 = evt.LeadTimeP50 |> Option.orElse state.LeadTimeP50 | |
| LeadTimeP95 = evt.LeadTimeP95 |> Option.orElse state.LeadTimeP95 | |
| PriceTiers = | |
| evt.PriceTiers | |
| |> Option.defaultValue state.PriceTiers | |
| Reliability = evt.Reliability |> Option.orElse state.Reliability | |
| Incoterm = evt.Incoterm |> Option.orElse state.Incoterm | |
| CapacityWindows = | |
| evt.CapacityWindows | |
| |> Option.defaultValue state.CapacityWindows | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| /// Apply supplier offer deleted event | |
| /// Note: In event sourcing, we typically don't delete but mark as deleted | |
| let applyDeleted (_evt: SupplierOfferDeletedEvt) (state: SupplierOffer) : SupplierOffer = | |
| { state with | |
| IsActive = false | |
| ModifiedDate = _evt.DeletedDate | |
| } | |
| /// Apply supplier offer status changed event | |
| let applyStatusChanged (evt: SupplierOfferStatusChangedEvt) (state: SupplierOffer) : SupplierOffer = | |
| { state with | |
| IsActive = evt.IsActive | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| /// Evolve supplier offer state | |
| let evolve (state: SupplierOffer option) (event: SupplierOfferEvent) : SupplierOffer option = | |
| match event, state with | |
| | SupplierOfferCreated e, None -> Some(applyCreated e) | |
| | SupplierOfferUpdated e, Some s -> Some(applyUpdated e s) | |
| | SupplierOfferDeleted e, Some s -> Some(applyDeleted e s) | |
| | SupplierOfferStatusChanged e, Some s -> Some(applyStatusChanged e s) | |
| | SupplierOfferCreated _, Some _ -> state // Idempotent - offer already exists | |
| | _, None -> None // Can't apply updates to non-existent offer |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Medhavi.Domain.Material.SupplyOrder | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open System.Text.Json.Serialization | |
| open Medhavi.Domain.Validation | |
| [<JsonFSharpConverter>] | |
| type SupplyOrderType = | |
| | WorkOrder | |
| | PurchaseOrder | |
| | TransportOrder | |
| [<JsonFSharpConverter>] | |
| type SupplyOrderState = | |
| | Created | |
| | Planned | |
| | Confirmed | |
| | Released | |
| | InProgress | |
| | Completed | |
| | Cancelled | |
| type SupplyOrder = | |
| { | |
| Id: SupplyOrderId | |
| OrderType: SupplyOrderType | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| UnitOfMeasure: UnitOfMeasureId | |
| State: SupplyOrderState | |
| RoutingId: RoutingId option | |
| SupplierId: string option | |
| IsFirm: bool | |
| IsExpedited: bool | |
| IsLocked: bool | |
| UsesLeadTimeQuantity: bool | |
| RequiredDeliveryDate: DateTimeOffset option | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type CreateSupplyOrderCmd = | |
| { | |
| Id: string | |
| OrderType: SupplyOrderType | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| Quantity: Quantity | |
| UnitOfMeasure: UnitOfMeasureId | |
| RoutingId: RoutingId option | |
| SupplierId: string option | |
| IsFirm: bool | |
| IsExpedited: bool | |
| IsLocked: bool | |
| UsesLeadTimeQuantity: bool | |
| RequiredDeliveryDate: DateTimeOffset option | |
| CreatedDate: DateTimeOffset | |
| } | |
| type PlanSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| PlannedDeliveryDate: DateTimeOffset | |
| } | |
| type ConfirmSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| ConfirmedDate: DateTimeOffset | |
| } | |
| type ReleaseSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| ReleasedDate: DateTimeOffset | |
| } | |
| type StartSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| StartedDate: DateTimeOffset | |
| } | |
| type CompleteSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| CompletedDate: DateTimeOffset | |
| } | |
| type PartialCompleteSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| CompletedQuantity: Quantity | |
| CompletedDate: DateTimeOffset | |
| } | |
| type CancelSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| CancelledDate: DateTimeOffset | |
| } | |
| type LockSupplyOrderCmd = | |
| { | |
| Id: SupplyOrderId | |
| Locked: bool | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type UpdateSupplyOrderPriorityCmd = | |
| { | |
| Id: SupplyOrderId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type SupplyOrderCommand = | |
| | CreateSupplyOrder of CreateSupplyOrderCmd | |
| | StartSupplyOrder of StartSupplyOrderCmd | |
| | PartialCompleteSupplyOrder of PartialCompleteSupplyOrderCmd | |
| | CompleteSupplyOrder of CompleteSupplyOrderCmd | |
| | PlanSupplyOrder of PlanSupplyOrderCmd | |
| | ConfirmSupplyOrder of ConfirmSupplyOrderCmd | |
| | ReleaseSupplyOrder of ReleaseSupplyOrderCmd | |
| | CancelSupplyOrder of CancelSupplyOrderCmd | |
| | LockSupplyOrder of LockSupplyOrderCmd | |
| | UpdateSupplyOrderPriority of UpdateSupplyOrderPriorityCmd | |
| // Events | |
| type SupplyOrderCreatedEvt = CreateSupplyOrderCmd | |
| type SupplyOrderPlannedEvt = | |
| { | |
| Id: SupplyOrderId | |
| PlannedDeliveryDate: DateTimeOffset | |
| } | |
| type SupplyOrderConfirmedEvt = | |
| { | |
| Id: SupplyOrderId | |
| ConfirmedDate: DateTimeOffset | |
| } | |
| type SupplyOrderReleasedEvt = | |
| { | |
| Id: SupplyOrderId | |
| ReleasedDate: DateTimeOffset | |
| } | |
| type SupplyOrderStartedEvt = | |
| { | |
| Id: SupplyOrderId | |
| StartedDate: DateTimeOffset | |
| } | |
| type SupplyOrderCompletedEvt = | |
| { | |
| Id: SupplyOrderId | |
| CompletedDate: DateTimeOffset | |
| } | |
| type SupplyOrderPartiallyCompletedEvt = | |
| { | |
| Id: SupplyOrderId | |
| CompletedQuantity: Quantity | |
| CompletedDate: DateTimeOffset | |
| } | |
| type SupplyOrderCancelledEvt = | |
| { | |
| Id: SupplyOrderId | |
| CancelledDate: DateTimeOffset | |
| } | |
| type SupplyOrderLockedEvt = | |
| { | |
| Id: SupplyOrderId | |
| Locked: bool | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type SupplyOrderPriorityUpdatedEvt = | |
| { | |
| Id: SupplyOrderId | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type SupplyOrderEvent = | |
| | SupplyOrderCreated of SupplyOrderCreatedEvt | |
| | SupplyOrderPlanned of SupplyOrderPlannedEvt | |
| | SupplyOrderConfirmed of SupplyOrderConfirmedEvt | |
| | SupplyOrderReleased of SupplyOrderReleasedEvt | |
| | SupplyOrderStarted of SupplyOrderStartedEvt | |
| | SupplyOrderCompleted of SupplyOrderCompletedEvt | |
| | SupplyOrderPartiallyCompleted of SupplyOrderPartiallyCompletedEvt | |
| | SupplyOrderCancelled of SupplyOrderCancelledEvt | |
| | SupplyOrderLocked of SupplyOrderLockedEvt | |
| | SupplyOrderPriorityUpdated of SupplyOrderPriorityUpdatedEvt | |
| // Signatures | |
| type DecideSupplyOrder = SupplyOrder option -> SupplyOrderCommand -> Result<SupplyOrderEvent list, DomainError> | |
| type EvolveSupplyOrder = Medhavi.Domain.Evolve<SupplyOrder, SupplyOrderEvent> | |
| let applyCreated (evt: SupplyOrderCreatedEvt) : SupplyOrder = | |
| let sid = | |
| SupplyOrderId.create evt.Id | |
| |> Result.defaultWith (fun e -> failwith (e.ToString())) | |
| { | |
| Id = sid | |
| OrderType = evt.OrderType | |
| ProductId = evt.ProductId | |
| StockingPointId = evt.StockingPointId | |
| Quantity = evt.Quantity | |
| UnitOfMeasure = evt.UnitOfMeasure | |
| State = SupplyOrderState.Created | |
| RoutingId = evt.RoutingId | |
| SupplierId = evt.SupplierId | |
| IsFirm = evt.IsFirm | |
| IsExpedited = evt.IsExpedited | |
| IsLocked = evt.IsLocked | |
| UsesLeadTimeQuantity = evt.UsesLeadTimeQuantity | |
| RequiredDeliveryDate = evt.RequiredDeliveryDate | |
| CreatedDate = evt.CreatedDate | |
| ModifiedDate = evt.CreatedDate | |
| } | |
| let applyPlanned (evt: SupplyOrderPlannedEvt) (state: SupplyOrder) = | |
| { state with | |
| State = SupplyOrderState.Planned | |
| ModifiedDate = evt.PlannedDeliveryDate | |
| } | |
| let applyConfirmed (evt: SupplyOrderConfirmedEvt) (state: SupplyOrder) = | |
| { state with | |
| State = SupplyOrderState.Confirmed | |
| ModifiedDate = evt.ConfirmedDate | |
| } | |
| let applyReleased (evt: SupplyOrderReleasedEvt) (state: SupplyOrder) = | |
| { state with | |
| State = SupplyOrderState.Released | |
| ModifiedDate = evt.ReleasedDate | |
| } | |
| let applyStarted (evt: SupplyOrderStartedEvt) (state: SupplyOrder) = | |
| { state with | |
| State = SupplyOrderState.InProgress | |
| ModifiedDate = evt.StartedDate | |
| } | |
| let applyCompleted (evt: SupplyOrderCompletedEvt) (state: SupplyOrder) = | |
| { state with | |
| State = SupplyOrderState.Completed | |
| ModifiedDate = evt.CompletedDate | |
| } | |
| let applyPartiallyCompleted (evt: SupplyOrderPartiallyCompletedEvt) (state: SupplyOrder) = | |
| { state with | |
| ModifiedDate = evt.CompletedDate | |
| // State remains InProgress for partial completion | |
| } | |
| let applyCancelled (evt: SupplyOrderCancelledEvt) (state: SupplyOrder) = | |
| { state with | |
| State = SupplyOrderState.Cancelled | |
| ModifiedDate = evt.CancelledDate | |
| } | |
| let applyLocked (evt: SupplyOrderLockedEvt) (state: SupplyOrder) = | |
| { state with | |
| IsLocked = evt.Locked | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let applyPriorityUpdated (evt: SupplyOrderPriorityUpdatedEvt) (state: SupplyOrder) = | |
| { state with | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let evolve (state: SupplyOrder option) (event: SupplyOrderEvent) : SupplyOrder option = | |
| match event, state with | |
| | SupplyOrderCreated e, None -> Some(applyCreated e) | |
| | SupplyOrderPlanned e, Some s -> Some(applyPlanned e s) | |
| | SupplyOrderConfirmed e, Some s -> Some(applyConfirmed e s) | |
| | SupplyOrderReleased e, Some s -> Some(applyReleased e s) | |
| | SupplyOrderStarted e, Some s -> Some(applyStarted e s) | |
| | SupplyOrderCompleted e, Some s -> Some(applyCompleted e s) | |
| | SupplyOrderPartiallyCompleted e, Some s -> Some(applyPartiallyCompleted e s) | |
| | SupplyOrderCancelled e, Some s -> Some(applyCancelled e s) | |
| | SupplyOrderLocked e, Some s -> Some(applyLocked e s) | |
| | SupplyOrderPriorityUpdated e, Some s -> Some(applyPriorityUpdated e s) | |
| | SupplyOrderCreated _, Some _ -> state | |
| | _, current -> current |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| namespace Medhavi.Domain.Material | |
| open System | |
| open Medhavi.Domain.Ids | |
| open System.Text.Json.Serialization | |
| open Medhavi.Domain | |
| /// Work Order Execution Feedback | |
| /// Tracks variance, scrap, and rework for work orders | |
| [<JsonFSharpConverter>] | |
| type WorkOrderExecution = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| PlannedQuantity: decimal | |
| ActualQuantity: decimal option | |
| PlannedStartTime: DateTimeOffset option | |
| ActualStartTime: DateTimeOffset option | |
| PlannedEndTime: DateTimeOffset option | |
| ActualEndTime: DateTimeOffset option | |
| ScrapQuantity: decimal | |
| ScrapReasons: string list | |
| ReworkQuantity: decimal | |
| ReworkReasons: string list | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type CreateWorkOrderExecutionCmd = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| PlannedQuantity: decimal | |
| PlannedStartTime: DateTimeOffset option | |
| PlannedEndTime: DateTimeOffset option | |
| CreatedDate: DateTimeOffset | |
| } | |
| type RecordVarianceCmd = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| ActualQuantity: decimal option | |
| ActualStartTime: DateTimeOffset option | |
| ActualEndTime: DateTimeOffset option | |
| RecordedDate: DateTimeOffset | |
| } | |
| type RecordScrapCmd = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| ScrapQuantity: decimal | |
| ScrapReason: string | |
| RecordedDate: DateTimeOffset | |
| } | |
| type RecordReworkCmd = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| ReworkQuantity: decimal | |
| ReworkReason: string | |
| RecordedDate: DateTimeOffset | |
| } | |
| type WorkOrderExecutionCommand = | |
| | CreateWorkOrderExecution of CreateWorkOrderExecutionCmd | |
| | RecordVariance of RecordVarianceCmd | |
| | RecordScrap of RecordScrapCmd | |
| | RecordRework of RecordReworkCmd | |
| // Events | |
| type WorkOrderExecutionCreatedEvt = CreateWorkOrderExecutionCmd | |
| type VarianceRecordedEvt = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| ActualQuantity: decimal option | |
| ActualStartTime: DateTimeOffset option | |
| ActualEndTime: DateTimeOffset option | |
| RecordedDate: DateTimeOffset | |
| } | |
| type ScrapRecordedEvt = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| ScrapQuantity: decimal | |
| ScrapReason: string | |
| RecordedDate: DateTimeOffset | |
| } | |
| type ReworkRecordedEvt = | |
| { | |
| SupplyOrderId: SupplyOrderId | |
| ReworkQuantity: decimal | |
| ReworkReason: string | |
| RecordedDate: DateTimeOffset | |
| } | |
| type WorkOrderExecutionEvent = | |
| | WorkOrderExecutionCreated of WorkOrderExecutionCreatedEvt | |
| | VarianceRecorded of VarianceRecordedEvt | |
| | ScrapRecorded of ScrapRecordedEvt | |
| | ReworkRecorded of ReworkRecordedEvt | |
| // Signatures | |
| type DecideWorkOrderExecution = | |
| WorkOrderExecution option -> WorkOrderExecutionCommand -> Result<WorkOrderExecutionEvent list, DomainError> | |
| type EvolveWorkOrderExecution = Evolve<WorkOrderExecution, WorkOrderExecutionEvent> | |
| // Apply functions | |
| let applyCreated (evt: WorkOrderExecutionCreatedEvt) : WorkOrderExecution = | |
| { | |
| SupplyOrderId = evt.SupplyOrderId | |
| PlannedQuantity = evt.PlannedQuantity | |
| ActualQuantity = None | |
| PlannedStartTime = evt.PlannedStartTime | |
| ActualStartTime = None | |
| PlannedEndTime = evt.PlannedEndTime | |
| ActualEndTime = None | |
| ScrapQuantity = 0m | |
| ScrapReasons = [] | |
| ReworkQuantity = 0m | |
| ReworkReasons = [] | |
| CreatedDate = evt.CreatedDate | |
| ModifiedDate = evt.CreatedDate | |
| } | |
| let applyVariance (evt: VarianceRecordedEvt) (state: WorkOrderExecution) : WorkOrderExecution = | |
| { state with | |
| ActualQuantity = evt.ActualQuantity |> Option.orElse state.ActualQuantity | |
| ActualStartTime = evt.ActualStartTime |> Option.orElse state.ActualStartTime | |
| ActualEndTime = evt.ActualEndTime |> Option.orElse state.ActualEndTime | |
| ModifiedDate = evt.RecordedDate | |
| } | |
| let applyScrap (evt: ScrapRecordedEvt) (state: WorkOrderExecution) : WorkOrderExecution = | |
| { state with | |
| ScrapQuantity = state.ScrapQuantity + evt.ScrapQuantity | |
| ScrapReasons = evt.ScrapReason :: state.ScrapReasons | |
| ModifiedDate = evt.RecordedDate | |
| } | |
| let applyRework (evt: ReworkRecordedEvt) (state: WorkOrderExecution) : WorkOrderExecution = | |
| { state with | |
| ReworkQuantity = state.ReworkQuantity + evt.ReworkQuantity | |
| ReworkReasons = evt.ReworkReason :: state.ReworkReasons | |
| ModifiedDate = evt.RecordedDate | |
| } | |
| let evolve (state: WorkOrderExecution option) (event: WorkOrderExecutionEvent) : WorkOrderExecution option = | |
| match event, state with | |
| | WorkOrderExecutionCreated e, None -> Some(applyCreated e) | |
| | VarianceRecorded e, Some s -> Some(applyVariance e s) | |
| | ScrapRecorded e, Some s -> Some(applyScrap e s) | |
| | ReworkRecorded e, Some s -> Some(applyRework e s) | |
| | WorkOrderExecutionCreated _, Some _ -> state | |
| | _, None -> None |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment