ShopFloor - Workflow Engine
Workflow Engine
Section titled “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.
1. The model in four entities
Section titled “1. The model in four entities”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"| Entity | Role | Key fields |
|---|---|---|
Route | Template defining what StationTypes a unit must visit. Scoped to a ProductLineCategory. | RouteID, ProductLineCategoryID |
RouteFlow | One ordered step inside a Route. | RouteID, StationTypeID, ScanOrder |
ProductionFlow | A physical instantiation of a Route inside a Project. Multiple ProductionFlows can share a Route. | ProductionFlowID, RouteID, ProjectID |
Station | A 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.
2. The scan dispatcher
Section titled “2. The scan dispatcher”All forward motion goes through one method: ScanUnitControllerService.UpdateWorkTrackingSN(sn, st, project), lines 19–156.
Algorithm
Section titled “Algorithm”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.What it enforces
Section titled “What it enforces”- 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.
What it does not enforce
Section titled “What it does not enforce”- No branching. The dispatcher only handles linear forward motion. Repair Center, BER, Quarantine, Harvesting are entered by direct
WorkTracking.StationIDassignment 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.
3. The dispatcher in pictures
Section titled “3. The dispatcher in pictures”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]4. How a WorkOrder gets a Route
Section titled “4. How a WorkOrder gets a Route”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.
5. Where the dispatcher is called
Section titled “5. Where the dispatcher is called”UpdateWorkTrackingSN is the central entry, but most station-specific services call it (or its underlying StationGeneralActionService checks) in addition to their own logic. Examples:
| Service | What it adds beyond the dispatch check |
|---|---|
GradingStationControllerservice | QR parse → grade lookup → set GradingID |
KittingStationControllerService | Accessory scan → KittingLog row → BOM completion check |
PackingStationControllerService | GradingID validation; optional BoxPallet creation |
CleaningStationControllerService | WindowsTestResultsUpload existence check |
PalletStationControllerService | Pallet assignment via PalletServiceFactory |
FinishGoodStationControllerService | Pallet close (IsOpen=false, CloseDate) |
RepairCenterStationControllerService | Direct StationID reassignment (bypasses dispatcher) |
TriageEvaluationService | Direct 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.
6. Branch routing (off-RouteFlow)
Section titled “6. Branch routing (off-RouteFlow)”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.
Examples
Section titled “Examples”| Trigger | Code path | Effect |
|---|---|---|
| Triage routes to Repair Center (Mexicali tenant) | TriageEvaluationService.PromoteBasedOnGrade | WorkTracking.StationID = repairCenterStation.StationID |
| Triage routes to Quarantine (RTV) | TriageEvaluationService.ProcessItemToStation (Quarantine branch) | WorkTracking.StationID = quarantineStation.StationID + Quarantine row + UnitNotes |
| 3 failed repairs → BER | RepairCenterStationControllerService.sendToBER (lines 955–999) | WorkTracking.StationID = berStation.StationID, GradingID = "TYPE BER", LabelSKU updated |
| RMA arrival → Dock reset | RMAControllerService.CreateNewRma (line 198) | WorkTracking.StationID = dockStation.StationID + clear dates and pallets |
| Harvesting consumes BER unit | HarvestingControllerService (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
StationTransitionServicethat encapsulates both routed and branched movements, captures the reason, and writes a unifiedWorkTrackingOperationsaudit row.
7. Audit of movements
Section titled “7. Audit of movements”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:
- EF Core change-tracking writes it automatically — unlikely for an explicit log table.
- A specific service (e.g.,
StationGeneralActionService) writes it on every successful scan — confirm by grep. - 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.
8. Restrictions and gating
Section titled “8. Restrictions and gating”Beyond ordered traversal, a few cross-cutting gates apply:
| Gate | Where | What it does |
|---|---|---|
| ProductLineCategory membership | StationGeneralActionService.IsStationChangeNotRestrictionCompliant | Rejects scans where WorkOrder and Station are in different product-line categories (when strict membership is on). |
| WindowsTestResultsUpload presence | CleaningStationControllerService | Rejects Cleaning advance if the unit lacks imaging upload — defense against units leaking past imaging. |
| GradingID required | PackingStationControllerService (line 186) | Rejects Packing if Project.GradingValidation=true and GradingID is null. |
| Auto-BER on 3 failed repairs | RepairCenterStationControllerService (line 1438) | After 3 failed Repaired=false attempts on the same error code, unit auto-moves to BER. |
| Out-of-route FG | FG_OutOfRoute station (ID 20) | ⚠️ Confirm — likely used when a unit reaches FG via a non-standard path. |
9. ProductionFlow vs. Route
Section titled “9. ProductionFlow vs. Route”A common source of confusion. Both look similar but serve different roles:
| Aspect | Route | ProductionFlow |
|---|---|---|
| Defines | Which StationTypes, in what order | Which physical Stations exist on a line |
| Scoped to | ProductLineCategory | Project |
| Cardinality | 1 Route can be shared by many WorkOrders and many ProductionFlows | Each ProductionFlow belongs to one Project |
| Multiplicity | Many 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
Stationbelongs to the unit’sProductionFlow. 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.
10. Worked example
Section titled “10. Worked example”Concrete walkthrough of a Grade-A laptop scan sequence.
| Entity | Value |
|---|---|
| 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-001 | Currently at Dock (StationTypeID=12) |
Scan #1 — Debox
Section titled “Scan #1 — Debox”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 = nowScan #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.Off-floor at Windows Test System
Section titled “Off-floor at Windows Test System”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 createdStationID is unchanged — still "Image Download".Scan #3 — back into the flow
Section titled “Scan #3 — back into the flow”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.11. Edge cases / what to know
Section titled “11. Edge cases / what to know”| Edge case | Behavior |
|---|---|
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 = 0 | Excluded 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 stations | Last-write-wins on WorkTracking.StationID; no row-level lock observed. EF Core concurrency token would help — none currently configured. |
🔧 Refactor finding: the
FindIndex == -1edge case is a latent bug worth a regression test. See 20-refactor-findings.md.
12. Reading on from here
Section titled “12. Reading on from here”- For what each station does → 12-station-catalog.md
- For the narrative happy path → 02-unit-flow.md
- For the bigger refactor list → 20-refactor-findings.md