1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:20:23 +01:00
effigenix/backend/docs/mvp/sequence-diagrams.md
2026-02-24 22:19:37 +01:00

31 KiB
Raw Blame History

Effigenix ERP Fachliche Sequenzdiagramme

Generiert aus den GitHub Issues und DDD-Planungsdokumenten. Alle Diagramme nutzen Mermaid-Syntax.


Inhaltsverzeichnis

  1. Stammdaten: Lagerorte verwalten
  2. Bestände: Bestandsposition & Chargen verwalten
  3. Chargen einbuchen & entnehmen
  4. Chargen sperren/entsperren & MHD-Überwachung
  5. Reservierungen (FEFO)
  6. Bestandsbewegungen (Stock Movements)
  7. Inventur
  8. Rezeptverwaltung
  9. Chargenplanung & Produktion (Batch Lifecycle)
  10. Produktionsaufträge (Production Orders)
  11. Produktion über Auftrag starten (End-to-End)
  12. Integration: Production → Inventory
  13. Integration: Procurement → Inventory
  14. Integration: Quality → Inventory
  15. Traceability: Vorwärts- & Rückwärts-Tracing

1. Stammdaten: Lagerorte verwalten

Issues: #1 (anlegen), #2 (bearbeiten/deaktivieren), #3 (abfragen)

sequenceDiagram
    actor User as Verwaltung
    participant Ctrl as StorageLocationController
    participant UC as CreateStorageLocation
    participant SL as StorageLocation (Aggregate)
    participant Repo as StorageLocationRepository

    Note over User,Repo: Story 1.1  Lagerort anlegen

    User->>Ctrl: POST /api/storage-locations
    Ctrl->>UC: execute(CreateStorageLocationCommand)
    UC->>Repo: existsByName(name)
    Repo-->>UC: false
    UC->>SL: StorageLocation.create(draft)
    SL-->>UC: Result.ok(storageLocation)
    UC->>Repo: save(storageLocation)
    UC-->>Ctrl: Result.ok(storageLocationId)
    Ctrl-->>User: 201 Created

    Note over User,Repo: Story 1.2  Lagerort bearbeiten

    User->>Ctrl: PUT /api/storage-locations/{id}
    Ctrl->>UC: execute(UpdateStorageLocationCommand)
    UC->>Repo: findById(id)
    Repo-->>UC: storageLocation
    UC->>SL: update(updateDraft)
    SL-->>UC: Result.ok()
    UC->>Repo: save(storageLocation)
    UC-->>Ctrl: Result.ok()
    Ctrl-->>User: 200 OK

    Note over User,Repo: Story 1.2  Lagerort deaktivieren

    User->>Ctrl: POST /api/storage-locations/{id}/deactivate
    Ctrl->>UC: execute(DeactivateStorageLocationCommand)
    UC->>Repo: findById(id)
    UC->>SL: deactivate()
    SL-->>UC: Result.ok()
    UC->>Repo: save(storageLocation)
    Ctrl-->>User: 200 OK

    Note over User,Repo: Story 1.3  Lagerorte abfragen

    User->>Ctrl: GET /api/storage-locations?type=COLD_ROOM&active=true
    Ctrl->>UC: execute(ListStorageLocationsQuery)
    UC->>Repo: findAll(filter)
    Repo-->>UC: List<StorageLocation>
    UC-->>Ctrl: List<StorageLocationResponse>
    Ctrl-->>User: 200 OK [...]

2. Bestände: Bestandsposition & Chargen verwalten

Issues: #4 (anlegen), #8 (abfragen), #9 (Parameter ändern)

sequenceDiagram
    actor User as Produktion/Lager
    participant Ctrl as StockController
    participant UC as CreateStock / UpdateStockParams
    participant Stock as Stock (Aggregate)
    participant Repo as StockRepository

    Note over User,Repo: Story 2.1  Bestandsposition anlegen

    User->>Ctrl: POST /api/stocks
    Ctrl->>UC: execute(CreateStockCommand)
    UC->>Repo: existsByArticleAndLocation(articleId, locationId)
    Repo-->>UC: false
    UC->>Stock: Stock.create(articleId, locationId)
    Stock-->>UC: Result.ok(stock)
    UC->>Repo: save(stock)
    Ctrl-->>User: 201 Created {stockId}

    Note over User,Repo: Story 2.6  Bestandsparameter ändern

    User->>Ctrl: PATCH /api/stocks/{id}/parameters
    Ctrl->>UC: execute(UpdateStockParamsCommand)
    UC->>Repo: findById(stockId)
    Repo-->>UC: stock
    UC->>Stock: updateMinimumLevel(newLevel)
    Stock-->>UC: Result.ok()
    UC->>Stock: updateMinimumShelfLife(newDays)
    Stock-->>UC: Result.ok()
    UC->>Repo: save(stock)
    Ctrl-->>User: 200 OK

    Note over User,Repo: Story 2.5  Bestandsposition abfragen

    User->>Ctrl: GET /api/stocks/{id}
    Ctrl->>UC: execute(GetStockQuery)
    UC->>Repo: findById(stockId)
    Repo-->>UC: stock (inkl. Batches & Reservierungen)
    UC-->>Ctrl: StockResponse
    Ctrl-->>User: 200 OK {totalQuantity, batches[], reservations[]}

3. Chargen einbuchen & entnehmen

Issues: #5 (einbuchen), #6 (entnehmen)

sequenceDiagram
    actor User as Lager/Produktion
    participant Ctrl as StockController
    participant UC as AddBatch / RemoveBatch
    participant Stock as Stock (Aggregate)
    participant Repo as StockRepository

    Note over User,Repo: Story 2.2  Charge einbuchen (addBatch)

    User->>Ctrl: POST /api/stocks/{id}/batches
    Note right of User: {batchRef, quantity, expiryDate, batchType}
    Ctrl->>UC: execute(AddBatchCommand)
    UC->>Repo: findById(stockId)
    Repo-->>UC: stock
    UC->>Stock: addBatch(batchReference, quantity, expiryDate)
    Note right of Stock: Validierung: qty > 0, expiryDate in Zukunft
    Stock-->>UC: Result.ok()
    Note right of Stock: totalQuantity += quantity
    UC->>Repo: save(stock)
    Ctrl-->>User: 201 Created

    Note over User,Repo: Story 2.3  Charge entnehmen (removeBatch)

    User->>Ctrl: POST /api/stocks/{id}/batches/{batchId}/remove
    Note right of User: {quantity}
    Ctrl->>UC: execute(RemoveBatchCommand)
    UC->>Repo: findById(stockId)
    Repo-->>UC: stock
    UC->>Stock: removeBatch(batchId, quantity)
    Note right of Stock: Prüfung: Batch vorhanden, qty ≤ verfügbar
    alt Menge reicht
        Stock-->>UC: Result.ok()
        UC->>Repo: save(stock)
        Ctrl-->>User: 200 OK
    else Nicht genug Bestand
        Stock-->>UC: Result.error(InsufficientStock)
        Ctrl-->>User: 409 Conflict
    end

4. Chargen sperren/entsperren & MHD-Überwachung

Issues: #7 (sperren/entsperren), #10 (MHD-Markierung), #11 (Mindestbestand)

sequenceDiagram
    actor User as QM/Lager
    participant Ctrl as StockController
    participant UC as BlockBatch / UnblockBatch
    participant Stock as Stock (Aggregate)
    participant Repo as StockRepository

    Note over User,Repo: Story 2.4  Charge sperren

    User->>Ctrl: POST /api/stocks/{id}/batches/{batchId}/block
    Note right of User: {reason}
    Ctrl->>UC: execute(BlockBatchCommand)
    UC->>Repo: findById(stockId)
    UC->>Stock: blockBatch(batchId, reason)
    Note right of Stock: Status: AVAILABLE → BLOCKED
    Stock-->>UC: Result.ok()
    UC->>Repo: save(stock)
    Ctrl-->>User: 200 OK

    User->>Ctrl: POST /api/stocks/{id}/batches/{batchId}/unblock
    Ctrl->>UC: execute(UnblockBatchCommand)
    UC->>Repo: findById(stockId)
    UC->>Stock: unblockBatch(batchId)
    Note right of Stock: Status: BLOCKED → AVAILABLE
    Stock-->>UC: Result.ok()
    UC->>Repo: save(stock)
    Ctrl-->>User: 200 OK
sequenceDiagram
    participant Scheduler as Täglicher Scheduler
    participant UC as MarkExpiredBatches
    participant Repo as StockRepository
    participant Stock as Stock (Aggregate)

    Note over Scheduler,Stock: Story 3.1  Abgelaufene & bald ablaufende Chargen

    Scheduler->>UC: execute()
    UC->>Repo: findAllWithBatches()
    Repo-->>UC: List<Stock>
    loop Für jeden Stock
        UC->>Stock: markExpiredBatches(today)
        Note right of Stock: expiryDate < today → EXPIRED
        Note right of Stock: expiryDate - today < minimumShelfLife → EXPIRING_SOON
        Stock-->>UC: List<DomainEvent>
        Note right of UC: Events: BatchExpired, BatchExpiringSoon
    end
    UC->>Repo: saveAll(stocks)

    Note over Scheduler,Stock: Story 3.2  Bestände unter Mindestbestand

    Scheduler->>UC: execute()
    UC->>Repo: findAllBelowMinimumLevel()
    Repo-->>UC: List<Stock>
    loop Für jeden Stock unter Minimum
        Note right of UC: Event: StockLevelBelowMinimum
    end

5. Reservierungen (FEFO)

Issues: #12 (reservieren), #13 (freigeben), #14 (bestätigen/entnehmen)

sequenceDiagram
    actor User as Produktion/Verkauf
    participant Ctrl as StockController
    participant UC as ReserveStock
    participant Stock as Stock (Aggregate)
    participant FEFO as FEFO-Algorithmus
    participant Repo as StockRepository

    Note over User,Repo: Story 4.1  Bestand reservieren (FEFO)

    User->>Ctrl: POST /api/stocks/{id}/reservations
    Note right of User: {quantity, referenceType, referenceId}
    Ctrl->>UC: execute(ReserveStockCommand)
    UC->>Repo: findById(stockId)
    Repo-->>UC: stock
    UC->>Stock: reserve(quantity, refType, refId)
    Stock->>FEFO: allocate(availableBatches, quantity)
    Note right of FEFO: Sortiere nach expiryDate ASC
    Note right of FEFO: Allokiere von ältester Charge zuerst
    FEFO-->>Stock: List<Allocation> [(batch1, qty1), (batch2, qty2)]
    Stock-->>UC: Result.ok(reservationId)
    UC->>Repo: save(stock)
    Ctrl-->>User: 201 Created {reservationId}

    Note over User,Repo: Story 4.2  Reservierung freigeben

    User->>Ctrl: DELETE /api/stocks/{id}/reservations/{resId}
    Ctrl->>UC: execute(ReleaseReservationCommand)
    UC->>Repo: findById(stockId)
    UC->>Stock: releaseReservation(reservationId)
    Note right of Stock: Allokierte Mengen → wieder AVAILABLE
    Stock-->>UC: Result.ok()
    UC->>Repo: save(stock)
    Ctrl-->>User: 200 OK

    Note over User,Repo: Story 4.3  Reservierung bestätigen (entnehmen)

    User->>Ctrl: POST /api/stocks/{id}/reservations/{resId}/confirm
    Ctrl->>UC: execute(ConfirmReservationCommand)
    UC->>Repo: findById(stockId)
    UC->>Stock: confirmReservation(reservationId)
    Note right of Stock: Reservierte Batches physisch entnommen
    Note right of Stock: totalQuantity -= reservedQuantity
    Note right of Stock: Reservierung entfernt
    Stock-->>UC: Result.ok(removedBatches)
    UC->>Repo: save(stock)
    Note right of UC: → StockMovement wird erstellt (Story 5.1)
    Ctrl-->>User: 200 OK

6. Bestandsbewegungen (Stock Movements)

Issues: #15 (erfassen), #16 (abfragen)

sequenceDiagram
    actor User as Lager/System
    participant Ctrl as StockMovementController
    participant UC as RecordStockMovement
    participant SM as StockMovement (Aggregate)
    participant Repo as StockMovementRepository
    participant StockRepo as StockRepository

    Note over User,Repo: Story 5.1  Bestandsbewegung erfassen

    User->>Ctrl: POST /api/stock-movements
    Note right of User: {articleId, batchId, movementType,<br/>direction, quantity, fromLocation,<br/>toLocation, reason, referenceDoc}
    Ctrl->>UC: execute(RecordStockMovementCommand)
    UC->>SM: StockMovement.create(draft)
    Note right of SM: Validierung:<br/>- GOODS_RECEIPT: fromLocation = null<br/>- WASTE: reason pflicht<br/>- TRANSFER: from ≠ to
    SM-->>UC: Result.ok(stockMovement)
    UC->>Repo: save(stockMovement)
    Note right of UC: Immutabel  keine Änderung möglich
    Ctrl-->>User: 201 Created {movementId}

    Note over User,Repo: Story 5.2  Bestandsbewegungen abfragen

    User->>Ctrl: GET /api/stock-movements?articleId=...&from=...&to=...
    Ctrl->>UC: execute(ListStockMovementsQuery)
    UC->>Repo: findByFilters(articleId, period, type, batchRef)
    Repo-->>UC: List<StockMovement>
    Ctrl-->>User: 200 OK [{movementId, type, direction, qty, date, ...}]

7. Inventur

Issues: #17 (anlegen), #18 (durchführen), #19 (abschließen), #20 (abbrechen/abfragen)

sequenceDiagram
    actor Counter as Zähler
    actor Approver as Prüfer (≠ Zähler)
    participant Ctrl as InventoryCountController
    participant UC as InventoryCount Use Cases
    participant IC as InventoryCount (Aggregate)
    participant StockRepo as StockRepository
    participant SMRepo as StockMovementRepository
    participant Repo as InventoryCountRepository

    Note over Counter,Repo: Story 6.1  Inventur anlegen

    Counter->>Ctrl: POST /api/inventory-counts
    Note right of Counter: {storageLocationId, countDate}
    Ctrl->>UC: execute(CreateInventoryCountCommand)
    UC->>StockRepo: findByLocation(locationId)
    StockRepo-->>UC: List<Stock> (aktuelle Bestände)
    UC->>IC: InventoryCount.create(location, date, expectedItems)
    Note right of IC: CountItems[] auto-befüllt:<br/>Artikel + Soll-Menge aus Bestand
    IC-->>UC: Result.ok(inventoryCount)
    UC->>Repo: save(inventoryCount)
    Ctrl-->>Counter: 201 Created {countId, items[]}

    Note over Counter,Repo: Story 6.2  Ist-Mengen erfassen

    Counter->>Ctrl: PATCH /api/inventory-counts/{id}/items/{itemId}
    Note right of Counter: {actualQuantity}
    Ctrl->>UC: execute(RecordCountItemCommand)
    UC->>Repo: findById(countId)
    UC->>IC: recordActualQuantity(itemId, actualQty)
    Note right of IC: variance = actualQty - expectedQty
    IC-->>UC: Result.ok()
    UC->>Repo: save(inventoryCount)
    Ctrl-->>Counter: 200 OK

    Note over Counter,Repo: Story 6.3  Inventur abschließen (4-Augen)

    Counter->>Ctrl: POST /api/inventory-counts/{id}/submit
    Ctrl->>UC: submitForApproval(countId, countedBy)
    UC->>IC: submit(countedBy)
    IC-->>UC: Result.ok()

    Approver->>Ctrl: POST /api/inventory-counts/{id}/approve
    Note right of Approver: approvedBy ≠ countedBy (4-Augen-Prinzip)
    Ctrl->>UC: execute(ApproveInventoryCountCommand)
    UC->>Repo: findById(countId)
    UC->>IC: approve(approvedBy)
    Note right of IC: Prüfung: approvedBy ≠ countedBy
    IC-->>UC: Result.ok(variances[])

    loop Für jede Abweichung
        UC->>SMRepo: save(StockMovement.ADJUSTMENT)
        Note right of SMRepo: Automatische Ausgleichsbuchung<br/>IN bei Mehr, OUT bei Weniger
    end
    UC->>Repo: save(inventoryCount)
    Ctrl-->>Approver: 200 OK {variances[], adjustments[]}

    Note over Counter,Repo: Story 6.4  Inventur abbrechen

    Counter->>Ctrl: POST /api/inventory-counts/{id}/cancel
    Ctrl->>UC: execute(CancelInventoryCountCommand)
    UC->>IC: cancel()
    Note right of IC: Nur möglich wenn noch nicht abgeschlossen
    IC-->>UC: Result.ok()
    UC->>Repo: save(inventoryCount)
    Ctrl-->>Counter: 200 OK

8. Rezeptverwaltung

Issues: #26 (anlegen), #27 (Zutaten), #28 (Schritte), #29 (aktivieren), #30 (archivieren), #31 (abfragen), #32 (Zykluserkennung)

sequenceDiagram
    actor User as Metzgermeister
    participant Ctrl as RecipeController
    participant UC as Recipe Use Cases
    participant Recipe as Recipe (Aggregate)
    participant CycleCheck as CycleDependencyChecker
    participant Repo as RecipeRepository

    Note over User,Repo: US-P02  Rezept anlegen (DRAFT)

    User->>Ctrl: POST /api/recipes
    Note right of User: {articleId, name, recipeType,<br/>yieldPercentage, shelfLifeDays}
    Ctrl->>UC: execute(CreateRecipeCommand)
    UC->>Recipe: Recipe.create(draft)
    Note right of Recipe: Status = DRAFT
    Recipe-->>UC: Result.ok(recipe)
    UC->>Repo: save(recipe)
    Ctrl-->>User: 201 Created {recipeId}

    Note over User,Repo: US-P03  Zutaten verwalten

    User->>Ctrl: POST /api/recipes/{id}/ingredients
    Note right of User: {articleId, quantity, unit, subRecipeId?}
    Ctrl->>UC: execute(AddIngredientCommand)
    UC->>Repo: findById(recipeId)
    UC->>Recipe: addIngredient(ingredientDraft)
    Note right of Recipe: Nur im DRAFT-Status erlaubt
    alt SubRecipeId angegeben
        UC->>CycleCheck: checkForCycles(recipeId, subRecipeId)
        Note right of CycleCheck: DFS-Traversierung aller<br/>verschachtelten Rezepte
        CycleCheck-->>UC: noCycle / cycleDetected
    end
    Recipe-->>UC: Result.ok()
    UC->>Repo: save(recipe)
    Ctrl-->>User: 201 Created

    Note over User,Repo: US-P04  Produktionsschritte verwalten

    User->>Ctrl: POST /api/recipes/{id}/steps
    Note right of User: {stepNumber, description,<br/>durationMinutes, temperatureCelsius}
    Ctrl->>UC: execute(AddProductionStepCommand)
    UC->>Recipe: addProductionStep(stepDraft)
    Recipe-->>UC: Result.ok()
    UC->>Repo: save(recipe)
    Ctrl-->>User: 201 Created

    Note over User,Repo: US-P05  Rezept aktivieren

    User->>Ctrl: POST /api/recipes/{id}/activate
    Ctrl->>UC: execute(ActivateRecipeCommand)
    UC->>Repo: findById(recipeId)
    UC->>Recipe: activate()
    Note right of Recipe: Prüfung: mind. 1 Zutat vorhanden<br/>Status: DRAFT → ACTIVE
    Recipe-->>UC: Result.ok()
    UC->>Repo: save(recipe)
    Ctrl-->>User: 200 OK

    Note over User,Repo: US-P06  Rezept archivieren

    User->>Ctrl: POST /api/recipes/{id}/archive
    Ctrl->>UC: execute(ArchiveRecipeCommand)
    UC->>Recipe: archive()
    Note right of Recipe: Status: ACTIVE → ARCHIVED
    Recipe-->>UC: Result.ok()
    UC->>Repo: save(recipe)
    Ctrl-->>User: 200 OK

9. Chargenplanung & Produktion (Batch Lifecycle)

Issues: #33 (planen), #34 (abfragen), #35 (starten + Verbrauch), #36 (abschließen), #37 (stornieren)

sequenceDiagram
    actor User as Metzgermeister
    participant Ctrl as BatchController
    participant UC as Batch Use Cases
    participant Batch as Batch (Aggregate)
    participant RecipeRepo as RecipeRepository
    participant Repo as BatchRepository

    Note over User,Repo: US-P09  Charge planen

    User->>Ctrl: POST /api/batches
    Note right of User: {recipeId, plannedQuantity, productionDate}
    Ctrl->>UC: execute(PlanBatchCommand)
    UC->>RecipeRepo: findById(recipeId)
    RecipeRepo-->>UC: recipe (muss ACTIVE sein)
    UC->>Batch: Batch.plan(draft)
    Note right of Batch: BatchNumber generiert: P-2026-02-24-001<br/>BestBeforeDate = productionDate + shelfLifeDays<br/>Status = PLANNED
    Batch-->>UC: Result.ok(batch)
    UC->>Repo: save(batch)
    Ctrl-->>User: 201 Created {batchId, batchNumber}

    Note over User,Repo: US-P10  Produktion starten

    User->>Ctrl: POST /api/batches/{id}/start
    Ctrl->>UC: execute(StartBatchCommand)
    UC->>Repo: findById(batchId)
    UC->>Batch: startProduction()
    Note right of Batch: Status: PLANNED → IN_PRODUCTION
    Batch-->>UC: Result.ok()
    UC->>Repo: save(batch)
    Ctrl-->>User: 200 OK

    Note over User,Repo: US-P10  Rohstoffverbrauch dokumentieren

    User->>Ctrl: POST /api/batches/{id}/consumptions
    Note right of User: {inputBatchId, quantity, unit}
    Ctrl->>UC: execute(RecordConsumptionCommand)
    UC->>Repo: findById(batchId)
    UC->>Batch: recordConsumption(consumptionDraft)
    Note right of Batch: Nur im Status IN_PRODUCTION<br/>inputBatchId muss eindeutig sein<br/>→ Chargen-Genealogie wird aufgebaut
    Batch-->>UC: Result.ok()
    UC->>Repo: save(batch)
    Ctrl-->>User: 201 Created

    Note over User,Repo: US-P11  Charge abschließen

    User->>Ctrl: POST /api/batches/{id}/complete
    Note right of User: {actualQuantity, waste, remarks}
    Ctrl->>UC: execute(CompleteBatchCommand)
    UC->>Repo: findById(batchId)
    UC->>Batch: complete(actualQty, waste, remarks)
    Note right of Batch: Prüfung: mind. 1 Consumption<br/>actualQuantity > 0<br/>Status: IN_PRODUCTION → COMPLETED
    Batch-->>UC: Result.ok()
    Note right of Batch: Event: BatchCompleted
    UC->>Repo: save(batch)
    Ctrl-->>User: 200 OK

    Note over User,Repo: US-P12  Charge stornieren

    User->>Ctrl: POST /api/batches/{id}/cancel
    Note right of User: {reason} (Pflichtfeld)
    Ctrl->>UC: execute(CancelBatchCommand)
    UC->>Batch: cancel(reason)
    Note right of Batch: Status: PLANNED/IN_PRODUCTION → CANCELLED
    Batch-->>UC: Result.ok()
    UC->>Repo: save(batch)
    Ctrl-->>User: 200 OK

10. Produktionsaufträge (Production Orders)

Issues: #38 (anlegen), #39 (freigeben), #40 (Produktion starten), #41 (abschließen/stornieren), #42 (umterminieren/abfragen)

sequenceDiagram
    actor User as Produktionsleiter
    participant Ctrl as ProductionOrderController
    participant UC as ProductionOrder Use Cases
    participant PO as ProductionOrder (Aggregate)
    participant RecipeRepo as RecipeRepository
    participant BatchUC as PlanBatch Use Case
    participant Repo as ProductionOrderRepository

    Note over User,Repo: US-P13  Produktionsauftrag anlegen

    User->>Ctrl: POST /api/production-orders
    Note right of User: {recipeId, plannedQuantity,<br/>plannedDate, priority}
    Ctrl->>UC: execute(CreateProductionOrderCommand)
    UC->>PO: ProductionOrder.create(draft)
    Note right of PO: Status = PLANNED<br/>Priority = NORMAL/HIGH/URGENT
    PO-->>UC: Result.ok(productionOrder)
    UC->>Repo: save(productionOrder)
    Ctrl-->>User: 201 Created {orderId}

    Note over User,Repo: US-P14  Produktionsauftrag freigeben

    User->>Ctrl: POST /api/production-orders/{id}/release
    Ctrl->>UC: execute(ReleaseProductionOrderCommand)
    UC->>Repo: findById(orderId)
    UC->>RecipeRepo: findById(recipeId)
    RecipeRepo-->>UC: recipe
    UC->>PO: release(recipe)
    Note right of PO: Prüfung: Rezept muss ACTIVE sein<br/>Status: PLANNED → RELEASED
    PO-->>UC: Result.ok()
    UC->>Repo: save(productionOrder)
    Ctrl-->>User: 200 OK

    Note over User,Repo: US-P15  Produktion über Auftrag starten

    User->>Ctrl: POST /api/production-orders/{id}/start-production
    Ctrl->>UC: execute(StartProductionFromOrderCommand)
    UC->>Repo: findById(orderId)

    UC->>BatchUC: planBatch(recipeId, plannedQty, date)
    Note right of BatchUC: Neue Charge wird automatisch erstellt
    BatchUC-->>UC: batchId

    UC->>PO: startProduction(batchId)
    Note right of PO: Status: RELEASED → IN_PRODUCTION<br/>BatchId wird verknüpft
    PO-->>UC: Result.ok()
    UC->>Repo: save(productionOrder)
    Ctrl-->>User: 200 OK {batchId}

    Note over User,Repo: US-P16  Auftrag abschließen

    User->>Ctrl: POST /api/production-orders/{id}/complete
    Ctrl->>UC: execute(CompleteProductionOrderCommand)
    UC->>Repo: findById(orderId)
    UC->>PO: complete()
    Note right of PO: Prüfung: verknüpfte Charge<br/>muss COMPLETED sein<br/>Status: IN_PRODUCTION → COMPLETED
    PO-->>UC: Result.ok()
    UC->>Repo: save(productionOrder)
    Ctrl-->>User: 200 OK

    Note over User,Repo: US-P17  Auftrag umterminieren

    User->>Ctrl: PATCH /api/production-orders/{id}/reschedule
    Note right of User: {newPlannedDate}
    Ctrl->>UC: execute(RescheduleProductionOrderCommand)
    UC->>PO: reschedule(newDate)
    Note right of PO: Nur in PLANNED/RELEASED erlaubt
    PO-->>UC: Result.ok()
    UC->>Repo: save(productionOrder)
    Ctrl-->>User: 200 OK

11. Produktion über Auftrag starten (End-to-End)

Issues: #38#42 (Production Orders), #33#37 (Batches), #12#14 (Reservierungen)

sequenceDiagram
    actor PL as Produktionsleiter
    actor MM as Metzgermeister
    participant PO as ProductionOrder
    participant Batch as Batch
    participant Stock as Stock (Inventory)
    participant SM as StockMovement

    Note over PL,SM: End-to-End: Vom Auftrag zur fertigen Charge

    PL->>PO: 1. Auftrag anlegen (recipeId, qty, date)
    Note right of PO: Status: PLANNED

    PL->>PO: 2. Auftrag freigeben
    Note right of PO: Prüft: Rezept ACTIVE<br/>Status: PLANNED → RELEASED

    PL->>PO: 3. Produktion starten
    PO->>Batch: Charge wird automatisch erstellt
    Note right of Batch: BatchNumber: P-2026-02-24-001<br/>Status: PLANNED → IN_PRODUCTION
    Note right of PO: Status: RELEASED → IN_PRODUCTION

    MM->>Stock: 4. Material reservieren (FEFO)
    Note right of Stock: Älteste Chargen zuerst allokiert

    MM->>Stock: 5. Reservierung bestätigen (entnehmen)
    Stock->>SM: StockMovement PRODUCTION_CONSUMPTION (OUT)
    Note right of Stock: totalQuantity sinkt

    MM->>Batch: 6. Verbrauch dokumentieren
    Note right of Batch: Consumption(inputBatchId, qty)<br/>→ Chargen-Genealogie

    MM->>Batch: 7. Charge abschließen
    Note right of Batch: actualQty, waste, remarks<br/>Status: IN_PRODUCTION → COMPLETED

    Batch->>Stock: 8. Fertiges Produkt einbuchen
    Stock->>SM: StockMovement PRODUCTION_OUTPUT (IN)
    Note right of Stock: Neuer StockBatch mit MHD

    PL->>PO: 9. Auftrag abschließen
    Note right of PO: Prüft: Charge COMPLETED<br/>Status: IN_PRODUCTION → COMPLETED

12. Integration: Production → Inventory

Issues: #22 (Produktionsergebnis einbuchen), #23 (Produktionsverbrauch verbuchen)

sequenceDiagram
    participant Batch as Batch (Production BC)
    participant EventBus as Domain Event Bus
    participant Handler as Integration Event Handler
    participant Stock as Stock (Inventory BC)
    participant SM as StockMovement
    participant StockRepo as StockRepository

    Note over Batch,StockRepo: Story 7.2  BatchCompleted → Bestand einbuchen

    Batch->>EventBus: BatchCompleted {batchId, articleId,<br/>actualQuantity, bestBeforeDate}
    EventBus->>Handler: handle(BatchCompleted)
    Handler->>StockRepo: findByArticleAndLocation(articleId, productionLocation)
    StockRepo-->>Handler: stock
    Handler->>Stock: addBatch(batchRef=PRODUCED, qty, expiryDate)
    Stock-->>Handler: Result.ok()
    Handler->>SM: StockMovement.create(PRODUCTION_OUTPUT, IN)
    Handler->>StockRepo: save(stock)
    Note right of Handler: Fertiges Produkt ist jetzt im Bestand

    Note over Batch,StockRepo: Story 7.3  ConsumptionRecorded → Bestand entnehmen

    Batch->>EventBus: ConsumptionRecorded {batchId,<br/>inputBatchId, quantity}
    EventBus->>Handler: handle(ConsumptionRecorded)
    Handler->>StockRepo: findByBatchReference(inputBatchId)
    StockRepo-->>Handler: stock
    Handler->>Stock: removeBatch(inputBatchId, quantity)
    Stock-->>Handler: Result.ok()
    Handler->>SM: StockMovement.create(PRODUCTION_CONSUMPTION, OUT)
    Handler->>StockRepo: save(stock)
    Note right of Handler: Rohstoff-Bestand reduziert

13. Integration: Procurement → Inventory

Issue: #21 (Wareneingang verarbeiten)

sequenceDiagram
    actor Lager as Wareneingang
    participant GR as GoodsReceipt (Procurement BC)
    participant QI as GoodsReceiptInspection (Quality BC)
    participant EventBus as Domain Event Bus
    participant Handler as Integration Event Handler
    participant Stock as Stock (Inventory BC)
    participant SM as StockMovement

    Note over Lager,SM: Story 7.1  Wareneingang → Bestand einbuchen

    Lager->>GR: Wareneingang erfassen
    Note right of GR: supplierBatchNumber, qty, expiryDate

    GR->>QI: Qualitätsprüfung anstoßen
    Note right of QI: Temperatur ≤ 4°C (Fleisch)<br/>Verpackung OK<br/>MHD ausreichend<br/>Dokumente vollständig

    alt Prüfung bestanden
        QI-->>GR: ACCEPTED
        GR->>EventBus: GoodsReceiptAccepted {articleId,<br/>supplierBatchNumber, qty, expiryDate}
        EventBus->>Handler: handle(GoodsReceiptAccepted)
        Handler->>Stock: addBatch(batchRef=PURCHASED,<br/>supplierBatchNumber, qty, expiryDate)
        Handler->>SM: StockMovement.create(GOODS_RECEIPT, IN)
        Note right of Stock: Ware im Bestand verfügbar
    else Prüfung fehlgeschlagen
        QI-->>GR: REJECTED
        GR->>EventBus: GoodsReceiptRejected {reason}
        Note right of GR: Ware wird zurückgewiesen<br/>Kein Bestand eingebucht
    end

14. Integration: Quality → Inventory

Issue: #24 (Qualitätssperre/Freigabe)

sequenceDiagram
    actor QM as Qualitätsmanager
    actor Approver as Freigeber (≠ QM)
    participant QH as QualityHold (Quality BC)
    participant EventBus as Domain Event Bus
    participant Handler as Integration Event Handler
    participant Stock as Stock (Inventory BC)

    Note over QM,Stock: Story 7.4  Qualitätssperre → Charge sperren

    QM->>QH: Qualitätssperre initiieren
    Note right of QH: Grund: TEMPERATURE_DEVIATION /<br/>SAMPLE_FAILED / CONTAMINATION
    QH->>EventBus: QualityHoldInitiated {batchId, reason}
    EventBus->>Handler: handle(QualityHoldInitiated)
    Handler->>Stock: blockBatch(batchId, reason)
    Note right of Stock: StockBatch Status: AVAILABLE → BLOCKED<br/>Kann nicht entnommen werden

    Note over QM,Stock: Korrekturmaßnahmen dokumentieren

    QM->>QH: addCorrectiveAction(description)

    Note over QM,Stock: Freigabe (4-Augen-Prinzip)

    Approver->>QH: release(approvedBy)
    Note right of QH: Prüfung: initiatedBy ≠ approvedBy
    QH->>EventBus: QualityHoldReleased {batchId}
    EventBus->>Handler: handle(QualityHoldReleased)
    Handler->>Stock: unblockBatch(batchId)
    Note right of Stock: StockBatch Status: BLOCKED → AVAILABLE

    Note over QM,Stock: Alternative: Charge abgelehnt

    QM->>QH: reject(reason)
    QH->>EventBus: QualityHoldRejected {batchId}
    EventBus->>Handler: handle(QualityHoldRejected)
    Handler->>Stock: removeBatch(batchId, fullQuantity)
    Note right of Stock: Charge komplett aus Bestand entfernt

15. Traceability: Vorwärts- & Rückwärts-Tracing

Issues: #43 (Vorwärts-Tracing / Rückruf), #44 (Rückwärts-Tracing / Herkunft)

sequenceDiagram
    actor QM as Qualitätsmanager
    participant Ctrl as TraceabilityController
    participant Svc as BatchTraceabilityService
    participant BatchRepo as BatchRepository
    participant SMRepo as StockMovementRepository

    Note over QM,SMRepo: US-P18  Vorwärts-Tracing (Rückruf-Szenario)
    Note over QM,SMRepo: "Welche Endprodukte sind von<br/>Rohstoff-Charge X betroffen?"

    QM->>Ctrl: GET /api/traceability/forward/{inputBatchId}
    Ctrl->>Svc: traceForward(inputBatchId)

    Svc->>BatchRepo: findByConsumptionInputBatchId(inputBatchId)
    BatchRepo-->>Svc: List<Batch> (Stufe 1: direkte Verwendung)

    loop Für jede gefundene Charge
        Svc->>BatchRepo: findByConsumptionInputBatchId(batchId)
        Note right of Svc: Rekursiv: auch Zwischenprodukte<br/>die in weiteren Chargen verwendet wurden
    end

    Svc->>SMRepo: findByBatchIds(allAffectedBatchIds)
    Note right of SMRepo: Welche Bewegungen?<br/>An welche Filialen geliefert?

    Svc-->>Ctrl: TraceabilityResult {<br/>  affectedBatches[],<br/>  affectedBranches[],<br/>  stockMovements[]<br/>}
    Ctrl-->>QM: 200 OK  Rückruf-Daten

    Note over QM,SMRepo: US-P19  Rückwärts-Tracing (Herkunftsnachweis)
    Note over QM,SMRepo: "Woher stammen die Zutaten<br/>von Endprodukt-Charge Y?"

    QM->>Ctrl: GET /api/traceability/backward/{outputBatchId}
    Ctrl->>Svc: traceBackward(outputBatchId)

    Svc->>BatchRepo: findById(outputBatchId)
    BatchRepo-->>Svc: batch (mit Consumptions)

    loop Für jede Consumption
        Note right of Svc: inputBatchId → Herkunft ermitteln
        Svc->>BatchRepo: findById(inputBatchId)
        BatchRepo-->>Svc: inputBatch
        Note right of Svc: Falls inputBatch eigene Consumptions hat:<br/>Rekursiv weiter zurückverfolgen
    end

    Svc-->>Ctrl: TraceabilityResult {<br/>  genealogyTree: {<br/>    outputBatch → [inputBatch1, inputBatch2]<br/>    inputBatch1 → [rawMaterialBatch1]<br/>  },<br/>  supplierBatches[] (Blätter des Baums)<br/>}
    Ctrl-->>QM: 200 OK  Herkunftsnachweis

Legende

Symbol Bedeutung
actor Menschlicher Akteur (Rolle)
participant Systemkomponente
Note Erklärung / Invarianten
alt/else Bedingte Verzweigung
loop Schleife / Wiederholung
Synchroner Aufruf
-->> Rückantwort
Event: XXX Domain Event