Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

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