Skip to content

ShopFloor - Workflow Engine

How the route/scan engine actually executes. This is the runtime mechanics — for the conceptual flow see 02-unit-flow.md, and for the station role definitions see 12-station-catalog.md.


The whole engine rests on these four entities working together:

erDiagram
Route ||--o{ RouteFlow : "ordered steps"
Route ||--o{ ProductionFlow : "instances"
Route ||--o{ WorkOrders : "runs on"
RouteFlow }o--|| StationType : "step is of type"
ProductionFlow ||--o{ Station : "physical stations"
Station }o--|| StationType : "is of type"
EntityRoleKey fields
RouteTemplate defining what StationTypes a unit must visit. Scoped to a ProductLineCategory.RouteID, ProductLineCategoryID
RouteFlowOne ordered step inside a Route.RouteID, StationTypeID, ScanOrder
ProductionFlowA physical instantiation of a Route inside a Project. Multiple ProductionFlows can share a Route.ProductionFlowID, RouteID, ProjectID
StationA physical workstation (scanner + screen + operator).StationID, StationTypeID, ProductionFlowID

A WorkOrders row carries a RouteID. Every WorkTracking row belongs to a WorkOrder and therefore inherits that Route. The Route’s ordered RouteFlow rows define the sequence of StationTypes the unit must pass through.


All forward motion goes through one method: ScanUnitControllerService.UpdateWorkTrackingSN(sn, st, project), lines 19–156.

1. Look up WorkTracking by (serial number, project).
2. Resolve WorkTracking.WorkOrder.RouteID.
3. Pull ordered list of RouteFlow rows for that Route, filtered to ScanOrder > 0.
4. Find currentIndex = position of WorkTracking.Station.StationTypeID in that list.
5. Find candidateIndex = position of scannedStation.StationTypeID in that list.
6. Reject unless candidateIndex == currentIndex + 1.
7. Call StationGeneralActionService.IsStationChangeNotRestrictionCompliant():
verify both WorkOrder and scanned Station share a ProductLineCategory
(if strict membership is enabled on the project).
8. Update WorkTracking.StationID and LastScanDate. Save.
  • No skipping stations. You cannot scan a unit at station N+2 while it’s at station N.
  • No going backwards. Same check rejects scans at earlier positions.
  • Strict product-line membership (when configured). A laptop on a laptop route can’t be scanned at a desktop-route station.
  • No branching. The dispatcher only handles linear forward motion. Repair Center, BER, Quarantine, Harvesting are entered by direct WorkTracking.StationID assignment from inside a service — completely bypassing the dispatcher.
  • No skip mechanism. If a unit shouldn’t visit a station (e.g., a monitor doesn’t need Kitting), the station must be absent from that Route’s RouteFlow. There is no “conditionally skip” feature.
  • No post-branch re-entry logic. Once a unit lands in Repair Center, there is no service method that explicitly returns it to a specific Route step. See 02-unit-flow.md §6.2.

flowchart TD
A[Operator scans serial<br/>at Station S] --> B[Look up WorkTracking by SN+Project]
B --> C[Resolve Route via WorkOrder.RouteID]
C --> D[Get ordered RouteFlow steps]
D --> E[currentIdx = position of unit's<br/>current StationType]
E --> F[candidateIdx = position of<br/>scanned Station's StationType]
F --> G{candidateIdx == currentIdx + 1?}
G -->|no| H[Reject scan<br/>show error]
G -->|yes| I[ProductLineCategory check]
I -->|fail| H
I -->|pass| J[Update WorkTracking.StationID]
J --> K[Set LastScanDate]
K --> L[RepositoryManager.Save]
L --> M[Audit.NET → Mongo]

flowchart LR
A[Pre-alert / inbound BOL arrives] --> B[WorkOrder created]
B --> C[ProductLineCategory selected<br/>e.g., Laptop]
C --> D[Route chosen — must match<br/>WorkOrder.ProductLineCategoryID]
D --> E[WorkOrder.RouteID set]
E --> F[All WorkTracking rows<br/>inherit this route]

The Route is fixed at WorkOrder creation. ⚠️ Assumption — there is no observed code path that changes a WorkOrder’s RouteID after creation. If a unit needs a different route (e.g., an RMA’d laptop now needs a refurb-only route), it appears the workaround is to create a new WorkOrder.


UpdateWorkTrackingSN is the central entry, but most station-specific services call it (or its underlying StationGeneralActionService checks) in addition to their own logic. Examples:

ServiceWhat it adds beyond the dispatch check
GradingStationControllerserviceQR parse → grade lookup → set GradingID
KittingStationControllerServiceAccessory scan → KittingLog row → BOM completion check
PackingStationControllerServiceGradingID validation; optional BoxPallet creation
CleaningStationControllerServiceWindowsTestResultsUpload existence check
PalletStationControllerServicePallet assignment via PalletServiceFactory
FinishGoodStationControllerServicePallet close (IsOpen=false, CloseDate)
RepairCenterStationControllerServiceDirect StationID reassignment (bypasses dispatcher)
TriageEvaluationServiceDirect StationID reassignment via ProcessItemToStation (bypasses dispatcher)

🔧 Refactor finding: route-compliance validation is duplicated across 6+ station services. Centralize so it lives in the dispatcher only.


Stations like Repair Center, BER, Quarantine, Harvesting, and StandByPallet aren’t part of any Route. They’re entered by services that directly set WorkTracking.StationID.

TriggerCode pathEffect
Triage routes to Repair Center (Mexicali tenant)TriageEvaluationService.PromoteBasedOnGradeWorkTracking.StationID = repairCenterStation.StationID
Triage routes to Quarantine (RTV)TriageEvaluationService.ProcessItemToStation (Quarantine branch)WorkTracking.StationID = quarantineStation.StationID + Quarantine row + UnitNotes
3 failed repairs → BERRepairCenterStationControllerService.sendToBER (lines 955–999)WorkTracking.StationID = berStation.StationID, GradingID = "TYPE BER", LabelSKU updated
RMA arrival → Dock resetRMAControllerService.CreateNewRma (line 198)WorkTracking.StationID = dockStation.StationID + clear dates and pallets
Harvesting consumes BER unitHarvestingControllerService (line 175 calls CreateParts)HarvestingEvent + Part rows; donor unit terminal

Observation: branch routing has no unified mechanism. Each service finds the target station by querying StationType by name or enum and assigning the StationID directly. There’s no “branch event” or “route override” concept.

🔧 Refactor finding: branch routing is scattered; consider a StationTransitionService that encapsulates both routed and branched movements, captures the reason, and writes a unified WorkTrackingOperations audit row.


WorkTrackingOperations is meant to log every station-to-station movement, but ⚠️ the write path is not fully traced. From the entities, the table holds per-unit audit rows; from code, it’s referenced as an inverse navigation on WorkTracking but I did not locate an explicit insert. Possibilities:

  1. EF Core change-tracking writes it automatically — unlikely for an explicit log table.
  2. A specific service (e.g., StationGeneralActionService) writes it on every successful scan — confirm by grep.
  3. The table is populated by Audit.NET or a trigger — possible but unusual.

This is worth confirming before relying on WorkTrackingOperations as the canonical movement log.


Beyond ordered traversal, a few cross-cutting gates apply:

GateWhereWhat it does
ProductLineCategory membershipStationGeneralActionService.IsStationChangeNotRestrictionCompliantRejects scans where WorkOrder and Station are in different product-line categories (when strict membership is on).
WindowsTestResultsUpload presenceCleaningStationControllerServiceRejects Cleaning advance if the unit lacks imaging upload — defense against units leaking past imaging.
GradingID requiredPackingStationControllerService (line 186)Rejects Packing if Project.GradingValidation=true and GradingID is null.
Auto-BER on 3 failed repairsRepairCenterStationControllerService (line 1438)After 3 failed Repaired=false attempts on the same error code, unit auto-moves to BER.
Out-of-route FGFG_OutOfRoute station (ID 20)⚠️ Confirm — likely used when a unit reaches FG via a non-standard path.

A common source of confusion. Both look similar but serve different roles:

AspectRouteProductionFlow
DefinesWhich StationTypes, in what orderWhich physical Stations exist on a line
Scoped toProductLineCategoryProject
Cardinality1 Route can be shared by many WorkOrders and many ProductionFlowsEach ProductionFlow belongs to one Project
MultiplicityMany ProductionFlows can implement the same Route (different facilities, different shifts)Each ProductionFlow has its own Station set
Touched by dispatcher?Yes (defines next allowed StationType)No (dispatcher uses Station.StationTypeID, not the ProductionFlow)

⚠️ Assumption: the dispatcher does not check that the scanned Station belongs to the unit’s ProductionFlow. This would let a unit be scanned at a Dock station on a different production line. Confirm whether this is enforced elsewhere — if not, it’s a quiet correctness gap.


Concrete walkthrough of a Grade-A laptop scan sequence.

EntityValue
Project”ACME-Refurb”
ProductLineCategory”Laptop”
Route “Standard Laptop Refurb”StationTypes ordered: Dock(1) → Debox(2) → Triage(3) → Image Download(4) → MoveFromWindowsTestSystem(5) → Cleaning(6) → Grading(7) → Kitting(8) → Packing(9) → QA(10) → Pallet(11) → FG(12)
ProductionFlow “Mexicali Line 1”Has physical Stations for each StationType above
WorkOrder “WO-1234”RouteID = Standard Laptop Refurb
WorkTracking SN-ABC-001Currently at Dock (StationTypeID=12)
Operator scans SN-ABC-001 at the Debox station (Mexicali Line 1 Debox).
Dispatcher:
currentIdx = position of Dock in RouteFlow = 0
candidateIdx = position of Debox = 1
candidateIdx == currentIdx + 1 ✓
ProductLineCategory matches ✓
→ WorkTracking.StationID = Debox station ID
→ LastScanDate = now

Scan #2 — Skip Image Download? Rejected.

Section titled “Scan #2 — Skip Image Download? Rejected.”
Operator (in error) scans SN-ABC-001 at Cleaning instead of Image Download.
Dispatcher:
currentIdx = Debox = 1
candidateIdx = Cleaning = 5
candidateIdx != currentIdx + 1 (5 != 2)
→ REJECTED. Operator sees error.

Scan after Triage routes to Image Download

Section titled “Scan after Triage routes to Image Download”
Operator submits Cosmetic + Functional evaluations at Triage.
TriageEvaluationService.PromoteBasedOnGrade:
Both scores ≤ 9 → Grade A
→ WorkTracking.StationID = Image Download station (direct write, skips dispatcher)
→ No scan happened, this is a service-driven transition.
Unit physically removed from shop floor.
Windows Test System uploads results:
→ SystemInformationBlob, WindowsTestResultBlob set on WorkTracking
→ SystemInformation + 8 Engineering* rows created
→ WindowsTestResultsUpload row created
→ ImagingAndTestingLog row created
StationID is unchanged — still "Image Download".
Operator scans SN-ABC-001 at MoveFromWindowsTestSystem station.
MoveFromWindowsTestSystemControllerService calls dispatcher:
currentIdx = Image Download = 3
candidateIdx = MoveFromWindowsTestSystem = 4
candidateIdx == currentIdx + 1 ✓
→ StationID updated, scan continues to Cleaning next.

Edge caseBehavior
Unit’s WorkOrder has no RouteID⚠️ Likely fails in ScanUnitControllerService.UpdateWorkTrackingSN step 2. Should be impossible (FK NOT NULL) but worth a sanity check on new WorkOrder creation paths.
RouteFlow.ScanOrder = 0Excluded from the ordered list (> 0 filter). Used for hidden / disabled steps.
Same StationType appears twice in a Route (e.g., Cleaning → Imaging → Cleaning)⚠️ FindIndex returns the first match. The second occurrence would be unreachable. Not believed to happen in practice — confirm before relying on it.
Unit’s current StationType not in the Route⚠️ FindIndex returns -1, so candidateIdx == currentIdx + 1 becomes n == 0 — could allow a scan at the very first station from any unknown state. Subtle bug? Worth confirming with a test.
Two operators scan the same unit simultaneously at different stationsLast-write-wins on WorkTracking.StationID; no row-level lock observed. EF Core concurrency token would help — none currently configured.

🔧 Refactor finding: the FindIndex == -1 edge case is a latent bug worth a regression test. See 20-refactor-findings.md.