Created
February 16, 2026 21:16
-
-
Save achal7/8060d1a62cb67e2cc59dacdcd8b12129 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.Customer | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Common.ResultCE | |
| type Customer = | |
| { | |
| Id: CustomerId | |
| Name: string | |
| Group: string | |
| Priority: int option | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineCustomerCmd = | |
| { | |
| Id: string | |
| Name: string | |
| Group: string | |
| Priority: int option | |
| IsActive: bool | |
| } | |
| type UpdateCustomerCmd = | |
| { | |
| Id: CustomerId | |
| Name: string option | |
| Group: string option | |
| Priority: int option | |
| IsActive: bool option | |
| } | |
| type RetireCustomerCmd = | |
| { | |
| Id: CustomerId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type CustomerCommand = | |
| | DefineCustomer of DefineCustomerCmd | |
| | UpdateCustomer of UpdateCustomerCmd | |
| | RetireCustomer of RetireCustomerCmd | |
| // Events | |
| type CustomerDefinedEvt = | |
| { | |
| Id: CustomerId | |
| Name: string | |
| Group: string | |
| Priority: int option | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| } | |
| type CustomerUpdatedEvt = | |
| { | |
| Id: CustomerId | |
| Name: string option | |
| Group: string option | |
| Priority: int option | |
| IsActive: bool option | |
| Modified: DateTimeOffset | |
| } | |
| type CustomerRetiredEvt = | |
| { | |
| Id: CustomerId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type CustomerEvent = | |
| | CustomerDefined of CustomerDefinedEvt | |
| | CustomerUpdated of CustomerUpdatedEvt | |
| | CustomerRetired of CustomerRetiredEvt | |
| // Signatures | |
| type DecideCustomer = Customer option -> CustomerCommand -> Result<CustomerEvent list, DomainError> | |
| type EvolveCustomer = Evolve<Customer, CustomerEvent> | |
| // Validation functions (includes business rules) | |
| let validateDefine (cmd: DefineCustomerCmd) : Result<unit, DomainError> = | |
| result { | |
| let! _ = required "Customer Id" cmd.Id | |
| let! _ = required "Customer name" cmd.Name | |
| let! _ = required "Customer group" cmd.Group | |
| return () | |
| } | |
| let validateUpdate (cmd: UpdateCustomerCmd) : Result<unit, DomainError> = | |
| // Validate that at least one field is being updated | |
| match cmd.Name, cmd.Group, cmd.Priority, cmd.IsActive with | |
| | None, None, None, None -> Error(DomainError.validation "At least one field must be updated") | |
| | Some name, _, _, _ when String.IsNullOrWhiteSpace name -> | |
| Error(DomainError.validation "Customer name cannot be empty") | |
| | _ -> Ok() | |
| let validateRetire (_cmd: RetireCustomerCmd) : Result<unit, DomainError> = | |
| // Retirement is always allowed | |
| Ok() | |
| // State evolution functions (pure state transitions) | |
| let applyDefined (evt: CustomerDefinedEvt) : Customer = | |
| { | |
| Id = evt.Id | |
| Name = evt.Name | |
| Group = evt.Group | |
| Priority = evt.Priority | |
| IsActive = evt.IsActive | |
| Created = evt.Created | |
| Modified = evt.Created | |
| } | |
| let applyUpdated (evt: CustomerUpdatedEvt) (state: Customer) : Customer = | |
| { state with | |
| Name = evt.Name |> Option.defaultValue state.Name | |
| Group = evt.Group |> Option.defaultValue state.Group | |
| Priority = evt.Priority |> Option.orElse state.Priority | |
| IsActive = evt.IsActive |> Option.defaultValue state.IsActive | |
| Modified = evt.Modified | |
| } | |
| let applyRetired (evt: CustomerRetiredEvt) (state: Customer) : Customer option = | |
| Some | |
| { state with | |
| IsActive = false | |
| Modified = evt.RetiredAt | |
| } | |
| let evolve (state: Customer option) (event: CustomerEvent) : Customer option = | |
| match event, state with | |
| | CustomerDefined e, None -> Some(applyDefined e) | |
| | CustomerUpdated e, Some s -> Some(applyUpdated e s) | |
| | CustomerRetired e, Some s -> applyRetired e s | |
| | CustomerRetired _, None -> None | |
| | CustomerDefined _, Some _ -> state // Idempotent - customer already exists | |
| | _, None -> None // Can't apply updates to non-existent customer |
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.CustomerOrder | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| open System.Text.Json.Serialization | |
| [<JsonFSharpConverter>] | |
| type OrderPriority = | |
| | Low | |
| | Normal | |
| | High | |
| | Critical | |
| [<JsonFSharpConverter>] | |
| type OrderAcceptanceStatus = | |
| | PendingEvaluation | |
| | Accepted of CommittedDeliveryDate: DateTimeOffset | |
| | Rejected of Reason: string | |
| | PartiallyAccepted of CommittedDeliveryDate: DateTimeOffset * PartialQuantity: Quantity | |
| [<JsonFSharpConverter>] | |
| type OrderConstraint = | |
| | FullDelivery // no split deliveries per line | |
| | FullOrder // all lines deliver together | |
| | NoConstraint | |
| type QuantityTolerance = | |
| { | |
| MinTolerancePct: Percent option // e.g., -0.05 for -5% | |
| MaxTolerancePct: Percent option // e.g., +0.05 for +5% | |
| } | |
| type OrderLine = | |
| { | |
| Id: OrderLineId | |
| LineNumber: int | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| /// Optional origin stocking point for transport (defaults to stocking point when missing). | |
| Origin: StockingPointId option | |
| /// Optional destination stocking point for transport (defaults to stocking point when missing). | |
| Destination: StockingPointId option | |
| Quantity: Quantity | |
| UnitOfMeasure: UnitOfMeasureId | |
| RequiredDeliveryDate: DateTimeOffset | |
| EarliestDesiredDelivery: DateTimeOffset option | |
| LatestDesiredDelivery: DateTimeOffset option | |
| } | |
| type CustomerOrder = | |
| { | |
| Id: CustomerOrderId | |
| CustomerId: string | |
| OrderNumber: string | |
| Priority: OrderPriority | |
| Lines: OrderLine list | |
| AcceptanceStatus: OrderAcceptanceStatus | |
| OrderConstraint: OrderConstraint | |
| QuantityTolerance: QuantityTolerance | |
| MaterialReservations: MaterialReservationId list | |
| CapacityAllocations: CapacityAllocationId list | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type CreateCustomerOrderCmd = | |
| { | |
| Id: string | |
| CustomerId: string | |
| OrderNumber: string | |
| Priority: OrderPriority | |
| Lines: OrderLine list | |
| OrderConstraint: OrderConstraint | |
| QuantityTolerance: QuantityTolerance | |
| CreatedDate: DateTimeOffset | |
| } | |
| type AcceptCustomerOrderCmd = | |
| { | |
| Id: CustomerOrderId | |
| CommittedDeliveryDate: DateTimeOffset | |
| } | |
| type RejectCustomerOrderCmd = { Id: CustomerOrderId; Reason: string } | |
| type PartiallyAcceptCustomerOrderCmd = | |
| { | |
| Id: CustomerOrderId | |
| CommittedDeliveryDate: DateTimeOffset | |
| PartialQuantity: Quantity | |
| } | |
| type UpdateCustomerOrderPriorityCmd = | |
| { | |
| Id: CustomerOrderId | |
| Priority: OrderPriority | |
| } | |
| type UpdateCustomerOrderConstraintsCmd = | |
| { | |
| Id: CustomerOrderId | |
| OrderConstraint: OrderConstraint | |
| QuantityTolerance: QuantityTolerance | |
| } | |
| type CustomerOrderCommand = | |
| | CreateCustomerOrder of CreateCustomerOrderCmd | |
| | AcceptCustomerOrder of AcceptCustomerOrderCmd | |
| | RejectCustomerOrder of RejectCustomerOrderCmd | |
| | PartiallyAcceptCustomerOrder of PartiallyAcceptCustomerOrderCmd | |
| | UpdateCustomerOrderPriority of UpdateCustomerOrderPriorityCmd | |
| | UpdateCustomerOrderConstraints of UpdateCustomerOrderConstraintsCmd | |
| // Events | |
| type CustomerOrderCreatedEvt = | |
| { | |
| Id: CustomerOrderId | |
| CustomerId: string | |
| OrderNumber: string | |
| Priority: OrderPriority | |
| Lines: OrderLine list | |
| OrderConstraint: OrderConstraint | |
| QuantityTolerance: QuantityTolerance | |
| CreatedDate: DateTimeOffset | |
| } | |
| type CustomerOrderAcceptedEvt = | |
| { | |
| Id: CustomerOrderId | |
| CommittedDeliveryDate: DateTimeOffset | |
| } | |
| type CustomerOrderRejectedEvt = { Id: CustomerOrderId; Reason: string } | |
| type CustomerOrderPartiallyAcceptedEvt = | |
| { | |
| Id: CustomerOrderId | |
| CommittedDeliveryDate: DateTimeOffset | |
| PartialQuantity: Quantity | |
| } | |
| type CustomerOrderPriorityUpdatedEvt = | |
| { | |
| Id: CustomerOrderId | |
| Priority: OrderPriority | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type CustomerOrderConstraintsUpdatedEvt = | |
| { | |
| Id: CustomerOrderId | |
| OrderConstraint: OrderConstraint | |
| QuantityTolerance: QuantityTolerance | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type CustomerOrderEvent = | |
| | CustomerOrderCreated of CustomerOrderCreatedEvt | |
| | CustomerOrderAccepted of CustomerOrderAcceptedEvt | |
| | CustomerOrderRejected of CustomerOrderRejectedEvt | |
| | CustomerOrderPartiallyAccepted of CustomerOrderPartiallyAcceptedEvt | |
| | CustomerOrderPriorityUpdated of CustomerOrderPriorityUpdatedEvt | |
| | CustomerOrderConstraintsUpdated of CustomerOrderConstraintsUpdatedEvt | |
| // Signatures | |
| type DecideCustomerOrder = CustomerOrder option -> CustomerOrderCommand -> Result<CustomerOrderEvent list, DomainError> | |
| type EvolveCustomerOrder = Evolve<CustomerOrder, CustomerOrderEvent> | |
| let validateCreate (cmd: CreateCustomerOrderCmd) : Result<unit, DomainError> = | |
| required "CustomerOrder Id" cmd.Id | |
| |> Result.bind (fun _ -> required "Customer Id" cmd.CustomerId) | |
| |> Result.bind (fun _ -> required "OrderNumber" cmd.OrderNumber) | |
| |> Result.map (fun _ -> ()) | |
| let applyCreated (evt: CustomerOrderCreatedEvt) : CustomerOrder = | |
| { | |
| Id = evt.Id | |
| CustomerId = evt.CustomerId | |
| OrderNumber = evt.OrderNumber | |
| Priority = evt.Priority | |
| Lines = evt.Lines | |
| AcceptanceStatus = OrderAcceptanceStatus.PendingEvaluation | |
| OrderConstraint = evt.OrderConstraint | |
| QuantityTolerance = evt.QuantityTolerance | |
| MaterialReservations = [] | |
| CapacityAllocations = [] | |
| CreatedDate = evt.CreatedDate | |
| ModifiedDate = evt.CreatedDate | |
| } | |
| let applyAccepted (evt: CustomerOrderAcceptedEvt) (state: CustomerOrder) : CustomerOrder = | |
| { state with | |
| AcceptanceStatus = OrderAcceptanceStatus.Accepted evt.CommittedDeliveryDate | |
| ModifiedDate = evt.CommittedDeliveryDate | |
| } | |
| let applyRejected (evt: CustomerOrderRejectedEvt) (state: CustomerOrder) : CustomerOrder = | |
| { state with | |
| AcceptanceStatus = OrderAcceptanceStatus.Rejected evt.Reason | |
| ModifiedDate = DateTimeOffset.UtcNow | |
| } | |
| let applyPartiallyAccepted (evt: CustomerOrderPartiallyAcceptedEvt) (state: CustomerOrder) : CustomerOrder = | |
| { state with | |
| AcceptanceStatus = OrderAcceptanceStatus.PartiallyAccepted(evt.CommittedDeliveryDate, evt.PartialQuantity) | |
| ModifiedDate = evt.CommittedDeliveryDate | |
| } | |
| let applyPriorityUpdated (evt: CustomerOrderPriorityUpdatedEvt) (state: CustomerOrder) : CustomerOrder = | |
| { state with | |
| Priority = evt.Priority | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let applyConstraintsUpdated (evt: CustomerOrderConstraintsUpdatedEvt) (state: CustomerOrder) : CustomerOrder = | |
| { state with | |
| OrderConstraint = evt.OrderConstraint | |
| QuantityTolerance = evt.QuantityTolerance | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let evolve (state: CustomerOrder option) (event: CustomerOrderEvent) : CustomerOrder option = | |
| match event, state with | |
| | CustomerOrderCreated e, None -> Some(applyCreated e) | |
| | CustomerOrderAccepted e, Some s -> Some(applyAccepted e s) | |
| | CustomerOrderRejected e, Some s -> Some(applyRejected e s) | |
| | CustomerOrderPartiallyAccepted e, Some s -> Some(applyPartiallyAccepted e s) | |
| | CustomerOrderPriorityUpdated e, Some s -> Some(applyPriorityUpdated e s) | |
| | CustomerOrderConstraintsUpdated e, Some s -> Some(applyConstraintsUpdated e s) | |
| | CustomerOrderCreated _, 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
| module Medhavi.Domain.Plant | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| type Plant = | |
| { | |
| Id: PlantId | |
| Code: string | |
| Name: string | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| RetiredAt: DateTimeOffset option | |
| } | |
| // Commands | |
| type DefinePlantCmd = | |
| { | |
| Id: string | |
| Code: string | |
| Name: string | |
| } | |
| type RenamePlantCmd = { Id: PlantId; NewName: string } | |
| type RetirePlantCmd = | |
| { | |
| Id: PlantId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type PlantCommand = | |
| | DefinePlant of DefinePlantCmd | |
| | RenamePlant of RenamePlantCmd | |
| | RetirePlant of RetirePlantCmd | |
| // Events | |
| type PlantDefinedEvt = | |
| { | |
| Id: PlantId | |
| Code: string | |
| Name: string | |
| Created: DateTimeOffset | |
| } | |
| type PlantRenamedEvt = | |
| { | |
| Id: PlantId | |
| NewName: string | |
| Modified: DateTimeOffset | |
| } | |
| type PlantRetiredEvt = | |
| { | |
| Id: PlantId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type PlantEvent = | |
| | PlantDefined of PlantDefinedEvt | |
| | PlantRenamed of PlantRenamedEvt | |
| | PlantRetired of PlantRetiredEvt | |
| // Signatures | |
| type DecidePlant = Plant option -> PlantCommand -> Result<PlantEvent list, DomainError> | |
| type EvolvePlant = Medhavi.Domain.Evolve<Plant, PlantEvent> | |
| let validateDefinePlant (cmd: DefinePlantCmd) : Result<unit, DomainError> = | |
| required "Plant code" cmd.Code | |
| |> Result.bind (fun _ -> required "Plant name" cmd.Name) | |
| |> Result.map (fun _ -> ()) | |
| let applyDefined (evt: PlantDefinedEvt) : Plant = | |
| { | |
| Id = evt.Id | |
| Code = evt.Code | |
| Name = evt.Name | |
| Created = evt.Created | |
| Modified = evt.Created | |
| RetiredAt = None | |
| } | |
| let applyRenamed (evt: PlantRenamedEvt) (state: Plant) : Plant = | |
| { state with | |
| Name = evt.NewName | |
| Modified = evt.Modified | |
| } | |
| let applyRetired (evt: PlantRetiredEvt) (state: Plant) : Plant = | |
| { state with | |
| RetiredAt = Some evt.RetiredAt | |
| Modified = evt.RetiredAt | |
| } | |
| let evolve (state: Plant option) (event: PlantEvent) : Plant option = | |
| match event, state with | |
| | PlantDefined e, None -> Some(applyDefined e) | |
| | PlantRenamed e, Some s -> Some(applyRenamed e s) | |
| | PlantRetired e, Some s -> Some(applyRetired e s) | |
| | PlantRetired _, None -> None | |
| | _, 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
| module Medhavi.Domain.Product | |
| open System | |
| open System.Text.Json.Serialization | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain.UnitOfMeasure | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| /// Product master (immutable record; no behavior here). | |
| [<JsonFSharpConverter>] | |
| type ProductState = | |
| | Active | |
| | Inactive | |
| type ProductionAttributes = | |
| { | |
| BaseUoM: UnitOfMeasureId | |
| BaseQuantity: Quantity | |
| LeadTimeQuantity: Quantity | |
| PieceWeight: float | |
| } | |
| type InventoryPolicy = | |
| { | |
| StockCoveredDays: float | |
| SupplyCanBeSplit: bool | |
| } | |
| type Product = | |
| { | |
| Id: ProductId | |
| ProductTypeId: string option | |
| Code: string | |
| Name: string | |
| ProductGroup: string | |
| State: ProductState | |
| ProductionAttributes: ProductionAttributes | |
| InventoryPolicy: InventoryPolicy | |
| Description: string option | |
| CreatedDate: DateTimeOffset | |
| ModifiedDate: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineProductCmd = | |
| { | |
| Id: string | |
| Code: string | |
| Name: string | |
| ProductGroup: string | |
| ProductionAttributes: | |
| {| | |
| BaseUoM: string | |
| BaseQuantity: Quantity | |
| LeadTimeQuantity: Quantity | |
| PieceWeight: float | |
| |} | |
| InventoryPolicy: | |
| {| | |
| StockCoveredDays: float | |
| SupplyCanBeSplit: bool | |
| |} | |
| Description: string option | |
| } | |
| type UpdateProductCmd = | |
| { | |
| Id: ProductId | |
| NewName: string option | |
| NewDescription: string option | |
| NewInventoryPolicy: InventoryPolicy option | |
| NewState: ProductState option | |
| } | |
| type RetireProductCmd = | |
| { | |
| Id: ProductId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type ProductCommand = | |
| | DefineProduct of DefineProductCmd | |
| | UpdateProduct of UpdateProductCmd | |
| | RetireProduct of RetireProductCmd | |
| // Events | |
| type ProductDefinedEvt = | |
| { | |
| Id: ProductId | |
| Code: string | |
| Name: string | |
| ProductGroup: string | |
| ProductionAttributes: ProductionAttributes | |
| InventoryPolicy: InventoryPolicy | |
| Description: string option | |
| CreatedDate: DateTimeOffset | |
| } | |
| type ProductUpdatedEvt = | |
| { | |
| Id: ProductId | |
| NewName: string option | |
| NewDescription: string option | |
| NewInventoryPolicy: InventoryPolicy option | |
| NewState: ProductState option | |
| ModifiedDate: DateTimeOffset | |
| } | |
| type ProductRetiredEvt = | |
| { | |
| Id: ProductId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type ProductEvent = | |
| | ProductDefined of ProductDefinedEvt | |
| | ProductUpdated of ProductUpdatedEvt | |
| | ProductRetired of ProductRetiredEvt | |
| // Signatures | |
| type DecideProduct = Product option -> ProductCommand -> Result<ProductEvent list, DomainError> | |
| type EvolveProduct = Medhavi.Domain.Evolve<Product, ProductEvent> | |
| let validateDefineProduct (cmd: DefineProductCmd) : Result<unit, DomainError> = | |
| required "Product code" cmd.Code | |
| |> Result.bind (fun _ -> required "Product name" cmd.Name) | |
| |> Result.bind (fun _ -> required "Product group" cmd.ProductGroup) | |
| |> Result.map (fun _ -> ()) | |
| let applyDefined (evt: ProductDefinedEvt) : Product = | |
| { | |
| Id = evt.Id | |
| ProductTypeId = None | |
| Code = evt.Code | |
| Name = evt.Name | |
| ProductGroup = evt.ProductGroup | |
| State = ProductState.Active | |
| ProductionAttributes = evt.ProductionAttributes | |
| InventoryPolicy = evt.InventoryPolicy | |
| Description = evt.Description | |
| CreatedDate = evt.CreatedDate | |
| ModifiedDate = evt.CreatedDate | |
| } | |
| let applyUpdated (evt: ProductUpdatedEvt) (state: Product) : Product = | |
| { state with | |
| Name = evt.NewName |> Option.defaultValue state.Name | |
| Description = | |
| evt.NewDescription | |
| |> Option.orElse state.Description | |
| InventoryPolicy = | |
| evt.NewInventoryPolicy | |
| |> Option.defaultValue state.InventoryPolicy | |
| State = evt.NewState |> Option.defaultValue state.State | |
| ModifiedDate = evt.ModifiedDate | |
| } | |
| let applyRetired (_evt: ProductRetiredEvt) (_state: Product) : Product option = None | |
| // Evolve: apply events to state | |
| let evolve (state: Product option) (event: ProductEvent) : Product option = | |
| match event, state with | |
| | ProductEvent.ProductDefined e, None -> Some(applyDefined e) | |
| | ProductEvent.ProductUpdated e, Some s -> Some(applyUpdated e s) | |
| | ProductEvent.ProductRetired e, Some s -> applyRetired e s | |
| | ProductEvent.ProductRetired _, None -> None | |
| | _, 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.Core | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| type SubstitutionPriority = int | |
| type ProductSubstitutionRule = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| PrimaryProductId: ProductId | |
| SubstituteProductId: ProductId | |
| ConversionRate: float | |
| Priority: SubstitutionPriority | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineProductSubstitutionRuleCmd = | |
| { | |
| Id: string | |
| PrimaryProductId: ProductId | |
| SubstituteProductId: ProductId | |
| ConversionRate: float | |
| Priority: SubstitutionPriority | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| } | |
| type ActivateProductSubstitutionRuleCmd = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| Modified: DateTimeOffset | |
| } | |
| type DeactivateProductSubstitutionRuleCmd = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| Modified: DateTimeOffset | |
| } | |
| type ExpireProductSubstitutionRuleCmd = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| EffectiveEnd: DateTimeOffset | |
| } | |
| type ProductSubstitutionRuleCommand = | |
| | DefineProductSubstitutionRule of DefineProductSubstitutionRuleCmd | |
| | ActivateProductSubstitutionRule of ActivateProductSubstitutionRuleCmd | |
| | DeactivateProductSubstitutionRule of DeactivateProductSubstitutionRuleCmd | |
| | ExpireProductSubstitutionRule of ExpireProductSubstitutionRuleCmd | |
| // Events | |
| type ProductSubstitutionRuleDefinedEvt = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| PrimaryProductId: ProductId | |
| SubstituteProductId: ProductId | |
| ConversionRate: float | |
| Priority: SubstitutionPriority | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| } | |
| type ProductSubstitutionRuleActivatedEvt = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| Modified: DateTimeOffset | |
| } | |
| type ProductSubstitutionRuleDeactivatedEvt = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| Modified: DateTimeOffset | |
| } | |
| type ProductSubstitutionRuleExpiredEvt = | |
| { | |
| Id: ProductSubstitutionRuleId | |
| EffectiveEnd: DateTimeOffset | |
| } | |
| type ProductSubstitutionRuleEvent = | |
| | ProductSubstitutionRuleDefined of ProductSubstitutionRuleDefinedEvt | |
| | ProductSubstitutionRuleActivated of ProductSubstitutionRuleActivatedEvt | |
| | ProductSubstitutionRuleDeactivated of ProductSubstitutionRuleDeactivatedEvt | |
| | ProductSubstitutionRuleExpired of ProductSubstitutionRuleExpiredEvt | |
| // Signatures | |
| type DecideProductSubstitutionRule = | |
| ProductSubstitutionRule option | |
| -> ProductSubstitutionRuleCommand | |
| -> Result<ProductSubstitutionRuleEvent list, DomainError> | |
| type EvolveProductSubstitutionRule = | |
| ProductSubstitutionRule option -> ProductSubstitutionRuleEvent -> ProductSubstitutionRule |
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.Routing | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open System.Text.Json.Serialization | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Common.ResultCE | |
| [<JsonFSharpConverter>] | |
| type RoutingType = | |
| | Purchase | |
| | Transport | |
| | Work | |
| type RoutingLotSizing = | |
| { | |
| HasLotSize: bool | |
| LotSize: Quantity option | |
| MinQuantity: Quantity option | |
| MaxQuantity: Quantity option | |
| } | |
| type EffectiveWindow = | |
| { | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| } | |
| type RoutingCommon = | |
| { | |
| Id: RoutingId | |
| Name: string | |
| RoutingType: RoutingType | |
| ResourceGroupId: ResourceGroupId option | |
| Cost: float option | |
| LotSizing: RoutingLotSizing | |
| Effective: EffectiveWindow | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| [<JsonFSharpConverter>] | |
| type RoutingSpecific = | |
| | PurchaseSpec of OutputProductId: ProductId * OutputStockingPointId: StockingPointId | |
| | TransportSpec of | |
| InputProductId: ProductId * | |
| InputStockingPointId: StockingPointId * | |
| OutputProductId: ProductId * | |
| OutputStockingPointId: StockingPointId * | |
| ConversionRate: Quantity option | |
| | WorkSpec of OutputProductId: ProductId * OutputStockingPointId: StockingPointId * ConversionRate: float option | |
| type RoutingStep = | |
| { | |
| RoutingStepId: RoutingStepId | |
| ExternalSequenceNr: string option | |
| Yield: float | |
| ResourceGroupId: ResourceGroupId option | |
| } | |
| type RoutingInput = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ConversionRate: float option | |
| } | |
| type RoutingOutput = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ConversionRate: float option | |
| IsCoProduct: bool | |
| } | |
| type StepCampaignRule = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| CampaignTypeId: CampaignTypeId | |
| IsAllowed: bool | |
| } | |
| type StepResourceMap = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| StandardResourceId: StandardResourceId | |
| ResourceGroupId: ResourceGroupId option | |
| IsAllowed: bool | |
| BottleneckProtection: TimeSpan option | |
| CoolingTime: TimeSpan option | |
| PreprocessingTime: TimeSpan option | |
| ProductionCapacityPerUnit: TimeSpan option | |
| ProductionTime: TimeSpan option | |
| SchedulingSpace: TimeSpan option | |
| TransportTime: TimeSpan option | |
| } | |
| type Routing = | |
| { | |
| Common: RoutingCommon | |
| Specific: RoutingSpecific | |
| Steps: RoutingStep list | |
| Inputs: RoutingInput list | |
| Outputs: RoutingOutput list | |
| CampaignRules: StepCampaignRule list | |
| ResourceMaps: StepResourceMap list | |
| } | |
| type RoutingPreference = | |
| { | |
| PreferredRoutingIds: RoutingId list // ordered by preference | |
| AlternativeRoutingIds: RoutingId list | |
| ValidFrom: DateTimeOffset option | |
| ValidTo: DateTimeOffset option | |
| } | |
| // Commands | |
| type DefineRoutingCmd = | |
| { | |
| Id: RoutingId | |
| Name: string | |
| RoutingType: RoutingType | |
| ResourceGroupId: ResourceGroupId option | |
| Cost: float option | |
| LotSizing: RoutingLotSizing | |
| Effective: EffectiveWindow | |
| Specific: RoutingSpecific | |
| Steps: RoutingStep list | |
| Inputs: RoutingInput list | |
| Outputs: RoutingOutput list | |
| } | |
| type AddRoutingStepCmd = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| ExternalSequenceNr: string option | |
| Yield: float | |
| ResourceGroupId: ResourceGroupId option | |
| } | |
| type AddRoutingInputCmd = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ConversionRate: float option | |
| } | |
| type AddRoutingOutputCmd = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| ProductId: ProductId | |
| StockingPointId: StockingPointId | |
| ConversionRate: float option | |
| IsCoProduct: bool | |
| } | |
| type SetLotSizingCmd = | |
| { | |
| RoutingId: RoutingId | |
| LotSizing: RoutingLotSizing | |
| } | |
| type SetCostCmd = | |
| { | |
| RoutingId: RoutingId | |
| Cost: float option | |
| } | |
| type MapStepToResourceCmd = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| Map: StepResourceMap | |
| } | |
| type SetStepCampaignRuleCmd = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| Rule: StepCampaignRule | |
| } | |
| type ActivateRoutingCmd = { RoutingId: RoutingId } | |
| type DeactivateRoutingCmd = { RoutingId: RoutingId } | |
| type ChangeEffectiveWindowCmd = | |
| { | |
| RoutingId: RoutingId | |
| Effective: EffectiveWindow | |
| } | |
| type RenameRoutingCmd = | |
| { | |
| RoutingId: RoutingId | |
| NewName: string | |
| } | |
| type RoutingCommand = | |
| | DefineRouting of DefineRoutingCmd | |
| | AddRoutingStep of AddRoutingStepCmd | |
| | AddRoutingInput of AddRoutingInputCmd | |
| | AddRoutingOutput of AddRoutingOutputCmd | |
| | SetLotSizing of SetLotSizingCmd | |
| | SetCost of SetCostCmd | |
| | MapStepToResource of MapStepToResourceCmd | |
| | SetStepCampaignRule of SetStepCampaignRuleCmd | |
| | ActivateRouting of ActivateRoutingCmd | |
| | DeactivateRouting of DeactivateRoutingCmd | |
| | ChangeEffectiveWindow of ChangeEffectiveWindowCmd | |
| | RenameRouting of RenameRoutingCmd | |
| // Events | |
| type RoutingDefinedEvt = | |
| { | |
| Common: RoutingCommon | |
| Specific: RoutingSpecific | |
| } | |
| type RoutingStepAddedEvt = | |
| { | |
| RoutingId: RoutingId | |
| Step: RoutingStep | |
| } | |
| type RoutingInputAddedEvt = | |
| { | |
| RoutingId: RoutingId | |
| Input: RoutingInput | |
| } | |
| type RoutingOutputAddedEvt = | |
| { | |
| RoutingId: RoutingId | |
| Output: RoutingOutput | |
| } | |
| type RoutingLotSizingSetEvt = | |
| { | |
| RoutingId: RoutingId | |
| LotSizing: RoutingLotSizing | |
| } | |
| type RoutingCostSetEvt = | |
| { | |
| RoutingId: RoutingId | |
| Cost: float option | |
| } | |
| type RoutingStepMappedToResourceEvt = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| Map: StepResourceMap | |
| } | |
| type RoutingStepCampaignRuleSetEvt = | |
| { | |
| RoutingId: RoutingId | |
| RoutingStepId: RoutingStepId | |
| Rule: StepCampaignRule | |
| } | |
| type RoutingActivatedEvt = { RoutingId: RoutingId } | |
| type RoutingDeactivatedEvt = { RoutingId: RoutingId } | |
| type RoutingEffectiveWindowChangedEvt = | |
| { | |
| RoutingId: RoutingId | |
| Effective: EffectiveWindow | |
| } | |
| type RoutingRenamedEvt = | |
| { | |
| RoutingId: RoutingId | |
| NewName: string | |
| } | |
| type RoutingEvent = | |
| | RoutingDefined of RoutingDefinedEvt | |
| | RoutingStepAdded of RoutingStepAddedEvt | |
| | RoutingInputAdded of RoutingInputAddedEvt | |
| | RoutingOutputAdded of RoutingOutputAddedEvt | |
| | RoutingLotSizingSet of RoutingLotSizingSetEvt | |
| | RoutingCostSet of RoutingCostSetEvt | |
| | RoutingStepMappedToResource of RoutingStepMappedToResourceEvt | |
| | RoutingStepCampaignRuleSet of RoutingStepCampaignRuleSetEvt | |
| | RoutingActivated of RoutingActivatedEvt | |
| | RoutingDeactivated of RoutingDeactivatedEvt | |
| | RoutingEffectiveWindowChanged of RoutingEffectiveWindowChangedEvt | |
| | RoutingRenamed of RoutingRenamedEvt | |
| // Signatures | |
| type DecideRouting = Routing option -> RoutingCommand -> Result<RoutingEvent list, DomainError> | |
| type EvolveRouting = Medhavi.Domain.Evolve<Routing, RoutingEvent> | |
| // Validation functions (includes business rules) | |
| let validateDefine (cmd: DefineRoutingCmd) : Result<unit, DomainError> = | |
| result { | |
| let! _ = required "Routing name" cmd.Name | |
| return () | |
| } | |
| // State evolution functions (pure state transitions) | |
| let applyDefined (evt: RoutingDefinedEvt) : Routing = | |
| { | |
| Common = evt.Common | |
| Specific = evt.Specific | |
| Steps = [] | |
| Inputs = [] | |
| Outputs = [] | |
| CampaignRules = [] | |
| ResourceMaps = [] | |
| } | |
| let applyStepAdded (evt: RoutingStepAddedEvt) (state: Routing) : Routing = | |
| { state with | |
| Steps = evt.Step :: state.Steps | |
| } | |
| let applyInputAdded (evt: RoutingInputAddedEvt) (state: Routing) : Routing = | |
| { state with | |
| Inputs = evt.Input :: state.Inputs | |
| } | |
| let applyOutputAdded (evt: RoutingOutputAddedEvt) (state: Routing) : Routing = | |
| { state with | |
| Outputs = evt.Output :: state.Outputs | |
| } | |
| let evolve (state: Routing option) (event: RoutingEvent) : Routing option = | |
| match event, state with | |
| | RoutingDefined e, None -> Some(applyDefined e) | |
| | RoutingStepAdded e, Some s -> Some(applyStepAdded e s) | |
| | RoutingInputAdded e, Some s -> Some(applyInputAdded e s) | |
| | RoutingOutputAdded e, Some s -> Some(applyOutputAdded e s) | |
| | RoutingDefined _, Some _ -> state // Idempotent - routing already exists | |
| | _, None -> None // Can't apply updates to non-existent routing | |
| | _ -> 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.StockingPoint | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open System.Text.Json.Serialization | |
| open Medhavi.Domain.Validation | |
| [<JsonFSharpConverter>] | |
| type StockingPointType = | |
| | Plant | |
| | DistributionCenter | |
| | Warehouse | |
| type StockingPoint = | |
| { | |
| Id: StockingPointId | |
| PlantId: PlantId | |
| Code: string | |
| Name: string | |
| Type: StockingPointType | |
| Location: string option | |
| Level: int option | |
| PlanningLevel: int option | |
| SupplyCanBeSplit: bool | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineStockingPointCmd = | |
| { | |
| Id: string | |
| PlantId: PlantId | |
| Code: string | |
| Name: string | |
| Type: StockingPointType | |
| Location: string option | |
| Level: int option | |
| PlanningLevel: int option | |
| SupplyCanBeSplit: bool | |
| } | |
| type RenameStockingPointCmd = | |
| { Id: StockingPointId; NewName: string } | |
| type RetireStockingPointCmd = | |
| { | |
| Id: StockingPointId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type StockingPointCommand = | |
| | DefineStockingPoint of DefineStockingPointCmd | |
| | RenameStockingPoint of RenameStockingPointCmd | |
| | RetireStockingPoint of RetireStockingPointCmd | |
| // Events | |
| type StockingPointDefinedEvt = | |
| { | |
| Id: StockingPointId | |
| PlantId: PlantId | |
| Code: string | |
| Name: string | |
| Type: StockingPointType | |
| Location: string option | |
| Level: int option | |
| PlanningLevel: int option | |
| SupplyCanBeSplit: bool | |
| Created: DateTimeOffset | |
| } | |
| type StockingPointRenamedEvt = | |
| { | |
| Id: StockingPointId | |
| NewName: string | |
| Modified: DateTimeOffset | |
| } | |
| type StockingPointRetiredEvt = | |
| { | |
| Id: StockingPointId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type StockingPointEvent = | |
| | StockingPointDefined of StockingPointDefinedEvt | |
| | StockingPointRenamed of StockingPointRenamedEvt | |
| | StockingPointRetired of StockingPointRetiredEvt | |
| // Signatures | |
| type DecideStockingPoint = StockingPoint option -> StockingPointCommand -> Result<StockingPointEvent list, DomainError> | |
| type EvolveStockingPoint = Medhavi.Domain.Evolve<StockingPoint, StockingPointEvent> | |
| let validateDefine (cmd: DefineStockingPointCmd) : Result<unit, DomainError> = | |
| required "StockingPoint code" cmd.Code | |
| |> Result.bind (fun _ -> required "StockingPoint name" cmd.Name) | |
| |> Result.map (fun _ -> ()) | |
| let applyDefined (evt: StockingPointDefinedEvt) : StockingPoint = | |
| { | |
| Id = evt.Id | |
| PlantId = evt.PlantId | |
| Code = evt.Code | |
| Name = evt.Name | |
| Type = evt.Type | |
| Location = evt.Location | |
| Level = evt.Level | |
| PlanningLevel = evt.PlanningLevel | |
| SupplyCanBeSplit = evt.SupplyCanBeSplit | |
| Created = evt.Created | |
| Modified = evt.Created | |
| } | |
| let applyRenamed (evt: StockingPointRenamedEvt) (state: StockingPoint) : StockingPoint = | |
| { state with | |
| Name = evt.NewName | |
| Modified = evt.Modified | |
| } | |
| let applyRetired (evt: StockingPointRetiredEvt) (state: StockingPoint) : StockingPoint = | |
| { state with Modified = evt.RetiredAt } | |
| let evolve (state: StockingPoint option) (event: StockingPointEvent) : StockingPoint option = | |
| match event, state with | |
| | StockingPointDefined e, None -> Some(applyDefined e) | |
| | StockingPointRenamed e, Some s -> Some(applyRenamed e s) | |
| | StockingPointRetired e, Some s -> Some(applyRetired e s) | |
| | StockingPointRetired _, None -> None | |
| | _, 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
| module Medhavi.Domain.Transport | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open System.Text.Json.Serialization | |
| open Medhavi.Common | |
| /// Transport mode (Air, Road, Rail, Sea, etc.) | |
| [<JsonFSharpConverter>] | |
| type TransportMode = | |
| | Air | |
| | Road | |
| | Rail | |
| | Sea | |
| | Pipeline | |
| | Other of string | |
| /// Transport schedule pattern | |
| [<JsonFSharpConverter>] | |
| type TransportSchedule = | |
| | Daily | |
| | Weekly of int // day of week (0=Sunday, 6=Saturday) | |
| | Monthly of int // day of month | |
| | OnDemand | |
| | Custom of string | |
| /// Regulatory/hazmat constraint | |
| [<JsonFSharpConverter>] | |
| type TransportConstraint = | |
| | Hazmat | |
| | TemperatureControlled | |
| | Refrigerated | |
| | Fragile | |
| | Oversized | |
| | Regulatory of string | |
| | Custom of string | |
| [<JsonFSharpConverter>] | |
| type TransportLegPurpose = | |
| | Primary | |
| | Alternate | |
| | Emergency | |
| type TransportCostDetail = | |
| { | |
| FixedCost: PositiveDecimal // Base cost per leg (regardless of quantity) | |
| VariableCostPerUnit: PositiveDecimal option // Cost per unit (kg, m3, etc.) | |
| MinFillThreshold: float option // Minimum fill percentage (0.0-1.0) | |
| MinFillPenalty: PositiveDecimal option // Penalty if utilization < MinFillThreshold | |
| LanePreference: PositiveDecimal option // Preference multiplier (1.0 = neutral, <1.0 = preferred, >1.0 = penalized) | |
| } | |
| type TransportCalendarId = | TransportCalendarId of string | |
| type CapacityProfile = | |
| | StaticCapacity of PositiveDecimal | |
| | ByDate of Map<DateTime, PositiveDecimal> | |
| type TransportCalendar = | |
| { | |
| Id: TransportCalendarId | |
| Name: string | |
| TimeZoneId: string // IANA or Windows Id. | |
| Pattern: TransportSchedule | |
| PatternTimeOfDay: TimeSpan option // local time-of-day for departures | |
| ExceptionalDates: DateTimeOffset list | |
| AdditionalDeparturesLocal: DateTimeOffset list // local date-times | |
| WindowStartOffset: TimeSpan option | |
| WindowEndOffset: TimeSpan option | |
| CapacityProfile: CapacityProfile option | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Add TransportCalendar commands | |
| type DefineTransportCalendarCmd = | |
| { | |
| Id: TransportCalendarId | |
| Name: string | |
| TimeZoneId: string | |
| Pattern: TransportSchedule | |
| PatternTimeOfDay: TimeSpan option | |
| ExceptionalDates: DateTimeOffset list | |
| AdditionalDeparturesLocal: DateTimeOffset list | |
| WindowStartOffset: TimeSpan option | |
| WindowEndOffset: TimeSpan option | |
| CapacityProfile: CapacityProfile option | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| Created: DateTimeOffset | |
| } | |
| type UpdateTransportCalendarCmd = | |
| { | |
| Id: TransportCalendarId | |
| Name: string option | |
| TimeZoneId: string option | |
| // ... other optional fields | |
| Modified: DateTimeOffset | |
| } | |
| type TransportCalendarDefinedEvt = | |
| { | |
| Id: TransportCalendarId | |
| Name: string | |
| TimeZoneId: string | |
| Pattern: TransportSchedule | |
| PatternTimeOfDay: TimeSpan option | |
| ExceptionalDates: DateTimeOffset list | |
| AdditionalDeparturesLocal: DateTimeOffset list | |
| WindowStartOffset: TimeSpan option | |
| WindowEndOffset: TimeSpan option | |
| CapacityProfile: CapacityProfile option | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| Created: DateTimeOffset | |
| } | |
| type TransportCalendarUpdatedEvt = | |
| { | |
| Id: TransportCalendarId | |
| Name: string option | |
| TimeZoneId: string option | |
| // ... other optional fields | |
| Modified: DateTimeOffset | |
| } | |
| type TransportCalendarDeactivatedEvt = | |
| { | |
| Id: TransportCalendarId | |
| DeactivatedAt: DateTimeOffset | |
| } | |
| type TransportCalendarEvent = | |
| | CalendarDefined of TransportCalendarDefinedEvt | |
| | CalendarUpdated of TransportCalendarUpdatedEvt | |
| | CalendarDeactivated of TransportCalendarDeactivatedEvt | |
| // Add evolve function for TransportCalendar | |
| type EvolveTransportCalendar = Evolve<TransportCalendar, TransportCalendarEvent> | |
| let evolveTransportCalendar: EvolveTransportCalendar = | |
| fun state evt -> | |
| match state, evt with | |
| | None, CalendarDefined e -> | |
| { | |
| Id = e.Id | |
| Name = e.Name | |
| TimeZoneId = e.TimeZoneId | |
| Pattern = e.Pattern | |
| PatternTimeOfDay = e.PatternTimeOfDay | |
| ExceptionalDates = e.ExceptionalDates | |
| AdditionalDeparturesLocal = e.AdditionalDeparturesLocal | |
| WindowStartOffset = e.WindowStartOffset | |
| WindowEndOffset = e.WindowEndOffset | |
| CapacityProfile = e.CapacityProfile | |
| EffectiveStart = e.EffectiveStart | |
| EffectiveEnd = e.EffectiveEnd | |
| IsActive = true | |
| Created = e.Created | |
| Modified = e.Created | |
| } | |
| |> Some | |
| | Some s, CalendarUpdated e -> | |
| { s with | |
| Name = e.Name |> Option.defaultValue s.Name | |
| TimeZoneId = e.TimeZoneId |> Option.defaultValue s.TimeZoneId | |
| // ... update other fields | |
| Modified = e.Modified | |
| } | |
| |> Some | |
| | Some s, CalendarDeactivated e -> | |
| { s with | |
| IsActive = false | |
| Modified = e.DeactivatedAt | |
| } | |
| |> Some | |
| | _ -> failwith "Invalid state/event combination" | |
| /// Transport Leg aggregate | |
| /// Represents a scheduled transport leg with mode, schedule, capacity, cutoff, constraints, reliability, and CO2 | |
| type TransportLeg = | |
| { | |
| Id: TransportLegId | |
| Origin: StockingPointId | |
| Destination: StockingPointId | |
| Mode: TransportMode | |
| Purpose: TransportLegPurpose | |
| Schedule: TransportSchedule | |
| CalendarId: TransportCalendarId option | |
| LeadTime: TimeSpan // Duration of the transport leg | |
| Capacity: PositiveDecimal option // Capacity in weight/volume units | |
| CapacityUnit: UnitOfMeasureId option | |
| CostDetail: TransportCostDetail | |
| Cutoff: TimeSpan option // Cutoff time before departure | |
| Constraints: TransportConstraint list | |
| Reliability: Percent option // 0.0-1.0 reliability factor | |
| CO2PerUnit: PositiveDecimal option // CO2 emissions per unit (kg/kg or kg/m3) | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineTransportLegCmd = | |
| { | |
| Id: TransportLegId | |
| Origin: StockingPointId | |
| Destination: StockingPointId | |
| Mode: TransportMode | |
| Schedule: TransportSchedule | |
| LeadTime: TimeSpan // Duration of the transport leg | |
| Capacity: PositiveDecimal option | |
| CapacityUnit: UnitOfMeasureId option | |
| Cutoff: TimeSpan option | |
| Constraints: TransportConstraint list | |
| Reliability: Percent option | |
| CO2PerUnit: PositiveDecimal option | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| Created: DateTimeOffset | |
| } | |
| type UpdateTransportLegCmd = | |
| { | |
| Id: TransportLegId | |
| Mode: TransportMode option | |
| Schedule: TransportSchedule option | |
| LeadTime: TimeSpan option // Duration of the transport leg | |
| Capacity: decimal option | |
| CapacityUnit: UnitOfMeasureId option | |
| Cutoff: TimeSpan option | |
| Constraints: TransportConstraint list option | |
| Reliability: float option | |
| CO2PerUnit: decimal option | |
| EffectiveEnd: DateTimeOffset option | |
| Modified: DateTimeOffset | |
| } | |
| type DeactivateTransportLegCmd = | |
| { | |
| Id: TransportLegId | |
| DeactivatedAt: DateTimeOffset | |
| } | |
| type TransportLegCommand = | |
| | DefineTransportLeg of DefineTransportLegCmd | |
| | UpdateTransportLeg of UpdateTransportLegCmd | |
| | DeactivateTransportLeg of DeactivateTransportLegCmd | |
| // Events | |
| type TransportLegDefinedEvt = | |
| { | |
| Id: TransportLegId | |
| Origin: StockingPointId | |
| Destination: StockingPointId | |
| Mode: TransportMode | |
| Schedule: TransportSchedule | |
| LeadTime: TimeSpan // Duration of the transport leg | |
| Capacity: PositiveDecimal option | |
| CapacityUnit: UnitOfMeasureId option | |
| Cutoff: TimeSpan option | |
| Constraints: TransportConstraint list | |
| Reliability: Percent option | |
| CO2PerUnit: PositiveDecimal option | |
| EffectiveStart: DateTimeOffset | |
| EffectiveEnd: DateTimeOffset option | |
| Created: DateTimeOffset | |
| CostDetail: TransportCostDetail | |
| } | |
| type TransportLegUpdatedEvt = | |
| { | |
| Id: TransportLegId | |
| Mode: TransportMode option | |
| Schedule: TransportSchedule option | |
| LeadTime: TimeSpan option // Duration of the transport leg | |
| Capacity: PositiveDecimal option | |
| CapacityUnit: UnitOfMeasureId option | |
| Cutoff: TimeSpan option | |
| Constraints: TransportConstraint list option | |
| Reliability: Percent option | |
| CO2PerUnit: PositiveDecimal option | |
| EffectiveEnd: DateTimeOffset option | |
| Modified: DateTimeOffset | |
| Cost: TransportCostDetail | |
| } | |
| type TransportLegDeactivatedEvt = | |
| { | |
| Id: TransportLegId | |
| DeactivatedAt: DateTimeOffset | |
| } | |
| type TransportLegEvent = | |
| | TransportLegDefined of TransportLegDefinedEvt | |
| | TransportLegUpdated of TransportLegUpdatedEvt | |
| | TransportLegDeactivated of TransportLegDeactivatedEvt | |
| // Signatures | |
| type DecideTransportLeg = Decide<TransportLeg, TransportLegCommand, TransportLegEvent, DomainError> | |
| type EvolveTransportLeg = Evolve<TransportLeg, TransportLegEvent> | |
| let tryParseInt (s: string) = | |
| match System.Int32.TryParse s with | |
| | true, v -> Some v | |
| | false, _ -> None | |
| let parseTransportSchedule (input: string) : Result<TransportSchedule, string> = | |
| let s = input.Trim() | |
| match s.ToLowerInvariant() with | |
| | "daily" -> Ok Daily | |
| | "ondemand" | |
| | "on-demand" -> Ok OnDemand | |
| | _ when s.StartsWith("weekly:", StringComparison.OrdinalIgnoreCase) -> | |
| let value = s.Substring("weekly:".Length) | |
| match tryParseInt value with | |
| | Some d when d >= 0 && d <= 6 -> Ok(Weekly d) | |
| | _ -> Error "Weekly schedule requires day-of-week (0=Sunday .. 6=Saturday)" | |
| | _ when s.StartsWith("monthly:", StringComparison.OrdinalIgnoreCase) -> | |
| let value = s.Substring("monthly:".Length) | |
| match tryParseInt value with | |
| | Some d when d >= 1 && d <= 31 -> Ok(Monthly d) | |
| | _ -> Error "Monthly schedule requires day-of-month (1 .. 31)" | |
| | _ when s.StartsWith("custom:", StringComparison.OrdinalIgnoreCase) -> | |
| Ok(TransportSchedule.Custom(s.Substring("custom:".Length))) | |
| | _ -> Error $"Unknown transport schedule: '{input}'" | |
| let transportScheduleToString = | |
| function | |
| | Daily -> "Daily" | |
| | Weekly d -> $"Weekly:{d}" | |
| | Monthly d -> $"Monthly:{d}" | |
| | OnDemand -> "OnDemand" | |
| | TransportSchedule.Custom s -> $"Custom:{s}" | |
| let parseTransportConstraint (input: string) = | |
| match input.Trim().ToLowerInvariant() with | |
| | "hazmat" -> Ok Hazmat | |
| | "temperaturecontrolled" | |
| | "temperature_controlled" | |
| | "temperature-controlled" -> Ok TemperatureControlled | |
| | "refrigerated" -> Ok Refrigerated | |
| | "fragile" -> Ok Fragile | |
| | "oversized" -> Ok Oversized | |
| | s when s.StartsWith("regulatory:") -> Ok(Regulatory(s.Substring("regulatory:".Length))) | |
| | s when s.StartsWith("custom:") -> Ok(Custom(s.Substring("custom:".Length))) | |
| | _ -> Error $"Unknown transport constraint: '{input}'" | |
| let parseTransportMode (input: string) : Result<TransportMode, string> = | |
| match input.Trim().ToLowerInvariant() with | |
| | "air" -> Ok Air | |
| | "road" -> Ok Road | |
| | "rail" -> Ok Rail | |
| | "sea" -> Ok Sea | |
| | "pipeline" -> Ok Pipeline | |
| | "" -> Error "Transport mode cannot be empty" | |
| | s -> Ok(Other s) | |
| let validateDefine (cmd: DefineTransportLegCmd) : Result<unit, DomainError> = | |
| result { | |
| // Validate that From and To are different | |
| let! _ = | |
| if cmd.Origin = cmd.Destination then | |
| Error(DomainError.validation "Transport leg origin and destination must be different") | |
| else | |
| Ok() | |
| // Validate lead time | |
| let! _ = | |
| if cmd.LeadTime.TotalDays < 0 then | |
| Error(DomainError.validation "Lead time days must be non-negative") | |
| else | |
| Ok() | |
| // Validate capacity if provided | |
| let! _ = | |
| match cmd.Capacity with | |
| | Some cap when (PositiveDecimal.value cap) <= 0M -> | |
| Error(DomainError.validation "Capacity must be greater than zero if provided") | |
| | _ -> Ok() | |
| return () | |
| } | |
| // Implement the Evolve function | |
| let evolveTransportLeg: EvolveTransportLeg = | |
| fun state evt -> | |
| match state, evt with | |
| | None, TransportLegDefined e -> | |
| { | |
| Id = e.Id | |
| Origin = e.Origin | |
| Destination = e.Destination | |
| CalendarId = None | |
| Mode = e.Mode | |
| Purpose = TransportLegPurpose.Primary | |
| Schedule = e.Schedule | |
| LeadTime = e.LeadTime | |
| Capacity = e.Capacity | |
| CapacityUnit = e.CapacityUnit | |
| Cutoff = e.Cutoff | |
| CostDetail = e.CostDetail | |
| Constraints = e.Constraints | |
| Reliability = e.Reliability | |
| CO2PerUnit = e.CO2PerUnit | |
| EffectiveStart = e.EffectiveStart | |
| EffectiveEnd = e.EffectiveEnd | |
| IsActive = true | |
| Created = e.Created | |
| Modified = e.Created | |
| } | |
| | Some s, TransportLegUpdated e -> | |
| { s with | |
| Mode = e.Mode |> Option.defaultValue s.Mode | |
| Schedule = e.Schedule |> Option.defaultValue s.Schedule | |
| LeadTime = e.LeadTime |> Option.defaultValue s.LeadTime | |
| Capacity = e.Capacity | |
| CapacityUnit = e.CapacityUnit | |
| Cutoff = e.Cutoff | |
| Constraints = e.Constraints |> Option.defaultValue s.Constraints | |
| Reliability = e.Reliability | |
| CO2PerUnit = e.CO2PerUnit | |
| EffectiveEnd = e.EffectiveEnd | |
| CostDetail = e.Cost | |
| Modified = e.Modified | |
| } | |
| | Some s, TransportLegDeactivated e -> | |
| { s with | |
| IsActive = false | |
| Modified = e.DeactivatedAt | |
| } | |
| | _ -> failwith "Invalid state/event combination" | |
| |> Some |
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.UnitConversion | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain.UnitOfMeasure | |
| open Medhavi.Domain | |
| // ================================================================================================= | |
| // UNIT CONVERSION AGGREGATE DOMAIN MODEL | |
| // ================================================================================================= | |
| // Core UnitConversion aggregate - defines conversion relationships between units | |
| type UnitConversion = | |
| { | |
| Id: UnitConversionId | |
| ProductId: ProductId option | |
| FromUoM: UnitOfMeasureId | |
| ToUoM: UnitOfMeasureId | |
| Ratio: float // multiply source by Ratio to get target | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineUnitConversionCmd = | |
| { | |
| Id: string | |
| ProductId: ProductId option | |
| FromUoM: UnitOfMeasureId | |
| ToUoM: UnitOfMeasureId | |
| Ratio: float | |
| IsActive: bool | |
| } | |
| type UpdateUnitConversionCmd = | |
| { | |
| Id: UnitConversionId | |
| Ratio: float | |
| Modified: DateTimeOffset | |
| } | |
| type ActivateUnitConversionCmd = | |
| { | |
| Id: UnitConversionId | |
| Modified: DateTimeOffset | |
| } | |
| type DeactivateUnitConversionCmd = | |
| { | |
| Id: UnitConversionId | |
| Modified: DateTimeOffset | |
| } | |
| // ================================================================================================= | |
| // UNIT CONVERSION COMMANDS | |
| // ================================================================================================= | |
| type UnitConversionCommand = | |
| | DefineUnitConversion of DefineUnitConversionCmd | |
| | UpdateUnitConversion of UpdateUnitConversionCmd | |
| | ActivateUnitConversion of ActivateUnitConversionCmd | |
| | DeactivateUnitConversion of DeactivateUnitConversionCmd | |
| // Events | |
| type UnitConversionDefinedEvt = | |
| { | |
| Id: UnitConversionId | |
| ProductId: ProductId option | |
| FromUoM: UnitOfMeasureId | |
| ToUoM: UnitOfMeasureId | |
| Ratio: float | |
| IsActive: bool | |
| Created: DateTimeOffset | |
| } | |
| type UnitConversionUpdatedEvt = | |
| { | |
| Id: UnitConversionId | |
| Ratio: float | |
| Modified: DateTimeOffset | |
| } | |
| type UnitConversionActivatedEvt = | |
| { | |
| Id: UnitConversionId | |
| Modified: DateTimeOffset | |
| } | |
| type UnitConversionDeactivatedEvt = | |
| { | |
| Id: UnitConversionId | |
| Modified: DateTimeOffset | |
| } | |
| // ================================================================================================= | |
| // UNIT CONVERSION EVENTS | |
| // ================================================================================================= | |
| type UnitConversionEvent = | |
| | UnitConversionDefined of UnitConversionDefinedEvt | |
| | UnitConversionUpdated of UnitConversionUpdatedEvt | |
| | UnitConversionActivated of UnitConversionActivatedEvt | |
| | UnitConversionDeactivated of UnitConversionDeactivatedEvt | |
| // ================================================================================================= | |
| // DOMAIN LOGIC SIGNATURES | |
| // ================================================================================================= | |
| // Decision function signature | |
| type DecideUnitConversion = | |
| UnitConversion option -> UnitConversionCommand -> Result<UnitConversionEvent list, DomainError> | |
| // Evolution function signature | |
| type EvolveUnitConversion = Medhavi.Domain.Evolve<UnitConversion, UnitConversionEvent> | |
| // Validation functions (includes business rules) | |
| let validateDefine (cmd: DefineUnitConversionCmd) : Result<unit, DomainError> = | |
| // Basic input validation | |
| if cmd.Ratio <= 0.0 then | |
| Error(DomainError.validation "Conversion ratio must be positive") | |
| elif cmd.FromUoM = cmd.ToUoM then | |
| Error(DomainError.validation "Cannot convert unit to itself") | |
| else | |
| Ok() | |
| let validateUpdate (cmd: UpdateUnitConversionCmd) : Result<unit, DomainError> = | |
| if cmd.Ratio <= 0.0 then | |
| Error(DomainError.validation "Conversion ratio must be positive") | |
| else | |
| Ok() | |
| let validateActivate (_cmd: ActivateUnitConversionCmd) : Result<unit, DomainError> = | |
| // Activation is always allowed | |
| Ok() | |
| let validateDeactivate (_cmd: DeactivateUnitConversionCmd) : Result<unit, DomainError> = | |
| // Deactivation is always allowed | |
| Ok() | |
| // State evolution functions (pure state transitions) | |
| let applyDefined (evt: UnitConversionDefinedEvt) : UnitConversion = | |
| { | |
| Id = evt.Id | |
| ProductId = evt.ProductId | |
| FromUoM = evt.FromUoM | |
| ToUoM = evt.ToUoM | |
| Ratio = evt.Ratio | |
| IsActive = evt.IsActive | |
| Created = evt.Created | |
| Modified = evt.Created | |
| } | |
| let applyUpdated (evt: UnitConversionUpdatedEvt) (state: UnitConversion) : UnitConversion = | |
| { state with | |
| Ratio = evt.Ratio | |
| Modified = evt.Modified | |
| } | |
| let applyActivated (evt: UnitConversionActivatedEvt) (state: UnitConversion) : UnitConversion = | |
| { state with | |
| IsActive = true | |
| Modified = evt.Modified | |
| } | |
| let applyDeactivated (evt: UnitConversionDeactivatedEvt) (state: UnitConversion) : UnitConversion = | |
| { state with | |
| IsActive = false | |
| Modified = evt.Modified | |
| } | |
| let evolve (state: UnitConversion option) (event: UnitConversionEvent) : UnitConversion option = | |
| match event, state with | |
| | UnitConversionDefined e, None -> Some(applyDefined e) | |
| | UnitConversionUpdated e, Some s -> Some(applyUpdated e s) | |
| | UnitConversionActivated e, Some s -> Some(applyActivated e s) | |
| | UnitConversionDeactivated e, Some s -> Some(applyDeactivated e s) | |
| | UnitConversionDefined _, Some _ -> state // Idempotent - conversion already exists | |
| | _, None -> None // Can't apply updates to non-existent conversion |
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.UnitOfMeasure | |
| open System | |
| open Medhavi.Domain.Ids | |
| open Medhavi.Domain | |
| open Medhavi.Domain.Validation | |
| open Medhavi.Common.ResultCE | |
| type UnitOfMeasure = | |
| { | |
| Id: UnitOfMeasureId | |
| Code: string | |
| Name: string | |
| IsBase: bool | |
| ToBaseFactor: decimal // multiplicative factor to a chosen base UoM | |
| Created: DateTimeOffset | |
| Modified: DateTimeOffset | |
| } | |
| // Commands | |
| type DefineUnitOfMeasureCmd = | |
| { | |
| Code: string | |
| Name: string | |
| IsBase: bool | |
| ToBaseFactor: decimal | |
| } | |
| type ChangeConversionFactorCmd = | |
| { | |
| Id: UnitOfMeasureId | |
| NewFactor: decimal | |
| } | |
| type MarkAsBaseUnitCmd = { Id: UnitOfMeasureId } | |
| type RetireUnitOfMeasureCmd = { Id: UnitOfMeasureId } | |
| type UnitOfMeasureCommand = | |
| | Define of DefineUnitOfMeasureCmd | |
| | ChangeConversionFactor of ChangeConversionFactorCmd | |
| | MarkAsBaseUnit of MarkAsBaseUnitCmd | |
| | Retire of RetireUnitOfMeasureCmd | |
| // Events | |
| type UnitOfMeasureDefinedEvt = | |
| { | |
| Id: UnitOfMeasureId | |
| Code: string | |
| Name: string | |
| IsBase: bool | |
| ToBaseFactor: decimal | |
| Created: DateTimeOffset | |
| } | |
| type ConversionFactorChangedEvt = | |
| { | |
| Id: UnitOfMeasureId | |
| NewFactor: decimal | |
| Modified: DateTimeOffset | |
| } | |
| type BaseUnitDesignatedEvt = | |
| { | |
| Id: UnitOfMeasureId | |
| Modified: DateTimeOffset | |
| } | |
| type UnitOfMeasureRetiredEvt = | |
| { | |
| Id: UnitOfMeasureId | |
| RetiredAt: DateTimeOffset | |
| } | |
| type UnitOfMeasureEvent = | |
| | UnitOfMeasureDefined of UnitOfMeasureDefinedEvt | |
| | ConversionFactorChanged of ConversionFactorChangedEvt | |
| | BaseUnitDesignated of BaseUnitDesignatedEvt | |
| | UnitOfMeasureRetired of UnitOfMeasureRetiredEvt | |
| // Signatures | |
| type DecideUnitOfMeasure = UnitOfMeasure option -> UnitOfMeasureCommand -> Result<UnitOfMeasureEvent list, DomainError> | |
| type EvolveUnitOfMeasure = Medhavi.Domain.Evolve<UnitOfMeasure, UnitOfMeasureEvent> | |
| // Validation functions (includes business rules) | |
| let validateDefine (cmd: DefineUnitOfMeasureCmd) : Result<unit, DomainError> = | |
| // Basic input validation (delegates to domain) | |
| if String.IsNullOrWhiteSpace cmd.Code then | |
| Error(DomainError.validation "Unit code cannot be empty") | |
| elif String.IsNullOrWhiteSpace cmd.Name then | |
| Error(DomainError.validation "Unit name cannot be empty") | |
| elif cmd.ToBaseFactor <= 0m then | |
| Error(DomainError.validation "Conversion factor must be positive") | |
| else | |
| Ok() | |
| let validateChangeFactor (cmd: ChangeConversionFactorCmd) : Result<unit, DomainError> = | |
| if cmd.NewFactor <= 0m then | |
| Error(DomainError.validation "Conversion factor must be positive") | |
| else | |
| Ok() | |
| let validateMarkAsBase (_cmd: MarkAsBaseUnitCmd) : Result<unit, DomainError> = | |
| // Additional validation could check if another base unit exists | |
| // This would require access to all units, so validation happens at application layer | |
| Ok() | |
| let validateRetire (_cmd: RetireUnitOfMeasureCmd) : Result<unit, DomainError> = | |
| // Additional validation could check if unit is currently used in products/BOMs | |
| // This requires data access, so validation happens at application layer | |
| Ok() | |
| // State evolution functions (pure state transitions) | |
| let applyDefined (evt: UnitOfMeasureDefinedEvt) : UnitOfMeasure = | |
| { | |
| Id = evt.Id | |
| Code = evt.Code | |
| Name = evt.Name | |
| IsBase = evt.IsBase | |
| ToBaseFactor = evt.ToBaseFactor | |
| Created = evt.Created | |
| Modified = evt.Created | |
| } | |
| let applyConversionFactorChanged (evt: ConversionFactorChangedEvt) (state: UnitOfMeasure) : UnitOfMeasure = | |
| { state with | |
| ToBaseFactor = evt.NewFactor | |
| Modified = evt.Modified | |
| } | |
| let applyBaseUnitDesignated (evt: BaseUnitDesignatedEvt) (state: UnitOfMeasure) : UnitOfMeasure = | |
| { state with | |
| IsBase = true | |
| Modified = evt.Modified | |
| } | |
| let applyRetired (evt: UnitOfMeasureRetiredEvt) (state: UnitOfMeasure) : UnitOfMeasure option = | |
| // Units are soft-deleted, but we return None to indicate they're no longer active | |
| None | |
| let evolve (state: UnitOfMeasure option) (event: UnitOfMeasureEvent) : UnitOfMeasure option = | |
| match event, state with | |
| | UnitOfMeasureDefined e, None -> Some(applyDefined e) | |
| | ConversionFactorChanged e, Some s -> Some(applyConversionFactorChanged e s) | |
| | BaseUnitDesignated e, Some s -> Some(applyBaseUnitDesignated e s) | |
| | UnitOfMeasureRetired e, Some s -> applyRetired e s | |
| | UnitOfMeasureRetired _, None -> None | |
| | _, current -> current |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment