Skip to content

Instantly share code, notes, and snippets.

@achal7
Created February 16, 2026 21:16
Show Gist options
  • Select an option

  • Save achal7/8060d1a62cb67e2cc59dacdcd8b12129 to your computer and use it in GitHub Desktop.

Select an option

Save achal7/8060d1a62cb67e2cc59dacdcd8b12129 to your computer and use it in GitHub Desktop.
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
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
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
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
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
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
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
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
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
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