Created
May 12, 2026 16:57
-
-
Save achal7/c2a076f3a5cc2e1ea74ddbf11f84c210 to your computer and use it in GitHub Desktop.
SupplyChainMockup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ConstraintAbstraction we needValues inside structureFunctorIndependent contextual valuesApplicativeDependent contextual stepsMonadEffectful functionsKleisli categoryCombining valuesMonoidTransforming between contextsNatural transformationConsuming valuesContravariant functorInput-output adaptationProfunctorRecursive interpretationF-algebra / catamorphismWorkflow descriptionFree structure / DSL | |
| 1. What are the input and output types? | |
| 2. Can this step fail? | |
| 3. Are checks independent or dependent? | |
| 4. Do we want error accumulation or fail-fast? | |
| 5. Does order matter? | |
| 6. What operation do we need? | |
| 7. Which lawful abstraction fits? | |
| Type + operation + laws = lawful structure | |
| Business use case | |
| ↓ | |
| Identify domain states | |
| ↓ | |
| Represent states as types | |
| ↓ | |
| Identify valid transitions | |
| ↓ | |
| Represent transitions as functions | |
| ↓ | |
| Ask whether each transition is plain, optional, failing, accumulating, async, etc. | |
| ↓ | |
| Choose abstraction: | |
| - plain function | |
| - Option | |
| - Validation | |
| - Result | |
| - Async<Result<_,_>> | |
| ↓ | |
| Use laws to ensure operations preserve meaning | |
| ↓ | |
| Compose small lawful parts into larger workflows | |
| > We represented domain states as types, business transitions as arrows, and used lawful abstractions like Applicative, Monad, Semigroup, Kleisli composition, and natural transformation-like conversion to compose the workflow safely. | |
| *** | |
| # 5. The Bigger Picture | |
| Think of category theory as a **language of composition**. | |
| Our implementation used that language to answer: | |
| | Design question | Category-theoretic idea | | |
| | -------------------------------------------- | -------------------------------------- | | |
| | What domain states exist? | Objects | | |
| | What transitions are valid? | Morphisms | | |
| | Can steps compose directly? | Composition | | |
| | What if a step can fail? | Result / Monad | | |
| | What if validations are independent? | Applicative | | |
| | How do we collect errors? | Semigroup / Monoid | | |
| | How do we compose failing functions? | Kleisli category | | |
| | How do we move from validation to execution? | Natural transformation-like conversion | | |
| | What must remain true? | Laws | | |
| *** | |
| ```text | |
| We are not inventing category theory. | |
| We are discovering category-theoretic structures inside our domain model. | |
| ``` | |
| Once you start seeing this, the rest of the concepts become much easier: | |
| * Functor = preserving structure while mapping | |
| * Applicative = combining independent contextual values | |
| * Monad = sequencing dependent contextual computations | |
| * Kleisli category = composing effectful domain transitions | |
| * Natural transformation = moving between contexts consistently | |
| * Isomorphism = lossless representation change | |
| * Monoid = lawful combination | |
| ```text | |
| Product/coproduct types define valid states. | |
| Morphisms define valid transitions. | |
| Composition defines valid workflows. | |
| ``` | |
| ### Common Contravariant Structures | |
| Validator<A> = A -> bool | |
| Predicate<A> = A -> bool | |
| Printer<A> = A -> string | |
| Encoder<A> = A -> Json | |
| Comparer<A> = A -> A -> int | |
| Equality<A> = A -> A -> bool | |
| Consumer<A> = A -> unit | |
| Sink<A> = A -> Effect | |
| Consumer<A> = contravariant | |
| Producer<A> = covariant | |
| Transformer<A,B> = profunctor-like | |
| ```text | |
| Contravariant = adapt reusable consumers/checks | |
| Applicative = combine independent validation results | |
| Monad = sequence dependent business steps | |
| ``` | |
| ```text | |
| Contravariant Rules | |
| ↓ | |
| Applicative Validation | |
| ↓ | |
| Natural transformation to Result | |
| ↓ | |
| Monad / Kleisli execution workflow | |
| ``` | |
| A Profunctor represents something that consumes an input and produces an output. | |
| Therefore, we can adapt both sides: | |
| * adapt the input backward, | |
| * adapt the output forward. | |
| ```text | |
| dimap : ('a2 -> 'a) -> ('b -> 'b2) -> P<'a, 'b> -> P<'a2, 'b2> | |
| let dimap before after p = before >> p >> after | |
| ``` | |
| ```text | |
| So use lenses when: | |
| * the update is structurally safe, | |
| * the field replacement is meaningful, | |
| * you need composable nested updates, | |
| * you are working with DTOs/configs/view models, | |
| * invariants are already protected by types. | |
| Avoid raw lenses when: | |
| * arbitrary field setting can break invariants, | |
| * domain operation needs validation, | |
| * update has business rules. | |
| ``` | |
| ```text | |
| Independent field checks → Applicative Validation | |
| Dependent domain transitions → Result Monad / Kleisli | |
| External calls → Async<Result<_,_>> | |
| DTO mapping → Functor/Bifunctor/Profunctor | |
| State transitions → Typed arrows | |
| Aggregation → Monoid/Fold | |
| ``` | |
| Algebraic API = capabilities as functions | |
| Free DSL = capabilities as data | |
| Interpreter = implementation of capabilities |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # **Category Theory with F#: From Composition to Supply Chain Domain Modeling** | |
| *** | |
| # Guiding Learning Method | |
| Every chapter will follow this pattern: | |
| ```text | |
| 1. Start with a concrete F# / domain problem | |
| 2. Discover the recurring composition pattern | |
| 3. Derive the abstraction | |
| 4. Introduce formal category theory definition | |
| 5. Study equations and laws | |
| 6. Encode the idea in F# | |
| 7. Apply it to domain modeling | |
| 8. Connect it to supply chain implementation | |
| ``` | |
| So we will not learn like this: | |
| ```text | |
| Definition → Theorem → Example | |
| ``` | |
| Instead, we will learn like this: | |
| ```text | |
| Problem → Need → Pattern → Abstraction → Definition → Laws → F# → Domain Design | |
| ``` | |
| This is the best fit for your preferred **constructive / derivational learning approach**. | |
| *** | |
| # Course Outline | |
| ## Part 0 — Orientation | |
| ### 1. Why Category Theory for F# Developers? | |
| Purpose: | |
| * Why category theory matters for software design | |
| * Why composition is the central theme | |
| * Why F# is a good host language | |
| * How category theory helps domain modeling | |
| * How the constructive learning approach will work | |
| *** | |
| # Part 1 — Composition Foundations | |
| ### 2. Functions, Composition, and Identity | |
| We begin from ordinary F# functions. | |
| Concepts: | |
| * Function composition | |
| * Identity | |
| * Associativity | |
| * Pipelines | |
| * Information flow | |
| Formal ideas: | |
| ```text | |
| id_A : A → A | |
| g ∘ f : A → C | |
| ``` | |
| *** | |
| ### 3. Categories | |
| Concepts: | |
| * Objects | |
| * Morphisms | |
| * Identity morphisms | |
| * Composition | |
| * Category laws | |
| F# intuition: | |
| ```text | |
| Types ≈ Objects | |
| Functions ≈ Morphisms | |
| ``` | |
| *** | |
| ### 4. Reasoning with Laws | |
| Concepts: | |
| * Equational reasoning | |
| * Lawful abstractions | |
| * Refactoring safely | |
| * Laws as design contracts | |
| Main question: | |
| > What must remain true after transformation? | |
| *** | |
| # Part 2 — Data Shape and Structure | |
| ### 5. Product Types, Coproduct Types, and Algebraic Data Types | |
| Concepts: | |
| * Records | |
| * Tuples | |
| * Discriminated unions | |
| * AND types | |
| * OR types | |
| * Domain modeling with shape | |
| Formal intuition: | |
| ```text | |
| Product : A × B | |
| Coproduct : A + B | |
| ``` | |
| *** | |
| ### 6. Initial and Terminal Objects | |
| Concepts: | |
| * Terminal object | |
| * Initial object | |
| * Unit | |
| * Impossible values | |
| * Unique morphisms | |
| Formal ideas: | |
| ```text | |
| Terminal object: for every A, exactly one morphism A → 1 | |
| Initial object : for every A, exactly one morphism 0 → A | |
| ``` | |
| *** | |
| # Part 3 — Structure-Preserving Mappings | |
| ### 7. Functors | |
| Concepts: | |
| * Mapping inside structure | |
| * Preserving shape | |
| * `map` | |
| * Containers and contexts | |
| Laws: | |
| ```text | |
| F(id) = id | |
| F(g ∘ f) = F(g) ∘ F(f) | |
| ``` | |
| F# examples: | |
| * `Option.map` | |
| * `List.map` | |
| * `Result.map` | |
| *** | |
| ### 8. Bifunctors and Higher-Kinded Patterns in F\# | |
| Concepts: | |
| * Mapping over two type parameters | |
| * Working around lack of native higher-kinded types | |
| * Error/value transformations | |
| F# examples: | |
| * `Result<'error, 'value>` | |
| * Pair-like structures | |
| * Domain outcomes | |
| *** | |
| ### 9. Natural Transformations | |
| Concepts: | |
| * Transforming one context into another | |
| * Preserving meaning across structures | |
| Formal idea: | |
| ```text | |
| η : F ⇒ G | |
| ``` | |
| Naturality law: | |
| ```text | |
| G f ∘ η_A = η_B ∘ F f | |
| ``` | |
| *** | |
| # Part 4 — Combining and Sequencing Computations | |
| ### 10. Semigroups and Monoids | |
| Concepts: | |
| * Combining values | |
| * Associativity | |
| * Identity | |
| * Aggregation | |
| * Folding | |
| Laws: | |
| ```text | |
| (a <> b) <> c = a <> (b <> c) | |
| empty <> a = a | |
| a <> empty = a | |
| ``` | |
| Supply chain examples: | |
| * Inventory quantity aggregation | |
| * Error accumulation | |
| * Demand aggregation | |
| *** | |
| ### 11. Applicative Functors | |
| Concepts: | |
| * Independent computations in context | |
| * Validation | |
| * Error accumulation | |
| * Building values from validated parts | |
| Operations: | |
| ```text | |
| pure : A → F A | |
| (<*>) : F (A → B) → F A → F B | |
| ``` | |
| *** | |
| ### 12. Monads | |
| Concepts: | |
| * Dependent sequencing | |
| * Context-aware workflows | |
| * `bind` | |
| * `return` | |
| Operations: | |
| ```text | |
| return : A → M A | |
| bind : M A → (A → M B) → M B | |
| ``` | |
| Monad laws: | |
| ```text | |
| return a >>= f = f a | |
| m >>= return = m | |
| (m >>= f) >>= g = m >>= (fun x -> f x >>= g) | |
| ``` | |
| *** | |
| ### 13. Kleisli Categories | |
| Concepts: | |
| * Composing effectful functions | |
| * Functions of shape: | |
| ```text | |
| A → M B | |
| ``` | |
| Used for: | |
| * Fail-fast pipelines | |
| * Service orchestration | |
| * Domain workflows | |
| * Order processing | |
| *** | |
| ### 14. Computation Expressions | |
| Concepts: | |
| * F# syntax for contextual computation | |
| * `Bind` | |
| * `Return` | |
| * `ReturnFrom` | |
| * `and!` | |
| * `MergeSources` | |
| This chapter connects category theory to idiomatic F#. | |
| *** | |
| # Part 5 — Direction, Consumption, and Transformation | |
| ### 15. Contravariant Functors | |
| Concepts: | |
| * Consumers | |
| * Validators | |
| * Comparers | |
| * Printers | |
| * Encoders | |
| Operation: | |
| ```text | |
| contramap : (B → A) → F A → F B | |
| ``` | |
| *** | |
| ### 16. Profunctors | |
| Concepts: | |
| * Things that consume and produce | |
| * Input/output adaptation | |
| * Bidirectional transformation | |
| Operation: | |
| ```text | |
| dimap : (A' → A) → (B → B') → P A B → P A' B' | |
| ``` | |
| *** | |
| ### 17. Isomorphisms | |
| Concepts: | |
| * Lossless conversion | |
| * Equivalent representations | |
| * Safe refactoring | |
| Formal condition: | |
| ```text | |
| g ∘ f = id_A | |
| f ∘ g = id_B | |
| ``` | |
| *** | |
| # Part 6 — Advanced Abstractions | |
| ### 18. F-Algebras, Catamorphisms, and Folding | |
| Concepts: | |
| * Recursive data | |
| * Folding | |
| * Interpretation | |
| * Event folding | |
| Formal intuition: | |
| ```text | |
| F A → A | |
| ``` | |
| Supply chain examples: | |
| * Fold order lines into totals | |
| * Fold shipment events into shipment state | |
| * Fold demand signals into forecast input | |
| *** | |
| ### 19. Free Structures and DSLs | |
| Concepts: | |
| * Free monoids | |
| * Free applicatives | |
| * Free monads | |
| * Description vs interpretation | |
| * Embedded DSLs in F# | |
| *** | |
| ### 20. Adjunctions | |
| Concepts: | |
| * Free/forgetful relationships | |
| * Adding structure | |
| * Forgetting structure | |
| * Moving between modeling worlds | |
| Formal idea: | |
| ```text | |
| Hom_D(F A, B) ≅ Hom_C(A, G B) | |
| ``` | |
| *** | |
| ### 21. Lenses and Optics | |
| Concepts: | |
| * Focusing into immutable data | |
| * Getters | |
| * Setters | |
| * Composable updates | |
| * Nested domain models | |
| *** | |
| # Part 7 — Domain Modeling with Category Theory | |
| ### 22. Types as Domain Constraints | |
| Concepts: | |
| * Domain primitives | |
| * Smart constructors | |
| * Refined types | |
| * Total functions | |
| * Illegal states unrepresentable | |
| Examples: | |
| * `Sku` | |
| * `WarehouseId` | |
| * `Quantity` | |
| * `LeadTime` | |
| * `Demand` | |
| * `ShipmentId` | |
| *** | |
| ### 23. Domain Workflows as Composable Arrows | |
| Concepts: | |
| * Workflows as transformations | |
| * State transitions | |
| * Valid business movements | |
| * Composition of steps | |
| Shapes: | |
| ```text | |
| A → B | |
| A → Result<Error, B> | |
| A → Async<Result<Error, B>> | |
| ``` | |
| *** | |
| ### 24. Error Modeling, Validation, and Partiality | |
| Concepts: | |
| * Partial functions | |
| * Total functions | |
| * `Option` | |
| * `Result` | |
| * Validation | |
| * Error accumulation vs fail-fast | |
| *** | |
| ### 25. Algebraic API Design | |
| Concepts: | |
| * APIs as algebras | |
| * Capabilities | |
| * Ports and adapters | |
| * Interpreters | |
| * Lawful module boundaries | |
| Examples: | |
| * Inventory service algebra | |
| * Supplier service algebra | |
| * Shipment service algebra | |
| * Forecasting service algebra | |
| *** | |
| # Part 8 — Supply Chain Application Chapters | |
| ### 26. Supply Chain Domain Model: Core Types and Invariants | |
| We build the foundation of a supply chain model. | |
| Areas: | |
| * Products | |
| * SKUs | |
| * Warehouses | |
| * Suppliers | |
| * Inventory | |
| * Orders | |
| * Shipments | |
| * Routes | |
| * Lead times | |
| *** | |
| ### 27. Inventory and Order Validation Pipeline | |
| Concepts applied: | |
| * Applicative validation | |
| * Monadic sequencing | |
| * Error semigroups | |
| * Kleisli composition | |
| Flow: | |
| ```text | |
| RawOrder | |
| → Validate customer | |
| → Validate SKUs | |
| → Validate quantities | |
| → Check inventory | |
| → Reserve stock | |
| → Confirm order | |
| ``` | |
| *** | |
| ### 28. Procurement and Supplier Selection Workflow | |
| Concepts applied: | |
| * Monoids | |
| * Contravariant comparers | |
| * Result workflows | |
| * Domain algebras | |
| * Natural transformations | |
| Concerns: | |
| * Lead time | |
| * MOQ | |
| * Cost | |
| * Reliability | |
| * Capacity | |
| * Region | |
| * Compliance | |
| *** | |
| ### 29. Shipment Planning and State Transitions | |
| Concepts applied: | |
| * Categories as state machines | |
| * Morphisms as valid transitions | |
| * Coproducts as event types | |
| * Catamorphisms for event folding | |
| * Lenses for immutable updates | |
| Lifecycle: | |
| ```text | |
| Planned | |
| → Booked | |
| → Picked | |
| → Dispatched | |
| → InTransit | |
| → Delivered | |
| → Closed | |
| ``` | |
| *** | |
| ### 30. Demand Forecasting and Data Transformation Boundaries | |
| Concepts applied: | |
| * Functors | |
| * Natural transformations | |
| * Profunctors | |
| * Isomorphisms | |
| * Algebraic API boundaries | |
| Flow: | |
| ```text | |
| RawDemandSignal | |
| → CleanedDemandSignal | |
| → AggregatedDemand | |
| → ForecastInput | |
| → ForecastResult | |
| → ReplenishmentRecommendation | |
| ``` | |
| *** | |
| ### 31. Building a Mini Supply Chain DSL in F\# | |
| Concepts applied: | |
| * Free structures | |
| * DSL design | |
| * Interpreters | |
| * Algebraic APIs | |
| Possible interpreters: | |
| * Execute | |
| * Log | |
| * Simulate | |
| * Test | |
| * Generate documentation | |
| * Estimate cost/time | |
| *** | |
| ### 32. End-to-End Supply Chain Case Study | |
| Final integration: | |
| ```text | |
| Demand Signal | |
| → Replenishment Decision | |
| → Supplier Selection | |
| → Purchase Order Creation | |
| → Inbound Shipment | |
| → Warehouse Receipt | |
| → Inventory Update | |
| → Customer Order Fulfillment | |
| ``` | |
| This chapter brings together: | |
| * Algebraic data types | |
| * Functors | |
| * Applicatives | |
| * Monads | |
| * Kleisli composition | |
| * Monoids | |
| * Natural transformations | |
| * State transitions | |
| * F-algebras | |
| * DSLs | |
| * Domain modeling | |
| *** | |
| # Final Learning Architecture | |
| To make the course effective, I suggest we use a **spiral approach**. | |
| That means we will revisit key ideas multiple times at increasing depth. | |
| ## Spiral 1 — Intuition | |
| We first understand the concept through ordinary F# code. | |
| Example: | |
| ```text | |
| map transforms a value inside a context. | |
| ``` | |
| ## Spiral 2 — Formalization | |
| Then we introduce the category theory definition and laws. | |
| Example: | |
| ```text | |
| F(id) = id | |
| F(g ∘ f) = F(g) ∘ F(f) | |
| ``` | |
| ## Spiral 3 — Design Meaning | |
| Then we ask what the concept means for software design. | |
| Example: | |
| ```text | |
| Functor preserves structure while transforming information. | |
| ``` | |
| ## Spiral 4 — Domain Modeling | |
| Then we apply the idea to real business modeling. | |
| Example: | |
| ```text | |
| Transform validated order data without losing validation context. | |
| ``` | |
| ## Spiral 5 — Supply Chain Implementation | |
| Finally, we use it in realistic supply chain workflows. | |
| Example: | |
| ```text | |
| RawOrder → ValidatedOrder → ReservedOrder → ConfirmedOrder | |
| ``` | |
| *** | |
| # How We’ll Handle Each Chapter | |
| For consistency, I’ll structure each chapter like this: | |
| ```text | |
| 1. Chapter goal | |
| 2. Concrete F# problem | |
| 3. Naive implementation | |
| 4. Pain point | |
| 5. Pattern discovery | |
| 6. Derived abstraction | |
| 7. Formal category theory definition | |
| 8. Laws and equations | |
| 9. F# implementation | |
| 10. Domain modeling interpretation | |
| 11. Supply chain example | |
| 12. Exercises | |
| 13. Summary | |
| ``` | |
| This gives us both: | |
| * practical intuition, and | |
| * mathematical precision. | |
| *** | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module SupplyChain.Supply | |
| open SharedKernel.DomainTypes | |
| open SharedKernel | |
| type RawInventoryBalance = | |
| { | |
| InventoryId: string | |
| ProductId: string | |
| LocationId: string | |
| OnHandQuantity: decimal | |
| ReservedQuantity: decimal | |
| SnapshotDate: System.DateOnly | |
| } | |
| type RawProducedSupply = | |
| { | |
| ProductionOrderId: string | |
| ProductId: string | |
| LocationId: string | |
| Quantity: decimal | |
| AvailableDate: System.DateOnly | |
| } | |
| type RawPurchaseSupply = | |
| { | |
| PurchaseOrderId: string | |
| ProductId: string | |
| LocationId: string | |
| Quantity: decimal | |
| ExpectedReceiptDate: System.DateOnly | |
| } | |
| type SupplyId = private SupplyId of string | |
| type ReservationId = private ReservationId of string | |
| type ProductionOrderId = private ProductionOrderId of string | |
| type PurchaseOrderId = private PurchaseOrderId of string | |
| type SupplyType = | |
| | OnHandInventory | |
| | ProducedSupply | |
| | PurchaseOrderSupply | |
| | FirmProductionOrder | |
| | PlannedProduction | |
| type InventoryBalance = | |
| { | |
| InventoryId: SupplyId | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| OnHand: Quantity | |
| Reserved: Quantity | |
| SnapshotDate: System.DateOnly | |
| } | |
| type ProducedSupply = | |
| { | |
| ProductionOrderId: ProductionOrderId | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| Quantity: Quantity | |
| AvailableDate: System.DateOnly | |
| } | |
| type PurchaseSupply = | |
| { | |
| PurchaseOrderId: PurchaseOrderId | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| Quantity: Quantity | |
| ExpectedReceiptDate: System.DateOnly | |
| } | |
| type AvailableSupply = | |
| { | |
| SupplyId: SupplyId | |
| SupplyType: SupplyType | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| AvailableQuantity: Quantity | |
| AvailableDate: System.DateOnly | |
| } | |
| type SupplyError = | |
| | Supplyailed of ValidationError list | |
| | SupplyNotFound of SupplyId | |
| | InsufficientSupply of ProductId * requested: Quantity * available: Quantity | |
| | SupplyPreparationFailed of string | |
| open Common.Validator | |
| module SupplyId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "SupplyId" | |
| |> map SupplyId | |
| let value (SupplyId x) = x | |
| module ProductionOrderId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "ProductionOrderId" | |
| |> map ProductionOrderId | |
| let value (ProductionOrderId x) = x | |
| module PurchaseOrderId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "PurchaseOrderId" | |
| |> map PurchaseOrderId | |
| let value (PurchaseOrderId x) = x | |
| let makeInventoryBalance inventoryId productId locationId onHand reserved snapshotDate = | |
| { | |
| InventoryId = inventoryId | |
| ProductId = productId | |
| LocationId = locationId | |
| OnHand = onHand | |
| Reserved = reserved | |
| SnapshotDate = snapshotDate | |
| } | |
| let validateInventoryBalance (raw: RawInventoryBalance) = | |
| makeInventoryBalance | |
| <!> SupplyId.create raw.InventoryId | |
| <*> ProductId.create raw.ProductId | |
| <*> LocationId.create raw.LocationId | |
| <*> Quantity.createNonNegative raw.OnHandQuantity | |
| <*> Quantity.createNonNegative raw.ReservedQuantity | |
| <*> return' raw.SnapshotDate | |
| let makeProducedSupply productionOrderId productId locationId quantity availableDate = | |
| { | |
| ProductionOrderId = productionOrderId | |
| ProductId = productId | |
| LocationId = locationId | |
| Quantity = quantity | |
| AvailableDate = availableDate | |
| } | |
| let validateProducedSupply (raw: RawProducedSupply) = | |
| makeProducedSupply | |
| <!> ProductionOrderId.create raw.ProductionOrderId | |
| <*> ProductId.create raw.ProductId | |
| <*> LocationId.create raw.LocationId | |
| <*> Quantity.createPositive raw.Quantity | |
| <*> return' raw.AvailableDate | |
| let makePurchaseSupply purchaseOrderId productId locationId quantity expectedReceiptDate = | |
| { | |
| PurchaseOrderId = purchaseOrderId | |
| ProductId = productId | |
| LocationId = locationId | |
| Quantity = quantity | |
| ExpectedReceiptDate = expectedReceiptDate | |
| } | |
| let validatePurchaseSupply (raw: RawPurchaseSupply) = | |
| makePurchaseSupply | |
| <!> PurchaseOrderId.create raw.PurchaseOrderId | |
| <*> ProductId.create raw.ProductId | |
| <*> LocationId.create raw.LocationId | |
| <*> Quantity.createPositive raw.Quantity | |
| <*> return' raw.ExpectedReceiptDate | |
| let inventoryToAvailableSupply inventory = | |
| let availableResult = | |
| Quantity.subtract inventory.OnHand inventory.Reserved | |
| match availableResult with | |
| | Ok available -> | |
| Valid | |
| { | |
| SupplyId = inventory.InventoryId | |
| SupplyType = OnHandInventory | |
| ProductId = inventory.ProductId | |
| LocationId = inventory.LocationId | |
| AvailableQuantity = available | |
| AvailableDate = inventory.SnapshotDate | |
| } | |
| | Error _ -> | |
| Invalid | |
| [ | |
| InvalidNonNegativeQuantity | |
| ("AvailableInventory", -1M) | |
| ] | |
| let producedSupplyToAvailableSupply produced = | |
| let supplyIdText = | |
| sprintf "PROD-%s" (ProductionOrderId.value produced.ProductionOrderId) | |
| let make supplyId = | |
| { | |
| SupplyId = supplyId | |
| SupplyType = ProducedSupply | |
| ProductId = produced.ProductId | |
| LocationId = produced.LocationId | |
| AvailableQuantity = produced.Quantity | |
| AvailableDate = produced.AvailableDate | |
| } | |
| SupplyId.create supplyIdText | |
| |> map make | |
| let purchaseSupplyToAvailableSupply purchase = | |
| let supplyIdText = | |
| sprintf "PO-%s" (PurchaseOrderId.value purchase.PurchaseOrderId) | |
| let make supplyId = | |
| { | |
| SupplyId = supplyId | |
| SupplyType = PurchaseOrderSupply | |
| ProductId = purchase.ProductId | |
| LocationId = purchase.LocationId | |
| AvailableQuantity = purchase.Quantity | |
| AvailableDate = purchase.ExpectedReceiptDate | |
| } | |
| SupplyId.create supplyIdText | |
| |> map make | |
| let prepareSupply rawInventory rawProduced rawPurchases = | |
| let inventorySupply = | |
| rawInventory | |
| |> traverse validateInventoryBalance | |
| |> function | |
| | Valid balances -> | |
| balances | |
| |> traverse inventoryToAvailableSupply | |
| | Invalid errors -> | |
| Invalid errors | |
| let producedSupply = | |
| rawProduced | |
| |> traverse validateProducedSupply | |
| |> function | |
| | Valid produced -> | |
| produced | |
| |> traverse producedSupplyToAvailableSupply | |
| | Invalid errors -> | |
| Invalid errors | |
| let purchaseSupply = | |
| rawPurchases | |
| |> traverse validatePurchaseSupply | |
| |> function | |
| | Valid purchases -> | |
| purchases | |
| |> traverse purchaseSupplyToAvailableSupply | |
| | Invalid errors -> | |
| Invalid errors | |
| let combineSupply inventory produced purchases = | |
| inventory @ produced @ purchases | |
| combineSupply | |
| <!> inventorySupply | |
| <*> producedSupply | |
| <*> purchaseSupply | |
| module SupplyChain.Capacity | |
| open SharedKernel.DomainTypes | |
| open SharedKernel | |
| open MasterData | |
| open Common.Validator | |
| type CapacityReservationId = private CapacityReservationId of string | |
| type Resource = | |
| { | |
| ResourceId: ResourceId | |
| Description: string | |
| LocationId: LocationId | |
| } | |
| type CapacityBucket = | |
| { | |
| ResourceId: ResourceId | |
| BucketDate: System.DateOnly | |
| LocationId: LocationId | |
| AvailableHours: decimal | |
| ReservedHours: decimal | |
| } | |
| module CapacityBucket = | |
| let remainingHours bucket = | |
| bucket.AvailableHours - bucket.ReservedHours | |
| let canReserve requiredHours bucket = | |
| remainingHours bucket >= requiredHours | |
| type CapacityRequirement = | |
| { | |
| PlannedProductionOrderId: string | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| ResourceId: ResourceId | |
| RequiredDate: System.DateOnly | |
| RequiredHours: decimal | |
| OperationNo: int | |
| } | |
| type CapacityReservation = | |
| { | |
| CapacityReservationId: CapacityReservationId | |
| PlannedProductionOrderId: string | |
| ResourceId: ResourceId | |
| LocationId: LocationId | |
| ReservationDate: System.DateOnly | |
| ReservedHours: decimal | |
| } | |
| type ProductionRequirement = | |
| { | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| Quantity: Quantity | |
| } | |
| type ProductionLimit = | |
| { | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| MinProductionQuantity: Quantity | |
| MaxProductionQuantity: Quantity | |
| } | |
| type ProductionLimitError = | |
| | BelowMinimumProduction of productId: ProductId * locationId: LocationId * requested: Quantity * minimum: Quantity | |
| | AboveMaximumProduction of productId: ProductId * locationId: LocationId * requested: Quantity * maximum: Quantity | |
| | ProductionNotAllowedAtLocation of productId: ProductId * locationId: LocationId | |
| type CapacityPlanningError = | |
| | RoutingMissing of ProductId * LocationId | |
| | ResourceMissing of ResourceId | |
| | CapacityBucketMissing of ResourceId * System.DateOnly | |
| | CapacityNotAvailable of resourceId: ResourceId * date: System.DateOnly * required: decimal * available: decimal | |
| | ProductionLimitViolation of ProductionLimitError | |
| | TransferLaneMissing of ProductId * fromLocation: LocationId * toLocation: LocationId | |
| | TransferNotAllowed of ProductId * fromLocation: LocationId * toLocation: LocationId | |
| | CapacityPlanningFailed of string | |
| type CapacityError = | |
| | ResourceNotFound of ResourceId | |
| | PlanningError of CapacityPlanningError | |
| type CapacityPlanningState = | |
| { | |
| Buckets: CapacityBucket list | |
| Reservations: CapacityReservation list | |
| Shortages: CapacityPlanningError list | |
| Messages: string list | |
| } | |
| let initialCapacityPlanningState buckets = | |
| { | |
| Buckets = buckets | |
| Reservations = [] | |
| Shortages = [] | |
| Messages = [] | |
| } | |
| module CapacityReservationId = | |
| open Common.Validator | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "CapacityReservationId" | |
| |> map CapacityReservationId | |
| let value (CapacityReservationId x) = x | |
| let checkProductionLimit limit (plannedOrder: ProductionRequirement) = | |
| if plannedOrder.ProductId <> limit.ProductId | |
| || plannedOrder.LocationId <> limit.LocationId then | |
| Error (ProductionNotAllowedAtLocation (plannedOrder.ProductId, plannedOrder.LocationId)) | |
| elif Quantity.value plannedOrder.Quantity < Quantity.value limit.MinProductionQuantity then | |
| Error | |
| (BelowMinimumProduction | |
| (plannedOrder.ProductId, | |
| plannedOrder.LocationId, | |
| plannedOrder.Quantity, | |
| limit.MinProductionQuantity)) | |
| elif Quantity.value plannedOrder.Quantity > Quantity.value limit.MaxProductionQuantity then | |
| Error | |
| (AboveMaximumProduction | |
| (plannedOrder.ProductId, | |
| plannedOrder.LocationId, | |
| plannedOrder.Quantity, | |
| limit.MaxProductionQuantity)) | |
| else | |
| Ok plannedOrder | |
| let calculateRequiredHours quantity operation = | |
| operation.SetupTimeHours + (Quantity.value quantity * operation.RunTimeHoursPerUnit) | |
| let makeCapacityReservationId (requirement: CapacityRequirement) = | |
| sprintf | |
| "CAPRES-%s-%s-%s" | |
| requirement.PlannedProductionOrderId | |
| (ResourceId.value requirement.ResourceId) | |
| (requirement.RequiredDate.ToString("yyyyMMdd")) | |
| |> CapacityReservationId.create | |
| let reserveCapacityRequirement state (requirement: CapacityRequirement) = | |
| let matchingBucket = | |
| state.Buckets | |
| |> List.tryFind (fun bucket -> | |
| bucket.ResourceId = requirement.ResourceId | |
| && bucket.LocationId = requirement.LocationId | |
| && bucket.BucketDate = requirement.RequiredDate) | |
| match matchingBucket with | |
| | None -> | |
| { | |
| state with | |
| Shortages = | |
| CapacityBucketMissing (requirement.ResourceId, requirement.RequiredDate) | |
| :: state.Shortages | |
| } | |
| | Some bucket -> | |
| let available = | |
| CapacityBucket.remainingHours bucket | |
| if available < requirement.RequiredHours then | |
| { | |
| state with | |
| Shortages = | |
| CapacityNotAvailable | |
| (requirement.ResourceId, | |
| requirement.RequiredDate, | |
| requirement.RequiredHours, | |
| available) | |
| :: state.Shortages | |
| } | |
| else | |
| match makeCapacityReservationId requirement with | |
| | Invalid errors -> | |
| { | |
| state with | |
| Shortages = | |
| CapacityPlanningFailed (sprintf "%A" errors) | |
| :: state.Shortages | |
| } | |
| | Valid reservationId -> | |
| let reservation = | |
| { | |
| CapacityReservationId = reservationId | |
| PlannedProductionOrderId = requirement.PlannedProductionOrderId | |
| ResourceId = requirement.ResourceId | |
| LocationId = requirement.LocationId | |
| ReservationDate = requirement.RequiredDate | |
| ReservedHours = requirement.RequiredHours | |
| } | |
| let updatedBucket = | |
| { | |
| bucket with | |
| ReservedHours = bucket.ReservedHours + requirement.RequiredHours | |
| } | |
| { | |
| state with | |
| Buckets = | |
| state.Buckets | |
| |> List.map (fun b -> | |
| if b.ResourceId = bucket.ResourceId | |
| && b.LocationId = bucket.LocationId | |
| && b.BucketDate = bucket.BucketDate then | |
| updatedBucket | |
| else | |
| b) | |
| Reservations = reservation :: state.Reservations | |
| Messages = | |
| sprintf | |
| "Reserved %M hours on resource %s for production order %s." | |
| requirement.RequiredHours | |
| (ResourceId.value requirement.ResourceId) | |
| requirement.PlannedProductionOrderId | |
| :: state.Messages | |
| } | |
| let reserveCapacityRequirements buckets requirements = | |
| let finalState = | |
| requirements | |
| |> List.fold reserveCapacityRequirement (initialCapacityPlanningState buckets) | |
| { | |
| finalState with | |
| Reservations = List.rev finalState.Reservations | |
| Shortages = List.rev finalState.Shortages | |
| Messages = List.rev finalState.Messages | |
| } | |
| let findRouting (routings: Routing list) (req: ProductionRequirement) = | |
| routings | |
| |> List.tryFind (fun routing -> | |
| routing.ProductId = req.ProductId | |
| && routing.LocationId = req.LocationId) | |
| let findProductionLimit limits (req: ProductionRequirement) = | |
| limits | |
| |> List.tryFind (fun limit -> | |
| limit.ProductId = req.ProductId | |
| && limit.LocationId = req.LocationId) | |
| let validateProductionLimit limits (req: ProductionRequirement) = | |
| match findProductionLimit limits req with | |
| | None -> | |
| Error | |
| (ProductionLimitViolation | |
| (ProductionNotAllowedAtLocation | |
| (req.ProductId, req.LocationId))) | |
| | Some limit -> | |
| checkProductionLimit limit req | |
| |> Result.mapError ProductionLimitViolation | |
| module SupplyChain.Demand | |
| open SharedKernel | |
| open SharedKernel.DomainTypes | |
| open Common.Validator | |
| type RawOrderLine = | |
| { | |
| OrderLineId: string | |
| ProductId: string | |
| Quantity: decimal | |
| RequestedDate: System.DateOnly | |
| } | |
| type RawCustomerOrder = | |
| { | |
| OrderId: string | |
| CustomerId: string | |
| ShipToLocationId: string | |
| Lines: RawOrderLine list | |
| } | |
| type RawForecastDemand = | |
| { | |
| ForecastId: string | |
| ProductId: string | |
| LocationId: string | |
| Quantity: decimal | |
| ForecastDate: System.DateOnly | |
| } | |
| type DemandId = private DemandId of string | |
| type OrderId = private OrderId of string | |
| type OrderLineId = private OrderLineId of string | |
| type ForecastId = private ForecastId of string | |
| type DemandSource = | |
| | SalesOrder | |
| | Forecast | |
| type DemandRequirement = | |
| { | |
| DemandId: DemandId | |
| Source: DemandSource | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| RequiredQuantity: Quantity | |
| DueDate: System.DateOnly | |
| Priority: int | |
| } | |
| type OrderLine = | |
| { | |
| OrderLineId: OrderLineId | |
| ProductId: ProductId | |
| Quantity: Quantity | |
| RequestedDate: System.DateOnly | |
| } | |
| type CustomerOrder = | |
| { | |
| OrderId: OrderId | |
| CustomerId: CustomerId | |
| Lines: OrderLine list | |
| ShipToLocationId: LocationId | |
| } | |
| type ForecastDemand = | |
| { | |
| ForecastId: ForecastId | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| Quantity: Quantity | |
| ForecastDate: System.DateOnly | |
| } | |
| type DemandError = | |
| | DemandValidationFailed of ValidationError list | |
| | DemandNotFound of DemandId | |
| | DemandPreparationFailed of string | |
| module DemandId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "DemandId" | |
| |> map DemandId | |
| let value (DemandId x) = x | |
| module OrderId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "OrderId" | |
| |> map OrderId | |
| let value (OrderId x) = x | |
| module OrderLineId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "OrderLineId" | |
| |> map OrderLineId | |
| let value (OrderLineId x) = x | |
| module ForecastId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "ForecastId" | |
| |> map ForecastId | |
| let value (ForecastId x) = x | |
| let makeOrderLine orderLineId productId quantity requestedDate = | |
| { | |
| OrderLineId = orderLineId | |
| ProductId = productId | |
| Quantity = quantity | |
| RequestedDate = requestedDate | |
| } | |
| let validateOrderLine (raw: RawOrderLine) = | |
| makeOrderLine | |
| <!> OrderLineId.create raw.OrderLineId | |
| <*> ProductId.create raw.ProductId | |
| <*> Quantity.createPositive raw.Quantity | |
| <*> return' raw.RequestedDate | |
| let makeCustomerOrder orderId customerId shipToLocationId lines = | |
| { | |
| OrderId = orderId | |
| CustomerId = customerId | |
| ShipToLocationId = shipToLocationId | |
| Lines = lines | |
| }:CustomerOrder | |
| open SharedKernel.DomainTypes | |
| let validateCustomerOrder (raw: RawCustomerOrder) = | |
| makeCustomerOrder | |
| <!> OrderId.create raw.OrderId | |
| <*> CustomerId.create raw.CustomerId | |
| <*> LocationId.create raw.ShipToLocationId | |
| <*> traverse validateOrderLine raw.Lines | |
| let makeForecastDemand forecastId productId locationId quantity forecastDate = | |
| { | |
| ForecastId = forecastId | |
| ProductId = productId | |
| LocationId = locationId | |
| Quantity = quantity | |
| ForecastDate = forecastDate | |
| } | |
| let validateForecastDemand (raw: RawForecastDemand) = | |
| makeForecastDemand | |
| <!> ForecastId.create raw.ForecastId | |
| <*> ProductId.create raw.ProductId | |
| <*> LocationId.create raw.LocationId | |
| <*> Quantity.createPositive raw.Quantity | |
| <*> return' raw.ForecastDate | |
| let demandIdFromOrderLine (orderId: OrderId) (lineId: OrderLineId) = | |
| let id = | |
| sprintf "SO-%s-%s" (OrderId.value orderId) (OrderLineId.value lineId) | |
| // safe because generated internally | |
| DemandId.create id | |
| let orderLineToDemandRequirement order (line: OrderLine) = | |
| let make demandId = | |
| { | |
| DemandId = demandId | |
| Source = SalesOrder | |
| ProductId = line.ProductId | |
| LocationId = order.ShipToLocationId | |
| RequiredQuantity = line.Quantity | |
| DueDate = line.RequestedDate | |
| Priority = 1 | |
| } | |
| demandIdFromOrderLine order.OrderId line.OrderLineId | |
| |> map make | |
| let customerOrderToDemandRequirements order = | |
| order.Lines | |
| |> traverse (orderLineToDemandRequirement order) | |
| let demandIdFromForecast (forecastId: ForecastId) = | |
| let id = | |
| sprintf "FC-%s" (ForecastId.value forecastId) | |
| DemandId.create id | |
| let forecastToDemandRequirement forecast = | |
| let make demandId = | |
| { | |
| DemandId = demandId | |
| Source = Forecast | |
| ProductId = forecast.ProductId | |
| LocationId = forecast.LocationId | |
| RequiredQuantity = forecast.Quantity | |
| DueDate = forecast.ForecastDate | |
| Priority = 5 | |
| } | |
| demandIdFromForecast forecast.ForecastId | |
| |> map make | |
| let prepareDemand rawOrders rawForecasts = | |
| let orderRequirements = | |
| rawOrders | |
| |> traverse validateCustomerOrder | |
| |> map | |
| (fun orders -> | |
| orders | |
| |> List.map customerOrderToDemandRequirements) | |
| |> function | |
| | Valid validations -> | |
| sequence validations | |
| |> map List.concat | |
| | Invalid errors -> | |
| Invalid errors | |
| let forecastRequirements = | |
| rawForecasts | |
| |> traverse validateForecastDemand | |
| |> function | |
| | Valid forecasts -> | |
| forecasts | |
| |> traverse forecastToDemandRequirement | |
| | Invalid errors -> | |
| Invalid errors | |
| let combineDemand orderReqs forecastReqs = | |
| orderReqs @ forecastReqs | |
| combineDemand | |
| <!> orderRequirements | |
| <*> forecastRequirements | |
| namespace SupplyChain.Domain | |
| open System | |
| module ResultEx = | |
| let mapError f = Result.mapError f | |
| let sequence (results: Result<'a, 'e> list) : Result<'a list, 'e list> = | |
| let folder next state = | |
| match next, state with | |
| | Ok value, Ok values -> Ok (value :: values) | |
| | Error err, Ok _ -> Error [ err ] | |
| | Ok _, Error errors -> Error errors | |
| | Error err, Error errors -> Error (err :: errors) | |
| List.foldBack folder results (Ok []) | |
| type DomainError = | |
| | ValidationError of string | |
| | DataError of string | |
| | MlError of string | |
| | OptimizationError of string | |
| type SkuId = private SkuId of string | |
| module SkuId = | |
| let create (raw: string) = | |
| if String.IsNullOrWhiteSpace raw then | |
| Error (ValidationError "SKU id cannot be blank") | |
| else | |
| raw.Trim().ToUpperInvariant() |> SkuId |> Ok | |
| let value (SkuId value) = value | |
| type LocationId = private LocationId of string | |
| module LocationId = | |
| let create (raw: string) = | |
| if String.IsNullOrWhiteSpace raw then | |
| Error (ValidationError "Location id cannot be blank") | |
| else | |
| raw.Trim().ToUpperInvariant() |> LocationId |> Ok | |
| let value (LocationId value) = value | |
| type DemandObservation = { | |
| Sku: SkuId | |
| Location: LocationId | |
| Date: DateTime | |
| Quantity: int | |
| } | |
| type DemandSeries = { | |
| Sku: SkuId | |
| Location: LocationId | |
| Values: (DateTime * float) list | |
| } | |
| type InventoryPolicy = { | |
| LeadTimeDays: int | |
| ReviewPeriodDays: int | |
| ServiceLevelZ: float | |
| } | |
| type InventoryPosition = { | |
| Sku: SkuId | |
| Location: LocationId | |
| OnHand: int | |
| OnOrder: int | |
| UnitCost: decimal | |
| HoldingCostPerUnit: decimal | |
| StockoutPenaltyPerUnit: decimal | |
| Policy: InventoryPolicy | |
| } | |
| type SupplierCapacity = { | |
| Sku: SkuId | |
| TotalUnitsAvailable: int | |
| } | |
| type ForecastSource = | |
| | MlNet | |
| | OnnxModel of string | |
| type ForecastRequest = { | |
| Series: DemandSeries | |
| Inventory: InventoryPosition | |
| HorizonDays: int | |
| } | |
| type Forecast = { | |
| Sku: SkuId | |
| Location: LocationId | |
| HorizonDays: int | |
| DailyDemand: int list | |
| MeanDailyDemand: float | |
| TotalDemand: int | |
| DemandStdDev: float | |
| SafetyStock: int | |
| ReorderPoint: int | |
| Source: ForecastSource | |
| } | |
| type OptimizationProblem = { | |
| Forecasts: Forecast list | |
| Inventories: InventoryPosition list | |
| Capacities: SupplierCapacity list | |
| } | |
| type ReplenishmentDecision = { | |
| Sku: SkuId | |
| Location: LocationId | |
| RecommendedOrderQty: int | |
| ForecastDemand: int | |
| SafetyStock: int | |
| TargetInventory: int | |
| ProjectedEndingInventory: int | |
| ExpectedShortage: int | |
| CostContribution: decimal | |
| } | |
| type ReplenishmentPlan = { | |
| Decisions: ReplenishmentDecision list | |
| TotalOrderQty: int | |
| ObjectiveValue: decimal | |
| Notes: string list | |
| } | |
| type ScenarioReport = { | |
| RunDate: DateTime | |
| Forecasts: Forecast list | |
| Plan: ReplenishmentPlan | |
| Notes: string list | |
| } | |
| module Keys = | |
| let demandKey (sku: SkuId) (location: LocationId) = | |
| SkuId.value sku, LocationId.value location | |
| module DomainRules = | |
| let targetInventory (forecast: Forecast) = forecast.TotalDemand + forecast.SafetyStock | |
| let projectedInventory (inventory: InventoryPosition) orderQty (forecast: Forecast) = | |
| inventory.OnHand + inventory.OnOrder + orderQty - forecast.TotalDemand | |
| let shortageFromProjected projectedInventory = | |
| if projectedInventory < 0 then abs projectedInventory else 0 | |
| let endingInventoryFromProjected projectedInventory = | |
| if projectedInventory > 0 then projectedInventory else 0 | |
| module SupplyChain.MasterData | |
| open Common.Validator | |
| open SharedKernel.DomainTypes | |
| open SharedKernel | |
| type ProductType = | |
| | FinishedGood | |
| | SemiFinishedGood | |
| | RawMaterial | |
| | PackagingMaterial | |
| type Product = | |
| { | |
| ProductId: ProductId | |
| Description: string | |
| BaseUom: UomCode | |
| ProductType: ProductType | |
| } | |
| type BomLine = | |
| { | |
| ParentProduct: ProductId | |
| ComponentProduct: ProductId | |
| QuantityPer: Quantity | |
| ComponentUom: UomCode | |
| } | |
| type Bom = | |
| { | |
| ParentProduct: ProductId | |
| Lines: BomLine list | |
| } | |
| type OperationNo = private OperationNo of int | |
| type RoutingOperation = | |
| { | |
| OperationNo: OperationNo | |
| ResourceId: ResourceId | |
| RunTimeHoursPerUnit: decimal | |
| SetupTimeHours: decimal | |
| QueueTimeDays: int | |
| MoveTimeDays: int | |
| } | |
| type Routing = | |
| { | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| Operations: RoutingOperation list | |
| } | |
| type TransferLane = | |
| { | |
| FromLocation: LocationId | |
| ToLocation: LocationId | |
| ProductId: ProductId | |
| TransferLeadTimeDays: int | |
| TransferCostPerUnit: decimal | |
| IsAllowed: bool | |
| } | |
| module OperationNo = | |
| let create value = | |
| if value > 0 then | |
| Valid (OperationNo value) | |
| else | |
| Invalid [ InvalidPositiveQuantity ("OperationNo", decimal value) ] | |
| let value (OperationNo x) = x | |
| type MasterDataError = | |
| | ProductNotFound of ProductId | |
| | BomNotFound of ProductId | |
| | RoutingNotFound of ProductId | |
| | InvalidBom of string | |
| | InvalidRouting of string | |
| module SupplyChain.Planning | |
| open Common.Validator | |
| open SharedKernel | |
| open SharedKernel.DomainTypes | |
| open Demand | |
| open Supply | |
| open Capacity | |
| module Materials = | |
| open MasterData | |
| type RequirementSource = | |
| | FromDemand of DemandId | |
| | FromParentRequirement of parentProduct: ProductId | |
| type RequirementLevel = | |
| | Level of int | |
| type MaterialRequirement = | |
| { | |
| Source: RequirementSource | |
| Level: RequirementLevel | |
| ParentProduct: ProductId option | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| RequiredQuantity: Quantity | |
| RequiredDate: System.DateOnly | |
| } | |
| type MaterialRequirementKey = | |
| { | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| RequiredDate: System.DateOnly | |
| } | |
| type BomExplosionResult = | |
| { | |
| DetailedRequirements: MaterialRequirement list | |
| AggregatedRequirements: MaterialRequirement list | |
| } | |
| type BomExplosionError = | |
| | BomCycleDetected of ProductId | |
| | InvalidBomQuantity of parent: ProductId * comp: ProductId | |
| | MasterDataError of MasterData.MasterDataError | |
| | BomExplosionFailed of string | |
| let explodeDemandOneLevel demand bom = | |
| bom.Lines | |
| |> List.map (fun line -> | |
| { | |
| Source = FromDemand demand.DemandId | |
| Level = Level 1 | |
| ParentProduct = Some demand.ProductId | |
| ProductId = line.ComponentProduct | |
| LocationId = demand.LocationId | |
| RequiredQuantity = Quantity.multiply demand.RequiredQuantity line.QuantityPer | |
| RequiredDate = demand.DueDate | |
| }) | |
| type BomLookup = ProductId -> Async<Result<Bom option, MasterDataError>> | |
| let explodeDemandOneLevelWithLookup (getBom:BomLookup) (demand: DemandRequirement) = | |
| async { | |
| let! bomResult = getBom demand.ProductId | |
| match bomResult with | |
| | Error err -> return Error (MasterDataError err) | |
| | Ok None -> return Ok [] | |
| | Ok (Some bom) -> return Ok (explodeDemandOneLevel demand bom) | |
| } | |
| let bindAsyncResult f asyncResult = | |
| async { | |
| let! result = asyncResult | |
| match result with | |
| | Ok value -> return! f value | |
| | Error error -> return Error error | |
| } | |
| let traverseAsyncResult f values = | |
| let folder value acc = | |
| async { | |
| let! accResult = acc | |
| match accResult with | |
| | Error e -> | |
| return Error e | |
| | Ok results -> | |
| let! valueResult = f value | |
| match valueResult with | |
| | Ok x -> | |
| return Ok (x :: results) | |
| | Error e -> | |
| return Error e | |
| } | |
| List.foldBack folder values (async { return Ok [] }) | |
| let explodeDemandsOneLevel (getBom:BomLookup) demands = | |
| async { | |
| let! explodedResult = | |
| traverseAsyncResult (explodeDemandOneLevelWithLookup getBom) demands | |
| match explodedResult with | |
| | Error e -> return Error e | |
| | Ok nestedRequirements -> | |
| return Ok (List.concat nestedRequirements) | |
| } | |
| let rec explodeProductRecursive | |
| (getBom: ProductId -> Async<Result<Bom option, MasterDataError>>) | |
| (visited: ProductId list) | |
| (source: RequirementSource) | |
| (locationId: LocationId) | |
| (requiredDate: System.DateOnly) | |
| (level: int) | |
| (parentProduct: ProductId option) | |
| (productId: ProductId) | |
| (requiredQuantity: Quantity) | |
| : Async<Result<MaterialRequirement list, BomExplosionError>> = | |
| async { | |
| if visited |> List.contains productId then | |
| return Error (BomCycleDetected productId) | |
| else | |
| let! bomResult = | |
| getBom productId | |
| match bomResult with | |
| | Error err -> | |
| return Error (MasterDataError err) | |
| | Ok None -> | |
| return Ok [] | |
| | Ok (Some bom) -> | |
| let explodeLine line = | |
| async { | |
| let componentQuantity = | |
| Quantity.multiply requiredQuantity line.QuantityPer | |
| let requirement = | |
| { | |
| Source = source | |
| Level = Level level | |
| ParentProduct = Some productId | |
| ProductId = line.ComponentProduct | |
| LocationId = locationId | |
| RequiredQuantity = componentQuantity | |
| RequiredDate = requiredDate | |
| } | |
| let! childResult = | |
| explodeProductRecursive | |
| getBom | |
| (productId :: visited) | |
| source | |
| locationId | |
| requiredDate | |
| (level + 1) | |
| (Some productId) | |
| line.ComponentProduct | |
| componentQuantity | |
| match childResult with | |
| | Error e -> | |
| return Error e | |
| | Ok childRequirements -> | |
| return Ok (requirement :: childRequirements) | |
| } | |
| let! results = | |
| bom.Lines | |
| |> traverseAsyncResult explodeLine | |
| match results with | |
| | Error e -> | |
| return Error e | |
| | Ok nested -> | |
| return Ok (List.concat nested) | |
| } | |
| let explodeDemandRecursive getBom (demand: DemandRequirement) = | |
| explodeProductRecursive | |
| getBom | |
| [] | |
| (FromDemand demand.DemandId) | |
| demand.LocationId | |
| demand.DueDate | |
| 1 | |
| None | |
| demand.ProductId | |
| demand.RequiredQuantity | |
| let explodeAllDemandsRecursive getBom demands = | |
| async { | |
| let! result = | |
| demands | |
| |> traverseAsyncResult (explodeDemandRecursive getBom) | |
| match result with | |
| | Error e -> | |
| return Error e | |
| | Ok nested -> | |
| return Ok (List.concat nested) | |
| } | |
| let aggregateMaterialRequirements (requirements: MaterialRequirement list) = | |
| requirements | |
| |> List.groupBy (fun r -> | |
| { | |
| ProductId = r.ProductId | |
| LocationId = r.LocationId | |
| RequiredDate = r.RequiredDate | |
| }) | |
| |> List.map (fun (key, reqs) -> | |
| let totalQuantity = | |
| reqs | |
| |> List.map (fun r -> r.RequiredQuantity) | |
| |> List.fold Quantity.add Quantity.zero | |
| { | |
| Source = FromParentRequirement key.ProductId | |
| Level = Level 0 | |
| ParentProduct = None | |
| ProductId = key.ProductId | |
| LocationId = key.LocationId | |
| RequiredQuantity = totalQuantity | |
| RequiredDate = key.RequiredDate | |
| }) | |
| let explodeAndAggregate getBom demands= | |
| async { | |
| let! explosionResult = | |
| explodeAllDemandsRecursive getBom demands | |
| match explosionResult with | |
| | Error e -> | |
| return Error e | |
| | Ok detailed -> | |
| let aggregated = | |
| aggregateMaterialRequirements detailed | |
| return | |
| Ok | |
| { | |
| DetailedRequirements = detailed | |
| AggregatedRequirements = aggregated | |
| } | |
| } | |
| open Materials | |
| open SharedKernel.Quantity | |
| type PeggingId = private PeggingId of string | |
| type PlanId = private PlanId of string | |
| type NetRequirementId = private NetRequirementId of string | |
| type PlannedProductionOrderId = private PlannedProductionOrderId of string | |
| type PlanningInput = | |
| { | |
| PlanningDate: System.DateOnly | |
| HorizonStart: System.DateOnly | |
| HorizonEnd: System.DateOnly | |
| Demands: Demand.DemandRequirement list | |
| Supplies: Supply.AvailableSupply list | |
| Products: MasterData.Product list | |
| Boms: MasterData.Bom list | |
| Routings: MasterData.Routing list | |
| Resources: Capacity.Resource list | |
| CapacityBuckets: Capacity.CapacityBucket list | |
| ProductionLimits: Capacity.ProductionLimit list | |
| TransferLanes: MasterData.TransferLane list | |
| } | |
| type PlanningInputError = | |
| | DemandInputInvalid of SharedKernel.ValidationError list | |
| | SupplyInputInvalid of SharedKernel.ValidationError list | |
| type Pegging = | |
| { | |
| PeggingId: PeggingId | |
| DemandId: DemandId | |
| SupplyId: SupplyId | |
| ProductId: ProductId | |
| Quantity: Quantity | |
| PeggedDate: System.DateOnly | |
| } | |
| type NetRequirement = | |
| { | |
| NetRequirementId: NetRequirementId | |
| DemandId: DemandId | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| RequiredQuantity: Quantity | |
| ShortageQuantity: Quantity | |
| DueDate: System.DateOnly | |
| } | |
| type PlannedProductionOrder = | |
| { | |
| PlannedProductionOrderId: PlannedProductionOrderId | |
| ProductId: ProductId | |
| LocationId: LocationId | |
| Quantity: Quantity | |
| DueDate: System.DateOnly | |
| SourceDemandId: DemandId | |
| } | |
| type NettingResult = | |
| { | |
| Peggings: Pegging list | |
| NetRequirements: NetRequirement list | |
| PlannedProductionOrders: PlannedProductionOrder list | |
| RemainingSupply: AvailableSupply list | |
| Messages: string list | |
| } | |
| type CapacityReservationProposal = | |
| { | |
| ProductId: ProductId | |
| ResourceId: ResourceId | |
| RequiredHours: decimal | |
| RequiredDate: System.DateOnly | |
| } | |
| type InterPlantTransferProposal = | |
| { | |
| ProductId: ProductId | |
| FromLocation: LocationId | |
| ToLocation: LocationId | |
| Quantity: Quantity | |
| ShipDate: System.DateOnly | |
| ArrivalDate: System.DateOnly | |
| } | |
| type CapacityPlanningResult = | |
| { | |
| Requirements: CapacityRequirement list | |
| Reservations: CapacityReservation list | |
| RemainingBuckets: CapacityBucket list | |
| Shortages: CapacityPlanningError list | |
| Messages: string list | |
| } | |
| type PlanningDecision = | |
| | UseExistingSupply of Pegging | |
| | CreatePlannedProduction of PlannedProductionOrder | |
| | ReserveCapacity of CapacityReservationProposal | |
| type SupplyChainPlan = | |
| { | |
| PlanId: PlanId | |
| Peggings: Pegging list | |
| NetRequirements: NetRequirement list | |
| PlannedProductionOrders: PlannedProductionOrder list | |
| RemainingSupply: AvailableSupply list | |
| CapacityRequirements: CapacityRequirement list | |
| CapacityReservations: CapacityReservation list | |
| CapacityShortages: CapacityPlanningError list | |
| TransferProposals: InterPlantTransferProposal list | |
| Messages: string list | |
| } | |
| type PlanningError = | |
| | DemandUnavailable | |
| | SupplyUnavailable | |
| | MasterDataMissing of string | |
| | CapacityUnavailable of string | |
| | PlanningFailed of string | |
| type PlanningStageResult = | |
| { | |
| Input: PlanningInput | |
| NettingResult: NettingResult option | |
| MaterialRequirements: MaterialRequirement list | |
| CapacityPlanningResult: CapacityPlanningResult option | |
| FinalPlan: SupplyChainPlan option | |
| Messages: string list | |
| } | |
| module PeggingId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "PeggingId" | |
| |> map PeggingId | |
| let value (PeggingId x) = x | |
| module NetRequirementId = | |
| let create value = | |
| value | |
| |> SharedKernel.String.requireNonEmpty "NetRequirementId" | |
| |> map NetRequirementId | |
| let value (NetRequirementId x) = x | |
| module PlannedProductionOrderId = | |
| let create demandId productId = | |
| sprintf | |
| "PPO-%s-%s" | |
| (DemandId.value demandId) | |
| (ProductId.value productId) | |
| |> PlannedProductionOrderId | |
| let value (PlannedProductionOrderId x) = x | |
| module PlanId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "PlanId" | |
| |> map PlanId | |
| let value (PlanId x) = x | |
| let makePeggingId demandId supplyId = | |
| sprintf | |
| "PEG-%s-%s" | |
| (DemandId.value demandId) | |
| (SupplyId.value supplyId) | |
| |> PeggingId.create | |
| let makeNetRequirementId demandId = | |
| sprintf | |
| "NET-%s" | |
| (DemandId.value demandId) | |
| |> NetRequirementId.create | |
| module Ports = | |
| open Demand | |
| open Supply | |
| open MasterData | |
| open Capacity | |
| type DemandPort = | |
| { | |
| GetOpenDemand : | |
| unit -> Async<Result<DemandRequirement list, DemandError>> | |
| } | |
| type SupplyPort = | |
| { | |
| GetAvailableSupply : | |
| unit -> Async<Result<AvailableSupply list, SupplyError>> | |
| } | |
| type MasterDataPort = | |
| { | |
| GetBom : | |
| ProductId -> Async<Result<Bom, MasterDataError>> | |
| GetRouting : | |
| ProductId -> Async<Result<Routing, MasterDataError>> | |
| } | |
| type CapacityPort = | |
| { | |
| CheckCapacity : | |
| CapacityRequirement -> Async<Result<CapacityBucket, CapacityError>> | |
| ReserveCapacity : | |
| CapacityRequirement -> Async<Result<CapacityReservation, CapacityError>> | |
| } | |
| module Heuristics = | |
| let canCover (demand: DemandRequirement) (supply: AvailableSupply) = | |
| demand.ProductId = supply.ProductId | |
| && demand.LocationId = supply.LocationId | |
| && supply.AvailableDate <= demand.DueDate | |
| && Quantity.value supply.AvailableQuantity >= Quantity.value demand.RequiredQuantity | |
| let chooseSupply demand supplies = | |
| supplies | |
| |> List.filter (canCover demand) | |
| |> List.sortBy (fun s -> s.AvailableDate) | |
| |> List.tryHead | |
| let createPlannedProduction (demand: DemandRequirement) = | |
| { | |
| PlannedProductionOrderId = PlannedProductionOrderId.create demand.DemandId demand.ProductId | |
| ProductId = demand.ProductId | |
| LocationId = demand.LocationId | |
| Quantity = demand.RequiredQuantity | |
| DueDate = demand.DueDate | |
| SourceDemandId = demand.DemandId | |
| } | |
| let planDemand demand supplies = | |
| match chooseSupply demand supplies with | |
| | Some supply -> | |
| // For now, PeggingId construction simplified | |
| let pegging = | |
| { | |
| PeggingId = Unchecked.defaultof<_> | |
| DemandId = demand.DemandId | |
| SupplyId = supply.SupplyId | |
| ProductId = demand.ProductId | |
| Quantity = demand.RequiredQuantity | |
| PeggedDate = demand.DueDate | |
| } | |
| UseExistingSupply pegging | |
| | None -> | |
| demand | |
| |> createPlannedProduction | |
| |> CreatePlannedProduction | |
| module PlanBuilder = | |
| open Heuristics | |
| open Demand | |
| open Supply | |
| let buildPlan demands supplies = | |
| let decisions = | |
| demands | |
| |> List.map (fun demand -> planDemand demand supplies) | |
| let peggings = | |
| decisions | |
| |> List.choose (function | |
| | UseExistingSupply pegging -> Some pegging | |
| | _ -> None) | |
| let plannedProductionOrders = | |
| decisions | |
| |> List.choose (function | |
| | CreatePlannedProduction order -> Some order | |
| | _ -> None) | |
| { | |
| PlanId = Unchecked.defaultof<_> | |
| Peggings = peggings | |
| PlannedProductionOrders = plannedProductionOrders | |
| NetRequirements = [] | |
| RemainingSupply = [] | |
| CapacityRequirements = [] | |
| CapacityReservations = [] | |
| CapacityShortages = [] | |
| TransferProposals = [] | |
| Messages = | |
| [ | |
| sprintf "Processed %d demand requirements." demands.Length | |
| sprintf "Created %d peggings." peggings.Length | |
| sprintf "Created %d planned production orders." plannedProductionOrders.Length | |
| ] | |
| } | |
| module Netting = | |
| open Supply | |
| type ConsumableSupply = | |
| { | |
| Supply: AvailableSupply | |
| RemainingQuantity: Quantity | |
| } | |
| type DemandSupplyMatch = | |
| { | |
| Demand: DemandRequirement | |
| MatchingSupply: AvailableSupply option | |
| } | |
| type DemandNettingState = | |
| { | |
| RemainingDemand: Quantity | |
| Supplies: ConsumableSupply list | |
| Peggings: Pegging list | |
| } | |
| let toConsumableSupply supply = | |
| { | |
| Supply = supply | |
| RemainingQuantity = supply.AvailableQuantity | |
| } | |
| let canSupplyCoverDemand (demand: Demand.DemandRequirement) consumableSupply = | |
| demand.ProductId = consumableSupply.Supply.ProductId | |
| && demand.LocationId = consumableSupply.Supply.LocationId | |
| && consumableSupply.Supply.AvailableDate <= demand.DueDate | |
| && greaterThanZero consumableSupply.RemainingQuantity | |
| let candidateSupplies demand supplies = | |
| supplies | |
| |> List.filter (canSupplyCoverDemand demand) | |
| |> List.sortBy (fun s -> s.Supply.AvailableDate) | |
| let canCover (demand: DemandRequirement) supply = | |
| demand.ProductId = supply.ProductId | |
| && demand.LocationId = supply.LocationId | |
| && supply.AvailableDate <= demand.DueDate | |
| && Quantity.value supply.AvailableQuantity >= Quantity.value demand.RequiredQuantity | |
| let findMatchingSupply supplies demand = | |
| supplies | |
| |> List.filter (canCover demand) | |
| |> List.sortBy (fun s -> s.AvailableDate) | |
| |> List.tryHead | |
| let matchDemandToSupply demands supplies = | |
| demands | |
| |> List.map (fun demand -> | |
| { | |
| Demand = demand | |
| MatchingSupply = findMatchingSupply supplies demand | |
| }) | |
| let consumeSupplyForDemand demand state consumableSupply = | |
| if isZero state.RemainingDemand then | |
| state | |
| elif not (canSupplyCoverDemand demand consumableSupply) then | |
| state | |
| else | |
| let peggedQty = | |
| minQuantity | |
| state.RemainingDemand | |
| consumableSupply.RemainingQuantity | |
| let newRemainingDemand = | |
| Quantity.value state.RemainingDemand - Quantity.value peggedQty | |
| |> Quantity.createNonNegative | |
| let newRemainingSupply = | |
| Quantity.value consumableSupply.RemainingQuantity - Quantity.value peggedQty | |
| |> Quantity.createNonNegative | |
| match newRemainingDemand, newRemainingSupply, makePeggingId demand.DemandId consumableSupply.Supply.SupplyId with | |
| | Valid remDemand, Valid remSupply, Valid peggingId -> | |
| let pegging = | |
| { | |
| PeggingId = peggingId | |
| DemandId = demand.DemandId | |
| SupplyId = consumableSupply.Supply.SupplyId | |
| ProductId = demand.ProductId | |
| Quantity = peggedQty | |
| PeggedDate = demand.DueDate | |
| } | |
| let updatedSupply = | |
| { | |
| consumableSupply with RemainingQuantity = remSupply | |
| } | |
| { | |
| RemainingDemand = remDemand | |
| Supplies = | |
| state.Supplies | |
| |> List.map (fun s -> | |
| if s.Supply.SupplyId = consumableSupply.Supply.SupplyId then | |
| updatedSupply | |
| else | |
| s) | |
| Peggings = pegging :: state.Peggings | |
| } | |
| | _ -> | |
| state | |
| type SingleDemandNettingResult = | |
| { | |
| Peggings: Pegging list | |
| NetRequirement: NetRequirement option | |
| PlannedProductionOrder: PlannedProductionOrder option | |
| Supplies: ConsumableSupply list | |
| } | |
| let netSingleDemand supplies (demand: Demand.DemandRequirement):SingleDemandNettingResult = | |
| let initialState = | |
| { | |
| RemainingDemand = demand.RequiredQuantity | |
| Supplies = supplies | |
| Peggings = [] | |
| } | |
| let candidates = | |
| candidateSupplies demand supplies | |
| let finalState = | |
| candidates | |
| |> List.fold (consumeSupplyForDemand demand) initialState | |
| let peggings = | |
| finalState.Peggings |> List.rev | |
| if isZero finalState.RemainingDemand then | |
| { | |
| Peggings = peggings | |
| NetRequirement = None | |
| PlannedProductionOrder = None | |
| Supplies = finalState.Supplies | |
| } | |
| else | |
| let netReqResult = | |
| makeNetRequirementId demand.DemandId | |
| let ppoIdResult = | |
| PlannedProductionOrderId.create demand.DemandId demand.ProductId | |
| match netReqResult, ppoIdResult with | |
| | Valid netReqId, ppoId -> | |
| let netRequirement = | |
| { | |
| NetRequirementId = netReqId | |
| DemandId = demand.DemandId | |
| ProductId = demand.ProductId | |
| LocationId = demand.LocationId | |
| RequiredQuantity = demand.RequiredQuantity | |
| ShortageQuantity = finalState.RemainingDemand | |
| DueDate = demand.DueDate | |
| } | |
| let plannedProduction = | |
| { | |
| PlannedProductionOrderId = ppoId | |
| ProductId = demand.ProductId | |
| LocationId = demand.LocationId | |
| Quantity = finalState.RemainingDemand | |
| DueDate = demand.DueDate | |
| SourceDemandId = demand.DemandId | |
| } | |
| { | |
| Peggings = peggings | |
| NetRequirement = Some netRequirement | |
| PlannedProductionOrder = Some plannedProduction | |
| Supplies = finalState.Supplies | |
| } | |
| | _ -> | |
| { | |
| Peggings = peggings | |
| NetRequirement = None | |
| PlannedProductionOrder = None | |
| Supplies = finalState.Supplies | |
| } | |
| type NettingState = | |
| { | |
| Supplies: ConsumableSupply list | |
| Peggings: Pegging list | |
| NetRequirements: NetRequirement list | |
| PlannedProductionOrders: PlannedProductionOrder list | |
| Messages: string list | |
| } | |
| let initialNettingState supplies = | |
| { | |
| Supplies = supplies |> List.map toConsumableSupply | |
| Peggings = [] | |
| NetRequirements = [] | |
| PlannedProductionOrders = [] | |
| Messages = [] | |
| } | |
| let netDemand state demand = | |
| let result = | |
| netSingleDemand state.Supplies demand | |
| let messages = | |
| match result.NetRequirement with | |
| | None -> | |
| sprintf | |
| "Demand %s fully covered." | |
| (DemandId.value demand.DemandId) | |
| | Some net -> | |
| sprintf | |
| "Demand %s has shortage %M." | |
| (DemandId.value demand.DemandId) | |
| (Quantity.value net.ShortageQuantity) | |
| { | |
| Supplies = result.Supplies | |
| Peggings = state.Peggings @ result.Peggings | |
| NetRequirements = | |
| match result.NetRequirement with | |
| | Some x -> state.NetRequirements @ [ x ] | |
| | None -> state.NetRequirements | |
| PlannedProductionOrders = | |
| match result.PlannedProductionOrder with | |
| | Some x -> state.PlannedProductionOrders @ [ x ] | |
| | None -> state.PlannedProductionOrders | |
| Messages = state.Messages @ [ messages ] | |
| } | |
| let netDemandAgainstSupply demands supplies = | |
| let orderedDemands = | |
| demands | |
| |> List.sortBy (fun d -> d.Priority, d.DueDate) | |
| let finalState = | |
| orderedDemands | |
| |> List.fold netDemand (initialNettingState supplies) | |
| { | |
| Peggings = finalState.Peggings | |
| NetRequirements = finalState.NetRequirements | |
| PlannedProductionOrders = finalState.PlannedProductionOrders | |
| RemainingSupply = | |
| finalState.Supplies | |
| |> List.map (fun s -> | |
| { | |
| s.Supply with | |
| AvailableQuantity = s.RemainingQuantity | |
| }) | |
| Messages = finalState.Messages | |
| } | |
| let runNetting planningInput = | |
| netDemandAgainstSupply | |
| planningInput.Demands | |
| planningInput.Supplies | |
| let buildSupplyChainPlan planId (nettingResult: NettingResult) = | |
| { | |
| PlanId = planId | |
| Peggings = nettingResult.Peggings | |
| NetRequirements = nettingResult.NetRequirements | |
| PlannedProductionOrders = nettingResult.PlannedProductionOrders | |
| RemainingSupply = nettingResult.RemainingSupply | |
| CapacityRequirements = [] | |
| CapacityReservations = [] | |
| CapacityShortages = [] | |
| TransferProposals = [] | |
| Messages = nettingResult.Messages | |
| } | |
| open MasterData | |
| let plannedOrderToCapacityRequirements (plannedOrder: PlannedProductionOrder) routing = | |
| routing.Operations | |
| |> List.map (fun operation -> | |
| { | |
| PlannedProductionOrderId = PlannedProductionOrderId.value plannedOrder.PlannedProductionOrderId | |
| ProductId = plannedOrder.ProductId | |
| LocationId = plannedOrder.LocationId | |
| ResourceId = operation.ResourceId | |
| RequiredDate = plannedOrder.DueDate.AddDays(-(operation.QueueTimeDays + operation.MoveTimeDays)) | |
| RequiredHours = calculateRequiredHours plannedOrder.Quantity operation | |
| OperationNo = OperationNo.value operation.OperationNo | |
| }:CapacityRequirement) | |
| let createRequirementsForOrder routings req = | |
| match findRouting routings req with | |
| | Some routing -> Ok routing | |
| | None -> Error (RoutingMissing (req.ProductId, req.LocationId)) | |
| let planCapacityForOrder limits routings (plannedOrder: PlannedProductionOrder) = | |
| plannedOrder | |
| |> (fun po -> { ProductId = po.ProductId; LocationId = po.LocationId; Quantity = po.Quantity}) | |
| |> validateProductionLimit limits | |
| |> Result.bind (createRequirementsForOrder routings) | |
| |> Result.map(plannedOrderToCapacityRequirements plannedOrder) | |
| let collectCapacityRequirements limits routings plannedOrders = | |
| let results = | |
| plannedOrders | |
| |> List.map (planCapacityForOrder limits routings) | |
| let errors = | |
| results | |
| |> List.choose (function | |
| | Error e -> Some e | |
| | Ok _ -> None) | |
| let requirements = | |
| results | |
| |> List.choose (function | |
| | Ok reqs -> Some reqs | |
| | Error _ -> None) | |
| |> List.concat | |
| if List.isEmpty errors then | |
| Ok requirements | |
| else | |
| Error errors | |
| (* | |
| Mature Capacity Planning Concepts | |
| ------------------------------------------ | |
| Finite capacity scheduling | |
| Alternate routings | |
| Alternate resources | |
| Setup families | |
| Sequence-dependent setup time | |
| Campaign planning | |
| Shift calendars | |
| Downtime calendars | |
| Labor constraints | |
| Material-capacity simultaneous feasibility | |
| Transfer lead times | |
| Transfer capacity | |
| Plant-specific min/max production | |
| Lot sizing | |
| Minimum batch quantity | |
| Maximum batch quantity | |
| Economic order quantity | |
| Shelf-life constraints | |
| Priority allocation | |
| *) | |
| let runCapacityPlanning limits routings buckets plannedOrders = | |
| match collectCapacityRequirements limits routings plannedOrders with | |
| | Error errors -> | |
| { | |
| Requirements = [] | |
| Reservations = [] | |
| RemainingBuckets = buckets | |
| Shortages = errors | |
| Messages = [ "Capacity planning failed during requirement generation." ] | |
| } | |
| | Ok requirements -> | |
| let reservationState = | |
| reserveCapacityRequirements buckets requirements | |
| { | |
| Requirements = requirements | |
| Reservations = reservationState.Reservations | |
| RemainingBuckets = reservationState.Buckets | |
| Shortages = reservationState.Shortages | |
| Messages = reservationState.Messages | |
| } | |
| (* | |
| let findTransferLane productId fromLocation toLocation (lanes: TransferLane list) = | |
| lanes | |
| |> List.tryFind (fun lane -> | |
| lane.ProductId = productId | |
| && lane.FromLocation = fromLocation | |
| && lane.ToLocation = toLocation) | |
| let proposeTransfer lane quantity requiredArrivalDate = | |
| if not lane.IsAllowed then | |
| TransferNotPossible "Transfer lane is not allowed." | |
| else | |
| let shipDate = requiredArrivalDate.AddDays(-lane.TransferLeadTimeDays) | |
| TransferProposed | |
| { | |
| ProductId = lane.ProductId | |
| FromLocation = lane.FromLocation | |
| ToLocation = lane.ToLocation | |
| Quantity = quantity | |
| ShipDate = shipDate | |
| ArrivalDate = requiredArrivalDate | |
| } | |
| *) | |
| module SupplyChain.SharedKernel | |
| open Common.Validator | |
| type ValidationError = | |
| | FieldMissing of fieldName: string | |
| | InvalidPositiveQuantity of fieldName: string * value: decimal | |
| | InvalidNonNegativeQuantity of fieldName: string * value: decimal | |
| | InvalidCode of fieldName: string * value: string | |
| module String = | |
| let requireNonEmpty fieldName value = | |
| if System.String.IsNullOrWhiteSpace value then | |
| Invalid [ FieldMissing fieldName ] | |
| else | |
| Valid value | |
| module Decimal = | |
| let requirePositive fieldName value = | |
| if value > 0M then | |
| Valid value | |
| else | |
| Invalid [ InvalidPositiveQuantity (fieldName, value) ] | |
| let requireNonNegative fieldName value = | |
| if value >= 0M then | |
| Valid value | |
| else | |
| Invalid [ InvalidNonNegativeQuantity (fieldName, value) ] | |
| type Quantity = private Quantity of decimal | |
| module Quantity = | |
| let createPositive value = | |
| value | |
| |> Decimal.requirePositive "Quantity" | |
| |> map Quantity | |
| let createNonNegative value = | |
| value | |
| |> Decimal.requireNonNegative "Quantity" | |
| |> map Quantity | |
| let zero = | |
| Quantity 0M | |
| let value (Quantity x) = x | |
| let add (Quantity a) (Quantity b) = | |
| Quantity (a + b) | |
| let subtract (Quantity a) (Quantity b) = | |
| let result = a - b | |
| if result < 0M then | |
| Error "Quantity cannot become negative." | |
| else | |
| Ok (Quantity result) | |
| let multiplyByDecimal factor (Quantity q) = | |
| Quantity (q * factor) | |
| let multiply (Quantity a) (Quantity b) = | |
| Quantity (a * b) | |
| let isZero q = value q = 0M | |
| let greaterThanZero q = value q > 0M | |
| let lessOrEqual a b = value a <= value b | |
| let minQuantity a b = if value a <= value b then a else b | |
| let subtractUnsafe a b = value a - value b |> createNonNegative | |
| module DomainTypes = | |
| type ProductId = private ProductId of string | |
| type LocationId = private LocationId of string | |
| type CustomerId = private CustomerId of string | |
| type ResourceId = private ResourceId of string | |
| type UomCode = private UomCode of string | |
| module ProductId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "ProductId" | |
| |> map ProductId | |
| let value (ProductId x) = x | |
| module LocationId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "LocationId" | |
| |> map LocationId | |
| let value (LocationId x) = x | |
| module CustomerId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "CustomerId" | |
| |> map CustomerId | |
| let value (CustomerId x) = x | |
| module ResourceId = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "ResourceId" | |
| |> map ResourceId | |
| let value (ResourceId x) = x | |
| module UomCode = | |
| let create value = | |
| value | |
| |> String.requireNonEmpty "UomCode" | |
| |> map UomCode | |
| let value (UomCode x) = x |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| namespace SupplyChain.Application | |
| open System | |
| open SupplyChain.Domain | |
| open SupplyChain.Domain.ResultEx | |
| module Workflow = | |
| type ScenarioConfig = { | |
| RunDate: DateTime | |
| HorizonDays: int | |
| PreferredOnnxModelPath: string option | |
| } | |
| type ResultBuilder() = | |
| member _.Bind(input, binder) = Result.bind binder input | |
| member _.Return(value) = Ok value | |
| member _.ReturnFrom(result) = result | |
| let result = ResultBuilder() | |
| let private buildForecastRequests horizonDays (inventories: InventoryPosition list) demandSeries = | |
| let inventoryByKey = | |
| inventories | |
| |> List.map (fun inventory -> Keys.demandKey inventory.Sku inventory.Location, inventory) | |
| |> Map.ofList | |
| let tryRequest (series: DemandSeries) = | |
| inventoryByKey | |
| |> Map.tryFind (Keys.demandKey series.Sku series.Location) | |
| |> function | |
| | Some inventory -> Ok { Series = series; Inventory = inventory; HorizonDays = horizonDays } | |
| | None -> | |
| Error (DataError (sprintf "Missing inventory for %s/%s" (SkuId.value series.Sku) (LocationId.value series.Location))) | |
| demandSeries | |
| |> List.map tryRequest | |
| |> sequence | |
| let private loadForecasts config deps requests = | |
| match config.PreferredOnnxModelPath with | |
| | Some path -> | |
| deps.ml.tryOnnxForecast path requests | |
| |> Result.bind (function | |
| | Some forecasts -> Ok forecasts | |
| | None -> deps.ml.forecastWithMlNet requests) | |
| | None -> deps.ml.forecastWithMlNet requests | |
| let run config deps = | |
| result { | |
| let! history = deps.data.loadDemandHistory () | |
| let! inventory = deps.data.loadInventory () | |
| let! capacities = deps.data.loadCapacities () | |
| let! demandSeries = deps.analytics.prepareDemandSeries history | |
| let! requests = buildForecastRequests config.HorizonDays inventory demandSeries | |
| let! forecasts = loadForecasts config deps requests | |
| let problem = { | |
| Forecasts = forecasts | |
| Inventories = inventory | |
| Capacities = capacities | |
| } | |
| let! plan = deps.optimization.optimizeReplenishment problem | |
| let notes = [ | |
| sprintf "Series prepared: %i" demandSeries.Length | |
| sprintf "Forecasts produced: %i" forecasts.Length | |
| sprintf "Optimization decisions: %i" plan.Decisions.Length | |
| ] | |
| return { | |
| RunDate = config.RunDate | |
| Forecasts = forecasts | |
| Plan = plan | |
| Notes = notes | |
| } | |
| } | |
| module PlanningWorkflow = | |
| open SupplyChain.Planning.PlanBuilder | |
| let runSimplePlanning ports = | |
| async { | |
| let! demandResult = | |
| ports.Demand.GetOpenDemand () | |
| match demandResult with | |
| | Error err -> | |
| return Error (DemandError err) | |
| | Ok demands -> | |
| let! supplyResult = | |
| ports.Supply.GetAvailableSupply () | |
| match supplyResult with | |
| | Error err -> | |
| return Error (SupplyError err) | |
| | Ok supplies -> | |
| let plan = | |
| buildPlan demands supplies | |
| return Ok plan | |
| } | |
| open SupplyChain.Supply | |
| open SupplyChain.Demand | |
| open SupplyChain.Planning | |
| open SupplyChain.SharedKernel | |
| open Common.Validator | |
| open SupplyChain.MasterData | |
| open SupplyChain.Capacity | |
| type RawPlanningRequest = | |
| { | |
| CustomerOrders: RawCustomerOrder list | |
| Forecasts: RawForecastDemand list | |
| InventoryBalances: RawInventoryBalance list | |
| ProducedSupplies: RawProducedSupply list | |
| PurchaseSupplies: RawPurchaseSupply list | |
| } | |
| type PlanningReferenceData = | |
| { | |
| Products: Product list | |
| Boms: Bom list | |
| Routings: Routing list | |
| Resources: Resource list | |
| CapacityBuckets: CapacityBucket list | |
| ProductionLimits: ProductionLimit list | |
| TransferLanes: TransferLane list | |
| } | |
| type AppError = | |
| | DemandPreparationError of ValidationError list | |
| | SupplyPreparationError of ValidationError list | |
| let preparePlanningInput (request: RawPlanningRequest) = | |
| let demandValidation = | |
| prepareDemand | |
| request.CustomerOrders | |
| request.Forecasts | |
| let supplyValidation = | |
| prepareSupply | |
| request.InventoryBalances | |
| request.ProducedSupplies | |
| request.PurchaseSupplies | |
| let makePlanningInput demands supplies = | |
| { | |
| Demands = demands | |
| Supplies = supplies | |
| PlanningDate = Data.planningDate | |
| HorizonStart = Data.horizonStart | |
| HorizonEnd = Data.horizonEnd | |
| Products = Data.products | |
| Boms = Data.boms | |
| Routings = Data.routings | |
| Resources = Data.resources | |
| CapacityBuckets = Data.capacityBuckets | |
| ProductionLimits = Data.productionLimits | |
| TransferLanes = Data.transferLanes | |
| }: PlanningInput | |
| makePlanningInput | |
| <!> demandValidation | |
| <*> supplyValidation | |
| let preparePlanningInputWithMaterials getBom (planningInput: PlanningInput) = | |
| async { | |
| let! explosionResult = | |
| Materials.explodeAndAggregate getBom planningInput.Demands | |
| match explosionResult with | |
| | Error e -> return Error e | |
| | Ok result -> | |
| return | |
| Ok | |
| { | |
| Demands = planningInput.Demands | |
| Supplies = planningInput.Supplies | |
| MaterialRequirements = result.DetailedRequirements | |
| AggregatedMaterialRequirements = result.AggregatedRequirements | |
| } | |
| } | |
| let preparePlanningInputResult request = | |
| request | |
| |> preparePlanningInput | |
| |> toResult | |
| let runNetting planningInput = | |
| let nettingResult = Netting.runNetting planningInput | |
| let planIdResult: Validation<PlanId,ValidationError> = PlanId.create "PLAN-001" | |
| match planIdResult with | |
| | Valid planId -> Ok (Netting.buildSupplyChainPlan planId nettingResult) | |
| | Invalid _ -> Error "Error while netting" | |
| let runPlanning planningInput = | |
| let nettingResult = Netting.runNetting planningInput | |
| let cplanning = runCapacityPlanning rout | |
| let planIdResult: Validation<PlanId,ValidationError> = PlanId.create "PLAN-001" | |
| match planIdResult with | |
| | Valid planId -> Ok (Netting.buildSupplyChainPlan planId nettingResult) | |
| | Invalid _ -> Error "Error while netting" | |
| namespace SupplyChain.Application | |
| open SupplyChain.Domain | |
| type DataPorts = { | |
| loadDemandHistory: unit -> Result<DemandObservation list, DomainError list> | |
| loadInventory: unit -> Result<InventoryPosition list, DomainError list> | |
| loadCapacities: unit -> Result<SupplierCapacity list, DomainError list> | |
| } | |
| type AnalyticsPorts = { | |
| prepareDemandSeries: DemandObservation list -> Result<DemandSeries list, DomainError list> | |
| } | |
| type MachineLearningPorts = { | |
| tryOnnxForecast: string -> ForecastRequest list -> Result<Forecast list option, DomainError list> | |
| forecastWithMlNet: ForecastRequest list -> Result<Forecast list, DomainError list> | |
| } | |
| type OptimizationPorts = { | |
| optimizeReplenishment: OptimizationProblem -> Result<ReplenishmentPlan, DomainError list> | |
| } | |
| type Dependencies = { | |
| data: DataPorts | |
| analytics: AnalyticsPorts | |
| ml: MachineLearningPorts | |
| optimization: OptimizationPorts | |
| } | |
| open SupplyChain | |
| open SupplyChain.Planning | |
| open SupplyChain.Planning.Ports | |
| open SupplyChain.Planning.PlanBuilder | |
| open SupplyChain.SharedKernel.DomainTypes | |
| open SupplyChain.MasterData | |
| type SupplyChainAppPorts = | |
| { | |
| Demand: DemandPort | |
| Supply: SupplyPort | |
| MasterData: MasterDataPort | |
| Capacity: CapacityPort | |
| } | |
| type AppError = | |
| | DemandError of Demand.DemandError | |
| | SupplyError of Supply.SupplyError | |
| | MasterDataError of MasterData.MasterDataError | |
| | CapacityError of Capacity.CapacityError | |
| | PlanningError of Planning.PlanningError | |
| type MasterDataPort = | |
| { | |
| GetBom : | |
| ProductId -> Async<Result<Bom option, MasterDataError>> | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment