Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save achal7/45a6639ec61b7664dd78bfb3d6cd6515 to your computer and use it in GitHub Desktop.
module Medhavi.Domain.Validation
open System
open Medhavi.Domain
/// Reusable validation helpers for command/input checks
let required (field: string) (value: string) =
if String.IsNullOrWhiteSpace value then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, $"{field} is required", Map.empty))
else
Ok(value.Trim())
let nonNegativeDecimal (field: string) (value: decimal) =
if value < 0m then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, $"{field} must be >= 0", Map.empty))
else
Ok value
let positiveDecimal (field: string) (value: decimal) =
if value <= 0m then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, $"{field} must be > 0", Map.empty))
else
Ok value
let inRange (field: string) (minValue: decimal) (maxValue: decimal) (value: decimal) =
if value < minValue || value > maxValue then
Error(
DomainError.Validation(
DomainErrorCodes.ValidationFailed,
$"{field} must be between {minValue} and {maxValue}",
Map.empty
)
)
else
Ok value
let dateNotPast (field: string) (now: DateTimeOffset) (value: DateTimeOffset) =
if value < now then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, $"{field} must not be in the past", Map.empty))
else
Ok value
let dateRange (startField: string) (endField: string) (startDate: DateTimeOffset) (endDate: DateTimeOffset) =
if endDate < startDate then
Error(
DomainError.Validation(
DomainErrorCodes.ValidationFailed,
$"{endField} must be greater than or equal to {startField}",
Map.empty
)
)
else
Ok(startDate, endDate)
let nonNegativeInt (field: string) (value: int) =
if value < 0 then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, $"{field} must be >= 0", Map.empty))
else
Ok value
let positiveInt (field: string) (value: int) =
if value <= 0 then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, $"{field} must be > 0", Map.empty))
else
Ok value
namespace Medhavi.Domain.Constraints
open System.Text.Json.Serialization
open System
open Medhavi.Domain.Ids
open Medhavi.Domain
[<JsonFSharpConverter>]
type ConstraintScope =
| Material
| Capacity
| Routing
| Campaign
| DataQuality
| Scheduling
| Promise
[<JsonFSharpConverter>]
type ConstraintSeverity =
| Problem
| Warning
| Info
[<JsonFSharpConverter>]
type ConstraintCode =
| MissingTransportLane
| OverCapacity
| PlanningLate
| CampaignViolation
| ResourceEligibilityOverride
| RoutingMismatch
| DataQualityIssue
| ToleranceBreach
| MissingCalendar
| Other of string
[<JsonFSharpConverter>]
type PolicyDecision =
| Allow
| AllowWithViolation of ConstraintCode * ConstraintSeverity
| Reject of ConstraintCode * ConstraintSeverity
[<JsonFSharpConverter>]
type ConstraintActor =
| User of string
| Optimizer
| Integration of string
| System
type ConstraintContext =
{
Actor: ConstraintActor
SourceAggregateType: string
SourceAggregateId: string
Code: ConstraintCode option
Scope: ConstraintScope option
Data: Map<string, string>
}
/// Generic policy contract to be applied by all actors (UI, optimizer, integration, system).
type ConstraintPolicy = ConstraintContext -> PolicyDecision
type ConstraintStatus =
| Active
| Resolved
type ConstraintViolation =
{
Id: ConstraintViolationId
SourceAggregateType: string
SourceAggregateId: string
Code: ConstraintCode
Severity: ConstraintSeverity
Scope: ConstraintScope
Message: string
ObservedAt: DateTimeOffset
ResolvedAt: DateTimeOffset option
ResolvedBy: string option
Status: ConstraintStatus
Data: Map<string, string>
}
// Commands
type RecordConstraintViolationCmd =
{
Id: string
SourceAggregateType: string
SourceAggregateId: string
Code: ConstraintCode
Severity: ConstraintSeverity
Scope: ConstraintScope
Message: string
ObservedAt: DateTimeOffset
Data: Map<string, string>
}
type ResolveConstraintViolationCmd =
{
Id: ConstraintViolationId
ResolvedAt: DateTimeOffset
ResolvedBy: string option
}
// Events
type ConstraintViolationRecordedEvt =
{
Id: ConstraintViolationId
SourceAggregateType: string
SourceAggregateId: string
Code: ConstraintCode
Severity: ConstraintSeverity
Scope: ConstraintScope
Message: string
ObservedAt: DateTimeOffset
Data: Map<string, string>
}
type ConstraintViolationResolvedEvt =
{
Id: ConstraintViolationId
ResolvedAt: DateTimeOffset
ResolvedBy: string option
}
type ConstraintViolationEvent =
| ConstraintViolationRecorded of ConstraintViolationRecordedEvt
| ConstraintViolationResolved of ConstraintViolationResolvedEvt
type ConstraintViolationCommand =
| RecordConstraintViolation of RecordConstraintViolationCmd
| ResolveConstraintViolation of ResolveConstraintViolationCmd
// Signatures
type DecideConstraintViolation =
ConstraintViolation option -> ConstraintViolationCommand -> Result<ConstraintViolationEvent list, DomainError>
type EvolveConstraintViolation = ConstraintViolation option -> ConstraintViolationEvent -> ConstraintViolation
module Medhavi.Domain.CostFx
open System
/// Cost breakdown for promise/plan decisions.
type CostBreakdown =
{
Material: decimal option
Production: decimal option
Transport: decimal option
Holding: decimal option
Other: decimal option
}
/// FX rate result with as-of timestamp.
type FxRate =
{
From: string
To: string
Rate: decimal
AsOf: DateTimeOffset
}
let normalizeCost (fx: FxRate option) (amount: decimal option) =
match fx, amount with
| None, a -> a
| Some r, Some a -> Some(a * r.Rate)
| _, None -> None
/// Guard against stale rates (returns None if stale beyond maxAge).
let guardStale (maxAge: TimeSpan) (rate: FxRate option) =
match rate with
| None -> None
| Some r when DateTimeOffset.UtcNow - r.AsOf > maxAge -> None
| Some r -> Some r
/// Apply FX normalization with stale-rate guard; returns None if no valid rate.
let applyFx (maxAge: TimeSpan) (fxRate: FxRate option) (amount: decimal option) =
amount |> normalizeCost (guardStale maxAge fxRate)
namespace Medhavi.Domain
open System
open System.Text.Json.Serialization
[<JsonFSharpConverter>]
type Money = { Amount: decimal; Currency: string }
[<JsonFSharpConverter>]
type Quantity =
private
| Quantity of decimal
// Required for Seq.sum, List.sum, etc.
static member Zero = Quantity 0m
static member (+)(Quantity a, Quantity b) = Quantity(a + b)
static member (-)(Quantity a, Quantity b) = Quantity(a - b)
static member (*)(Quantity v, scalar: decimal) = Quantity(v * scalar)
static member (*)(scalar: decimal, Quantity v) = Quantity(scalar * v)
static member (/)(Quantity v, scalar: decimal) = Quantity(v / scalar)
// Comparison operators
static member op_LessThan(Quantity a, Quantity b) = a < b
static member op_LessThanOrEqual(Quantity a, Quantity b) = a <= b
static member op_GreaterThan(Quantity a, Quantity b) = a > b
static member op_GreaterThanOrEqual(Quantity a, Quantity b) = a >= b
module Quantity =
let create (value: decimal) : Result<Quantity, DomainError> =
if value < 0m then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, "Value must be non-negative", Map.empty))
else
Ok(Quantity value)
let value (Quantity v) = v
let zero = Quantity 0m
// Utility functions
let isZero (Quantity v) = v = 0m
let isPositive (Quantity v) = v > 0m
let minOf (Quantity a) (Quantity b) = Quantity(min a b)
let maxOf (Quantity a) (Quantity b) = Quantity(max a b)
/// Safe subtraction - clamps to zero if result would be negative
let subtract (Quantity a) (Quantity b) = Quantity(max 0m (a - b))
/// Try subtract - returns Error if result would be negative
let trySubtract (Quantity a) (Quantity b) : Result<Quantity, DomainError> =
if a >= b then
Ok(Quantity(a - b))
else
Error(
DomainError.Validation(
DomainErrorCodes.ValidationFailed,
"Subtraction would result in negative quantity",
Map.empty
)
)
/// Ratio between two quantities (a / b)
let ratio (Quantity a) (Quantity b) : decimal = a / b
/// Scale by a factor
let scale (factor: decimal) (Quantity v) = Quantity(v * factor)
/// Sum a sequence of quantities
let sum (quantities: Quantity seq) = quantities |> Seq.fold (+) zero
/// Positive decimal (>= 0m)
[<Struct>]
type PositiveDecimal =
private
| PositiveDecimal of decimal
static member create(value: decimal) =
if value < 0m then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, "Value must be non-negative", Map.empty))
else
Ok(PositiveDecimal value)
static member value(PositiveDecimal v) = v
/// Required by SRTP (List.sum / sumBy)
static member Zero = PositiveDecimal 0m
/// Arithmetic
static member (+)(PositiveDecimal a, PositiveDecimal b) = PositiveDecimal(a + b)
static member (-)(PositiveDecimal a, PositiveDecimal b) =
if a - b < 0m then
failwith "PositiveDecimal subtraction underflow"
else
PositiveDecimal(a - b)
static member (*)(PositiveDecimal v, scalar: decimal) = PositiveDecimal(v * scalar)
static member (*)(scalar: decimal, PositiveDecimal v) = PositiveDecimal(scalar * v)
static member (/)(PositiveDecimal v, scalar: decimal) = PositiveDecimal(v / scalar)
// Comparison operators
static member op_LessThan(PositiveDecimal a, PositiveDecimal b) = a < b
static member op_LessThanOrEqual(PositiveDecimal a, PositiveDecimal b) = a <= b
static member op_GreaterThan(PositiveDecimal a, PositiveDecimal b) = a > b
static member op_GreaterThanOrEqual(PositiveDecimal a, PositiveDecimal b) = a >= b
static member op_Equality(PositiveDecimal a, PositiveDecimal b) = a = b
static member op_Inequality(PositiveDecimal a, PositiveDecimal b) = a <> b
static member op_Multiply(PositiveDecimal a, PositiveDecimal b) = PositiveDecimal(a * b)
/// Percent in the range [0.0, 1.0]
type Percent = private | Percent of decimal
module Percent =
let create (value: decimal) =
if value < 0m || value > 1m then
Error(
DomainError.Validation(
DomainErrorCodes.ValidationFailed,
"Percent must be between 0.0 and 1.0",
Map.empty
)
)
else
Ok(Percent value)
let value (Percent v) = v
type Window =
{
Start: DateTimeOffset
End: DateTimeOffset
}
module Window =
let overlaps (a: Window) (b: Window) = a.Start < b.End && b.Start < a.End
let contains (outer: Window) (inner: Window) =
outer.Start <= inner.Start
&& outer.End >= inner.End
let applySlack (slack: TimeSpan) (w: Window) = { Start = w.Start; End = w.End + slack }
let isAfter (t: DateTimeOffset) (w: Window) = t <= w.Start
/// Validate cutoff: departure must be >= earliest and before cutoff end.
let meetsCutoff (earliest: DateTimeOffset) (cutoffEnd: DateTimeOffset) (departure: DateTimeOffset) =
departure >= earliest && departure <= cutoffEnd
let start (window: Window) = window.Start
let endTime (window: Window) = window.End
let duration (window: Window) = window.End - window.Start
/// Shared decide/evolve function signatures for event-sourced aggregates.
type Decide<'state, 'cmd, 'evt, 'err> = 'state option -> 'cmd -> Result<'evt list, 'err>
type Evolve<'state, 'evt> = 'state option -> 'evt -> 'state option
namespace Medhavi.Domain
open System
open System.Text.Json.Serialization
/// Domain error codes for programmatic error handling
module DomainErrorCodes =
let ValidationFailed = "DOMAIN_VALIDATION_FAILED"
let NotFound = "DOMAIN_NOT_FOUND"
let Conflict = "DOMAIN_CONFLICT"
let InvariantViolation = "DOMAIN_INVARIANT_VIOLATION"
[<JsonFSharpConverter>]
type DomainError =
| Validation of code: string * message: string * data: Map<string, obj>
| NotFound of code: string * message: string * data: Map<string, obj>
| Conflict of code: string * message: string * data: Map<string, obj> // e.g., code uniqueness, base unit already exists
| Invariant of code: string * message: string * data: Map<string, obj> // e.g., invariants violated
/// Human-readable error message
member this.Message =
match this with
| Validation(_, msg, _)
| NotFound(_, msg, _)
| Conflict(_, msg, _)
| Invariant(_, msg, _) -> msg
/// Machine-readable error code
member this.Code =
match this with
| Validation(code, _, _)
| NotFound(code, _, _)
| Conflict(code, _, _)
| Invariant(code, _, _) -> code
/// Optional contextual data for debugging and logging
member this.Data =
match this with
| Validation(_, _, data)
| NotFound(_, _, data)
| Conflict(_, _, data)
| Invariant(_, _, data) -> data
/// Create a validation error
static member validation message = Validation(DomainErrorCodes.ValidationFailed, message, Map.empty)
/// Create a validation error with contextual data
static member validationWith message (data: Map<string, obj>) =
Validation(DomainErrorCodes.ValidationFailed, message, data)
/// Create a not found error
static member notFound message = NotFound(DomainErrorCodes.NotFound, message, Map.empty)
/// Create a not found error with contextual data
static member notFoundWith message (data: Map<string, obj>) = NotFound(DomainErrorCodes.NotFound, message, data)
/// Create a conflict error
static member conflict message = Conflict(DomainErrorCodes.Conflict, message, Map.empty)
/// Create a conflict error with contextual data
static member conflictWith message (data: Map<string, obj>) = Conflict(DomainErrorCodes.Conflict, message, data)
/// Create an invariant violation error
static member invariant message = Invariant(DomainErrorCodes.InvariantViolation, message, Map.empty)
/// Create an invariant violation error with contextual data
static member invariantWith message (data: Map<string, obj>) =
Invariant(DomainErrorCodes.InvariantViolation, message, data)
/// Active patterns and utilities for DomainError
[<RequireQualifiedAccess>]
module DomainError =
/// Pattern match on validation errors
let (|Validation|_|) =
function
| Validation(_, msg, _) -> Some msg
| _ -> None
/// Pattern match on not found errors
let (|NotFound|_|) =
function
| NotFound(_, msg, _) -> Some msg
| _ -> None
/// Pattern match on conflict errors
let (|Conflict|_|) =
function
| Conflict(_, msg, _) -> Some msg
| _ -> None
/// Pattern match on invariant errors
let (|Invariant|_|) =
function
| Invariant(_, msg, _) -> Some msg
| _ -> None
(*
Examples of using DomainError:
// 1. Creating errors using static factory methods
let validationError = DomainError.validation "ProductId cannot be empty"
let notFoundError = DomainError.notFound "Product not found"
let conflictError = DomainError.conflict "Product code already exists"
let invariantError = DomainError.invariant "Cannot cancel a completed order"
// 2. Creating errors with contextual data for debugging
let validationWithData =
DomainError.validationWith
"Quantity must be positive"
(Map.ofList [ ("Field", box "Quantity"); ("Value", box -5) ])
let notFoundWithData =
DomainError.notFoundWith
"Product not found"
(Map.ofList [ ("ProductId", box "PROD-123"); ("StockingPointId", box "SP-456") ])
// 3. Pattern matching using active patterns
let handleError (error: DomainError) =
match error with
| DomainError.Validation msg -> printfn "Validation error: %s" msg
| DomainError.NotFound msg -> printfn "Not found: %s" msg
| DomainError.Conflict msg -> printfn "Conflict: %s" msg
| DomainError.Invariant msg -> printfn "Invariant violation: %s" msg
// 4. Pattern matching with error code and data extraction
let handleErrorWithDetails (error: DomainError) =
match error with
| DomainError.Validation msg ->
printfn "Validation error [%s]: %s" error.Code msg
printfn "Context: %A" error.Data
| DomainError.NotFound msg ->
printfn "Not found [%s]: %s" error.Code msg
printfn "Context: %A" error.Data
| DomainError.Conflict msg ->
printfn "Conflict [%s]: %s" error.Code msg
printfn "Context: %A" error.Data
| DomainError.Invariant msg ->
printfn "Invariant violation [%s]: %s" error.Code msg
printfn "Context: %A" error.Data
// 5. Using in Result types
let validateProductId (productId: string) : Result<string, DomainError> =
if System.String.IsNullOrWhiteSpace(productId) then
Error(DomainError.validation "ProductId cannot be empty")
else
Ok productId
let findProduct (productId: string) : Result<Product, DomainError> =
match tryFindProduct productId with
| Some product -> Ok product
| None ->
Error(
DomainError.notFoundWith
$"Product {productId} not found"
(Map.ofList [ ("ProductId", box productId) ])
)
// 6. Pattern matching in Result handling
let processResult (result: Result<Product, DomainError>) =
match result with
| Ok product -> printfn "Success: %A" product
| Error(DomainError.Validation msg) -> printfn "Validation failed: %s" msg
| Error(DomainError.NotFound msg) -> printfn "Not found: %s" msg
| Error(DomainError.Conflict msg) -> printfn "Conflict: %s" msg
| Error(DomainError.Invariant msg) -> printfn "Invariant violation: %s" msg
// 7. Error code-based handling (programmatic)
let handleByCode (error: DomainError) =
match error.Code with
| DomainErrorCodes.ValidationFailed -> "Handle validation error"
| DomainErrorCodes.NotFound -> "Handle not found error"
| DomainErrorCodes.Conflict -> "Handle conflict error"
| DomainErrorCodes.InvariantViolation -> "Handle invariant violation"
| _ -> "Unknown error"
// 8. Extracting error information
let errorInfo (error: DomainError) =
{
Code = error.Code
Message = error.Message
Data = error.Data
}
// 9. Combining multiple validation errors (using Result.mapError)
let validateOrder (order: Order) : Result<Order, DomainError> =
validateProductId order.ProductId
|> Result.bind (fun _ -> validateQuantity order.Quantity)
|> Result.mapError (fun err ->
match err with
| DomainError.Validation msg -> DomainError.validationWith msg (Map.ofList [ ("OrderId", box order.Id) ])
| _ -> err
)
// 10. Pattern matching in async/asyncResult workflows
let asyncProcessOrder (orderId: string) =
async {
match! findOrder orderId with
| Ok order -> return Ok order
| Error(DomainError.NotFound msg) ->
return Error(DomainError.notFoundWith $"Order {orderId} not found" (Map.ofList [ ("OrderId", box orderId) ]))
| Error err -> return Error err
}
*)
module Medhavi.Domain.IdsFactory
open System
open System.Security.Cryptography
open System.Text
open Medhavi.Domain
open Medhavi.Domain.Ids
/// External system identifier (e.g., from ERP, MES, etc.)
type ExternalSystemId =
{
SystemName: string // e.g., "ERP", "MES", "SAP"
ExternalId: string // The ID from the external system
}
/// ID generation strategy
type IdGenerationStrategy =
| Deterministic of ExternalSystemId // Use external system ID to generate deterministic internal ID
| Random // Generate random ID (for internal-only entities)
| Explicit of string // Use explicitly provided ID
/// Common ID factory for creating deterministic IDs from external system identifiers
/// This ensures idempotent ingestion: same external ID always produces same internal ID
/// Normalize external system name (lowercase, trimmed)
let private normalizeSystemName (name: string) : string = name.Trim().ToLowerInvariant().Replace(" ", "_")
/// Normalize external ID (trimmed, but preserve case for uniqueness)
let private normalizeExternalId (id: string) : string = id.Trim()
/// Create a deterministic internal ID from external system identifier
/// Uses SHA256 hash to ensure same external ID always produces same internal ID
/// Format: "{systemName}:{hash}" where hash is first 16 chars of SHA256
let createDeterministicId (externalSystemId: ExternalSystemId) (aggregateType: string) : string =
let normalizedSystem = normalizeSystemName externalSystemId.SystemName
let normalizedExternalId = normalizeExternalId externalSystemId.ExternalId
let combined = $"{normalizedSystem}:{aggregateType}:{normalizedExternalId}"
// Use SHA256 to create deterministic hash
use sha256 = SHA256.Create()
let hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined))
let hashString =
BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant()
// Use first 16 characters of hash + last 4 of original ID for readability
let shortHash = hashString.Substring(0, 16)
let idSuffix =
if normalizedExternalId.Length >= 4 then
normalizedExternalId.Substring(Math.Max(0, normalizedExternalId.Length - 4))
else
normalizedExternalId
$"{normalizedSystem}-{aggregateType}-{shortHash}-{idSuffix}"
/// Create deterministic ProductId from external system identifier
let createProductId (strategy: IdGenerationStrategy) : Result<ProductId, DomainError> =
match strategy with
| Deterministic extId ->
let deterministicId = createDeterministicId extId "product"
ProductId.create deterministicId
| Random ->
let randomId = Guid.NewGuid().ToString("N")
ProductId.create randomId
| Explicit id -> ProductId.create id
/// Create deterministic SupplyOrderId from external system identifier
let createSupplyOrderId (strategy: IdGenerationStrategy) : Result<SupplyOrderId, DomainError> =
match strategy with
| Deterministic extId ->
let deterministicId = createDeterministicId extId "supplyorder"
SupplyOrderId.create deterministicId
| Random ->
let randomId = Guid.NewGuid().ToString("N")
SupplyOrderId.create randomId
| Explicit id -> SupplyOrderId.create id
/// Create deterministic CustomerOrderId from external system identifier
let createCustomerOrderId (strategy: IdGenerationStrategy) : Result<CustomerOrderId, DomainError> =
match strategy with
| Deterministic extId ->
let deterministicId = createDeterministicId extId "customerorder"
CustomerOrderId.create deterministicId
| Random ->
let randomId = Guid.NewGuid().ToString("N")
CustomerOrderId.create randomId
| Explicit id -> CustomerOrderId.create id
/// Create deterministic StockingPointId from external system identifier
let createStockingPointId (strategy: IdGenerationStrategy) : Result<StockingPointId, DomainError> =
match strategy with
| Deterministic extId ->
let deterministicId = createDeterministicId extId "stockingpoint"
StockingPointId.create deterministicId
| Random ->
let randomId = Guid.NewGuid().ToString("N")
StockingPointId.create randomId
| Explicit id -> StockingPointId.create id
/// Create deterministic RoutingId from external system identifier
let createRoutingId (strategy: IdGenerationStrategy) : Result<RoutingId, DomainError> =
match strategy with
| Deterministic extId ->
let deterministicId = createDeterministicId extId "routing"
RoutingId.create deterministicId
| Random ->
let randomId = Guid.NewGuid().ToString("N")
RoutingId.create randomId
| Explicit id -> RoutingId.create id
namespace Medhavi.Domain.Ids
open System
open System.Text.Json.Serialization
open System.Security.Cryptography
open System.Text
open Medhavi.Domain
/// Generic helper to validate non-empty, trimmed identifiers.
module private IdHelpers =
let createId (ctor: string -> 'id) (name: string) (value: string) =
if String.IsNullOrWhiteSpace value then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, $"{name} must not be empty", Map.empty))
else
Ok(ctor value)
/// Deterministic ID helpers for idempotency (pure, side-effect free).
module DeterministicIds =
let private hashParts (parts: string list) =
use sha = SHA256.Create()
let payload = String.concat "|" parts |> Encoding.UTF8.GetBytes
let bytes = sha.ComputeHash payload
bytes
|> Array.take 12
|> Array.fold (fun acc b -> acc + b.ToString("x2")) ""
/// Build a deterministic ID for reservations (material/capacity/transport) given a natural key.
let reservationId (scope: string) (reference: string) (windowStart: DateTimeOffset) (windowEnd: DateTimeOffset) =
hashParts [ "resv"; scope; reference; windowStart.ToString("O"); windowEnd.ToString("O") ]
/// Deterministic itinerary/transport option ID (origin/destination + legs signature).
let itineraryId (origin: string) (destination: string) (legsSignature: string) =
hashParts [ "itin"; origin; destination; legsSignature ]
/// Deterministic capacity allocation/assignment ID.
let allocationId (resourceRef: string) (windowStart: DateTimeOffset) (windowEnd: DateTimeOffset) =
hashParts [ "alloc"; resourceRef; windowStart.ToString("O"); windowEnd.ToString("O") ]
/// Deterministic peg (demand↔supply) ID.
let pegId (demandId: string) (supplyId: string) = hashParts [ "peg"; demandId; supplyId ]
/// Deterministic proposal/recommendation ID (e.g., supply order proposals).
let proposalId (proposalType: string) (anchorId: string) (timestamp: DateTimeOffset) =
hashParts [ "proposal"; proposalType; anchorId; timestamp.ToString("O") ]
/// Plant identifier
[<JsonFSharpConverter>]
type PlantId = private | PlantId of string
module PlantId =
let create = IdHelpers.createId PlantId "PlantId"
let value (PlantId v) = v
/// Stocking point identifier
[<JsonFSharpConverter>]
type StockingPointId = private | StockingPointId of string
module StockingPointId =
let create = IdHelpers.createId StockingPointId "StockingPointId"
let value (StockingPointId v) = v
/// Product identifier
[<JsonFSharpConverter>]
type ProductId = private | ProductId of string
module ProductId =
let create = IdHelpers.createId ProductId "ProductId"
let value (ProductId v) = v
/// Product substitution rule identifier
[<JsonFSharpConverter>]
type ProductSubstitutionRuleId = private | ProductSubstitutionRuleId of string
module ProductSubstitutionRuleId =
let create =
IdHelpers.createId ProductSubstitutionRuleId "ProductSubstitutionRuleId"
let value (ProductSubstitutionRuleId v) = v
/// Routing identifiers
[<JsonFSharpConverter>]
type RoutingId = private | RoutingId of string
module RoutingId =
let create = IdHelpers.createId RoutingId "RoutingId"
let value (RoutingId v) = v
[<JsonFSharpConverter>]
type RoutingStepId = private | RoutingStepId of string
module RoutingStepId =
let create = IdHelpers.createId RoutingStepId "RoutingStepId"
let value (RoutingStepId v) = v
[<JsonFSharpConverter>]
type TransportLaneId = private | TransportLaneId of string
module TransportLaneId =
let create = IdHelpers.createId TransportLaneId "TransportLaneId"
let value (TransportLaneId v) = v
[<JsonFSharpConverter>]
type TransportLegId = private | TransportLegId of string
module TransportLegId =
let create = IdHelpers.createId TransportLegId "TransportLegId"
let value (TransportLegId v) = v
/// Resource identifiers
[<JsonFSharpConverter>]
type ResourceGroupId = private | ResourceGroupId of string
module ResourceGroupId =
let create = IdHelpers.createId ResourceGroupId "ResourceGroupId"
let value (ResourceGroupId v) = v
[<JsonFSharpConverter>]
type StandardResourceId = private | StandardResourceId of string
module StandardResourceId =
let create = IdHelpers.createId StandardResourceId "StandardResourceId"
let value (StandardResourceId v) = v
[<JsonFSharpConverter>]
type PhysicalResourceId = private | PhysicalResourceId of string
module PhysicalResourceId =
let create = IdHelpers.createId PhysicalResourceId "PhysicalResourceId"
let value (PhysicalResourceId v) = v
[<JsonFSharpConverter>]
type CombinedResourceId = private | CombinedResourceId of string
module CombinedResourceId =
let create = IdHelpers.createId CombinedResourceId "CombinedResourceId"
let value (CombinedResourceId v) = v
/// Calendar identifier
[<JsonFSharpConverter>]
type CalendarId = private | CalendarId of string
module CalendarId =
let create = IdHelpers.createId CalendarId "CalendarId"
let value (CalendarId v) = v
/// Resource period identifiers
[<JsonFSharpConverter>]
type ResourceGroupCapacityId = private | ResourceGroupCapacityId of string
module ResourceGroupCapacityId =
let create = IdHelpers.createId ResourceGroupCapacityId "ResourceGroupCapacityId"
let value (ResourceGroupCapacityId v) = v
[<JsonFSharpConverter>]
type StandardResourcePeriodId = private | StandardResourcePeriodId of string
module StandardResourcePeriodId =
let create = IdHelpers.createId StandardResourcePeriodId "StandardResourcePeriodId"
let value (StandardResourcePeriodId v) = v
[<JsonFSharpConverter>]
type PhysicalResourcePeriodId = private | PhysicalResourcePeriodId of string
module PhysicalResourcePeriodId =
let create = IdHelpers.createId PhysicalResourcePeriodId "PhysicalResourcePeriodId"
let value (PhysicalResourcePeriodId v) = v
[<JsonFSharpConverter>]
type CombinedResourcePeriodId = private | CombinedResourcePeriodId of string
module CombinedResourcePeriodId =
let create = IdHelpers.createId CombinedResourcePeriodId "CombinedResourcePeriodId"
let value (CombinedResourcePeriodId v) = v
/// Campaign identifiers
[<JsonFSharpConverter>]
type CampaignTypeId = private | CampaignTypeId of string
module CampaignTypeId =
let create = IdHelpers.createId CampaignTypeId "CampaignTypeId"
let value (CampaignTypeId v) = v
[<JsonFSharpConverter>]
type CampaignId = private | CampaignId of string
module CampaignId =
let create = IdHelpers.createId CampaignId "CampaignId"
let value (CampaignId v) = v
[<JsonFSharpConverter>]
type CampaignTypePeriodId = private | CampaignTypePeriodId of string
module CampaignTypePeriodId =
let create = IdHelpers.createId CampaignTypePeriodId "CampaignTypePeriodId"
let value (CampaignTypePeriodId v) = v
[<JsonFSharpConverter>]
type CampaignTypeAssignmentId = private | CampaignTypeAssignmentId of string
module CampaignTypeAssignmentId =
let create = IdHelpers.createId CampaignTypeAssignmentId "CampaignTypeAssignmentId"
let value (CampaignTypeAssignmentId v) = v
/// Assignment identifiers
[<JsonFSharpConverter>]
type PeriodAssignmentId = private | PeriodAssignmentId of string
module PeriodAssignmentId =
let create = IdHelpers.createId PeriodAssignmentId "PeriodAssignmentId"
let value (PeriodAssignmentId v) = v
[<JsonFSharpConverter>]
type ResourceAssignmentId = private | ResourceAssignmentId of string
module ResourceAssignmentId =
let create = IdHelpers.createId ResourceAssignmentId "ResourceAssignmentId"
let value (ResourceAssignmentId v) = v
/// Capacity hold identifiers
[<JsonFSharpConverter>]
type CapacityAllocationId = private | CapacityAllocationId of string
module CapacityAllocationId =
let create = IdHelpers.createId CapacityAllocationId "CapacityAllocationId"
let value (CapacityAllocationId v) = v
[<JsonFSharpConverter>]
type CapacityReservationId = private | CapacityReservationId of string
module CapacityReservationId =
let create = IdHelpers.createId CapacityReservationId "CapacityReservationId"
let value (CapacityReservationId v) = v
/// Order identifiers
[<JsonFSharpConverter>]
type CustomerOrderId = private | CustomerOrderId of string
module CustomerOrderId =
let create = IdHelpers.createId CustomerOrderId "CustomerOrderId"
let value (CustomerOrderId v) = v
/// Order line identifiers
[<JsonFSharpConverter>]
type OrderLineId = private | OrderLineId of string
module OrderLineId =
let create = IdHelpers.createId OrderLineId "OrderLineId"
let value (OrderLineId v) = v
/// Customer identifier
[<JsonFSharpConverter>]
type CustomerId = private | CustomerId of string
module CustomerId =
let create = IdHelpers.createId CustomerId "CustomerId"
let value (CustomerId v) = v
[<JsonFSharpConverter>]
type SupplyOrderId = private | SupplyOrderId of string
module SupplyOrderId =
let create = IdHelpers.createId SupplyOrderId "SupplyOrderId"
let value (SupplyOrderId v) = v
/// Inventory identifier (used for projections/links)
[<JsonFSharpConverter>]
type InventoryId = private | InventoryId of string
module InventoryId =
let create = IdHelpers.createId InventoryId "InventoryId"
let value (InventoryId v) = v
[<JsonFSharpConverter>]
type InventoryTargetId = private | InventoryTargetId of string
module InventoryTargetId =
let create = IdHelpers.createId InventoryTargetId "InventoryTargetId"
let value (InventoryTargetId v) = v
[<JsonFSharpConverter>]
type PlanUnitId = private | PlanUnitId of string
module PlanUnitId =
let create = IdHelpers.createId PlanUnitId "PlanUnitId"
let value (PlanUnitId v) = v
[<JsonFSharpConverter>]
type OperationId = private | OperationId of string
module OperationId =
let create = IdHelpers.createId OperationId "OperationId"
let value (OperationId v) = v
/// BOM / material identifiers
[<JsonFSharpConverter>]
type BomId = private | BomId of string
module BomId =
let create = IdHelpers.createId BomId "BomId"
let value (BomId v) = v
[<JsonFSharpConverter>]
type MaterialRequirementId = private | MaterialRequirementId of string
module MaterialRequirementId =
let create = IdHelpers.createId MaterialRequirementId "MaterialRequirementId"
let value (MaterialRequirementId v) = v
[<JsonFSharpConverter>]
type MaterialReservationId = private | MaterialReservationId of string
module MaterialReservationId =
let create = IdHelpers.createId MaterialReservationId "MaterialReservationId"
let value (MaterialReservationId v) = v
[<JsonFSharpConverter>]
type PurchaseOrderRecommendationId = private | PurchaseOrderRecommendationId of string
module PurchaseOrderRecommendationId =
let create =
IdHelpers.createId PurchaseOrderRecommendationId "PurchaseOrderRecommendationId"
let value (PurchaseOrderRecommendationId v) = v
//. Unit of measure identifier
[<JsonFSharpConverter>]
type UnitOfMeasureId = private | UnitOfMeasureId of string
module UnitOfMeasureId =
let create (value: string) : Result<UnitOfMeasureId, DomainError> =
if String.IsNullOrWhiteSpace value then
Error(
DomainError.Validation(DomainErrorCodes.ValidationFailed, "UnitOfMeasureId cannot be empty", Map.empty)
)
else
Ok(UnitOfMeasureId value)
let value (UnitOfMeasureId v) = v
/// Supplier identifier
[<JsonFSharpConverter>]
type SupplierId = private | SupplierId of string
module SupplierId =
let create (value: string) : Result<SupplierId, DomainError> =
if String.IsNullOrWhiteSpace value then
Error(DomainError.Validation(DomainErrorCodes.ValidationFailed, "SupplierId cannot be empty", Map.empty))
else
Ok(SupplierId value)
let value (SupplierId v) = v
[<JsonFSharpConverter>]
type SupplierOfferId = private | SupplierOfferId of string
module SupplierOfferId =
let create = IdHelpers.createId SupplierOfferId "SupplierOfferId"
let value (SupplierOfferId v) = v
/// Unit conversion identifier
[<JsonFSharpConverter>]
type UnitConversionId = private | UnitConversionId of string
module UnitConversionId =
let create = IdHelpers.createId UnitConversionId "UnitConversionId"
let value (UnitConversionId v) = v
/// Optimization identifiers
[<JsonFSharpConverter>]
type CombiId = private | CombiId of string
module CombiId =
let create = IdHelpers.createId CombiId "CombiId"
let value (CombiId v) = v
[<JsonFSharpConverter>]
type LeadTimeCalculationId = private | LeadTimeCalculationId of string
module LeadTimeCalculationId =
let create = IdHelpers.createId LeadTimeCalculationId "LeadTimeCalculationId"
let value (LeadTimeCalculationId v) = v
/// Constraint violation id
[<JsonFSharpConverter>]
type ConstraintViolationId = private | ConstraintViolationId of string
module ConstraintViolationId =
let create = IdHelpers.createId ConstraintViolationId "ConstraintViolationId"
let value (ConstraintViolationId v) = v
namespace Medhavi.Domain.TelemetryContracts
open System
/// Standard telemetry contracts for providers/orchestrator.
type KpiEvent =
| PromiseEvaluated of promiseId: string * decision: string * limiter: string option * latencyMs: int
| ProviderLatency of name: string * latencyMs: int
| CacheHitMiss of cache: string * hit: bool
| ProviderError of name: string * error: string
| AtRiskDetected of promiseId: string * reason: string
| CapacityUtilization of resource: string * usedMs: int * availableMs: int
| CapacityAllocationLatency of step: string * latencyMs: int
| CapacityBottleneckDetected of resource: string * step: string
| CapacityChurn of cycleId: string * allocationsChanged: int
| CapacityLockAdherence of resource: string * reservedMs: int * usedMs: int
type TelemetryProvider =
{
RecordKpi: KpiEvent -> unit
RecordError: exn * string -> unit
RecordLatency: string * TimeSpan -> unit
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment