Skip to content

ShopFloor - Unit Flow

Unit Flow: How a Unit Moves Through the System

Section titled “Unit Flow: How a Unit Moves Through the System”

This is the single most important document for understanding CTLShopFloor. It traces a physical unit (laptop, desktop, monitor) from the moment its serial number is scanned at the receiving dock until it leaves the building — sold, scrapped, or shipped back to a vendor.

Read 03-glossary.md first if any term is unfamiliar.

Items marked ⚠️ Assumption are inferred from code and need confirmation by someone with operational knowledge. Items marked 🔧 Refactor finding are tracked in 20-refactor-findings.md.


1. Mental model: the system in one paragraph

Section titled “1. Mental model: the system in one paragraph”

A unit’s lifecycle is a state machine where the state is WorkTracking.StationID and transitions are gated by RouteFlow. Every unit belongs to a WorkOrder. Every WorkOrder picks a Route. A Route is an ordered list of StationTypes (via RouteFlow.ScanOrder). To move a unit forward, an operator scans its serial number at a physical Station; the dispatcher confirms the scanned station’s type is the next type in the unit’s RouteFlow, and only then updates WorkTracking.StationID. Branches off the happy path (Repair Center, BER, Quarantine, Harvesting) are not in the RouteFlow — they’re explicit station re-assignments triggered by specific business rules (e.g., a failing Triage score, three failed repair attempts, an RTV flag).


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

The flow:

  1. Look up WorkTracking by serial + project.
  2. Look up the unit’s WorkOrder.RouteID.
  3. Pull RouteFlow rows for that Route where ScanOrder > 0, ordered ascending.
  4. Find the unit’s current station’s position (stationofItemIndex) and the scanned station’s position (nextStationIndex) in that ordered list.
  5. Reject unless stationofItemIndex == nextStationIndex - 1. No skipping, no jumping backwards.
  6. Call StationGeneralActionService.IsStationChangeNotRestrictionCompliant() to verify both the unit’s WorkOrder and the target station share a ProductLineCategory (when strict membership is enabled).
  7. Update WorkTracking.StationID and LastScanDate. Save.

Implication: the Route is a strictly-linear linked list per unit. If a station doesn’t apply to a given product (e.g., a monitor doesn’t need Kitting), it must be absent from that Route’s RouteFlow — there is no “skip” mechanism.

🔧 Refactor finding: route-compliance validation is duplicated in 6+ station services instead of living solely in the dispatcher. See 20-refactor-findings.md.


3. The happy path (a Grade A laptop, end to end)

Section titled “3. The happy path (a Grade A laptop, end to end)”

This is the canonical flow. Branches and exceptions are covered in the following sections.

flowchart LR
subgraph Receiving
A[Dock<br/>InboundPallet check-in] --> B[Debox]
B --> C[Sorting]
end
subgraph WIP
C --> D[Triage<br/>Cosmetic + Functional]
D -->|Grade A| E[Image Download]
E -.->|leaves shop floor| F[(Windows Test System<br/>OS install + spec capture + DPK injection)]
F -.->|results uploaded| G[MoveFromWindowsTestSystem]
G --> H[Cleaning]
H --> I[Grading]
end
subgraph PreFGI[Pre-FGI]
I --> J[Kitting<br/>scan accessories]
J --> K[Packing]
K --> L[QA]
end
subgraph FGI
L --> M[Pallet Station]
M --> N[Finished Good<br/>close OutboundPallet]
end
subgraph PostFGI[Post-FGI]
N --> O[Sales Inventory]
O --> P[Pick/Ship]
P --> Q[Sold/Fulfilled]
end
  1. Dock. Truck arrives. Each unit’s serial is scanned against the InboundPallet + pre-alert. A WorkTracking row is created (or, for an in-system RMA, an existing row is reset — see §5). ArrivedSKU and LabelSKU are recorded. Special inbound types (IMC, AS-IS, FLIP) get a GradingID auto-assigned here per DockStationControllerService lines 91–169.
  2. Debox. Units are unpacked from shipping cartons.
  3. Sorting. Units are routed by destination type. ⚠️ Assumption — sorting is primarily decisional; not all routes include a Sorting station.
  1. Triage. Operator administers Cosmetic + Functional evaluations. Both must be submitted. For a Grade A unit (Cosmetic ≤ 9 AND Functional ≤ 9 on the Mexicali tenant), the unit auto-promotes to Image Download. See §4 for the full branch tree.
  2. Image Download. Unit physically leaves the shop floor and is moved to the Windows Test System. The shop-floor station’s purpose is to record WorkTracking.StationID = Image Download so the unit appears in a “waiting for imaging” queue.
  3. Windows Test System. Off-floor. Windows is installed, the Digital Product Key from DigitalProductKey is injected, hardware specs are captured into SystemInformation + SystemInformationDisk + SystemInformationGraphic + the Engineering* tables, and the captured JSON blob is also stored on WorkTracking.SystemInformationBlob. Test results upload via WindowsTestResultsUpload. ImagingAndTestingLog records the operation.
  4. MoveFromWindowsTestSystem. Unit returns to the shop floor; this station re-integrates it into the main flow.

🔧 Refactor finding: the station PromoteImageDownloadUnit does the same job as MoveFromWindowsTestSystem under a misleading name. ⚠️ Assumption — they may be used in different production flows (one for repaired units re-imaging, one for first-time). Confirm before consolidating.

  1. Cleaning. Physical wipe-down. Verifies WindowsTestResultsUpload exists before allowing advance — a defense against units progressing without imaging results.
  2. Grading. Operator scans a QR code that encodes the SKU+grade. The system looks up the matching GradingTypes row, sets WorkTracking.GradingID, optionally SubGradingID.
  1. Kitting. For each accessory listed in the unit’s KittingBOM, the operator scans the barcode. Each scan creates a KittingLog row tying the KeypartsNumberID to the unit. The station blocks advance until all required items are scanned (or an IsException flag is recorded).
  2. Packing. Unit is sealed in its outbound box. If Project.GradingValidation == true, rejects units missing a GradingID. May trigger creation of a BoxPallet (sub-pallet) for multi-box shipments (see SF-220).
  3. QA. Final inspection. ⚠️ Assumption — exact validation rules per project not fully traced.
  1. Pallet Station. Unit is assigned to an OutboundPallet. Pallet identifier is auto-generated from PalletPrefix (e.g., OB000001).
  2. Finished Good. When the pallet is full, the operator closes it: OutboundPallets.IsOpen=false, CloseDate set. The unit is now FGI and visible to sales.
  1. Sales Inventory. Pallet/units are available for sale. A SalesImport job ([14-jobs-and-integrations.md] — TBD) reconciles sales data.
  2. Pick / Ship. UnitShippedDate is set on WorkTracking.
  3. Sold / Fulfilled. UnitSoldDate is set. SoldFulfilledOrScrapedLog records the terminal disposition.

Triage is the first major branching point. The logic lives in TriageEvaluationService.cs and is branched by tenant — a significant smell flagged below.

  • A serial number scanned at a Triage station.
  • ⚠️ Assumption — the trigger gate is WorkTracking.StationTypeID == Deboxing(10) per TriageEvaluationController.cs:53. Confirm whether Sorting is also a valid predecessor.
  • Operator submits both evaluations (Cosmetic and Functional). The system requires latestScores.Count == 2 before routing.

Each evaluation is scored 0–100. A worse-than-threshold grade or any option flagged autoFail=true triggers a failure. Score thresholds map to A/B/C/D grades via TriageEvaluationGradesScore. ⚠️ The mapping is per-evaluation-type and stored in the DB, not in code — confirm current thresholds against the live data.

Tenant: Dallas SL (CanBeProccesed method, lines 271–371)

Section titled “Tenant: Dallas SL (CanBeProccesed method, lines 271–371)”

This tenant uses interactive routing — the worker is offered destinations.

CosmeticFunctionalRTV flag set?Offered destination(s)
≥ 46anyyes (any option)Quarantine (primary) or Repair Center
≥ 46anynoRepair Center only
≤ 45≤ 9IneligibleCanBeProccesed=false. ⚠️ No automatic destination; unit appears to stay at Triage.
≤ 45> 9yesRepair Center or Quarantine (RTV optional)
≤ 45> 9noRepair Center or Quarantine

Tenant: Mexicali (default) (PromoteBasedOnGrade method, lines 374–512)

Section titled “Tenant: Mexicali (default) (PromoteBasedOnGrade method, lines 374–512)”

This tenant uses automatic routing.

CosmeticFunctionalAuto destinationWhat’s written
≤ 9 (A)≤ 9 (A)Image DownloadWorkTracking.StationID updated
any other combinationRepair CenterError codes extracted from both evaluations; TriageEvaluationSubmission flags needRepair, needPaint, needRelam per matched codes; TriageErrorRecords row per error; RepairCenter row created
EntityWhenPurpose
TriageEvaluationSubmissionEach evaluation submittedFinal score, grade letter, serialized Q&A JSON, auto-fail flag, needRepair/needPaint/needRelam flags
TriageErrorRecordsRouting to Repair CenterPer-error: SN, SKU, model, BOL, error name, user, timestamp, KeypartCategoryErrorCodeID
QuarantineRouting to QuarantineSN, error type, error code, failure description, timestamp
UnitNotesRouting to QuarantineNarrative “Failed from Triage for the next issues: …”
WorkTrackingFinal promotionUpdated StationID, LastScanDate
  • 🔧 Hardcoded tenant branch at line 294 — if (tenantName == "Dallas SL"). The author left a comment: //<-- HATE THIS HARDCODED THING.
  • 🔧 Hard-indexed evaluation order (lines 401–404) — relies on the alphabetical sort of evaluation names putting “Cosmetic” first, “Functional” second.
  • 🔧 Magic score thresholds (46, 45, 9) with no configuration table or constants.
  • 🔧 Three split entry pointsCanBeProccesed, PromoteBasedOnGrade, ProcessItemToStation — with no unified orchestrator.
  • 🔧 Serialized JSON as source of truth — questionnaire responses are stored as a JSON blob in SerializedSelections, deserialized in-memory for decision logic. No queryable normalized table.
  • 🔧 Inconsistent auto-fail logic — functional triggers on substring “Not Functional” or the autoFail flag; cosmetic only on the flag.

A returned unit enters through one of two paths depending on whether it’s already in the database.

The returned unit was previously shipped from this facility, so its serial exists in WorkTracking.

flowchart LR
A[Customer return arrives] --> B[Operator creates<br/>RmaHeader + RmaItemsRelation<br/>RMAControllerService.CreateNewRma]
B --> C[WorkTracking reset:<br/>StationID = Dock<br/>ReceivedDate, GradingID,<br/>OutboundPalletID, ItemTypeID,<br/>SoldDate, ShippedDate cleared]
C --> D[Unit re-enters normal route<br/>starting from Dock]

Triggered via RMAControllerService.CreateNewRma() (lines 107–113, 184, 198). Requires the unit’s current StationID to be in FG, Shipped, Sold, or Absent state (gated at lines 168–172).

The returned unit’s serial is not in WorkTracking — typical for units shipped by a different facility/program.

flowchart LR
A[Customer pre-alert<br/>Excel uploaded] --> B[Rows staged in<br/>RmaNotInsystemPreAlert]
B --> C{Validation<br/>RmaForOutOfSystemUnitsControllerService.ProcessFile}
C -->|valid| D[GenerateRMA:<br/>New WorkOrder IsRMA=true<br/>+ New WorkTracking per unit<br/>+ RmaHeader + RmaItemsRelation]
C -->|already in system| E[Treated as in-system path:<br/>existing WorkTracking reset to Dock]
D --> F[Units enter at Dock]
E --> F

If the ArrivedSKU references a model not yet in BOMHeader, the system auto-creates a placeholder BOMHeader and emails admins via SendNewBomHeaderItemEmailNotification(). ⚠️ Errors here are silently swallowed (lines 451–454).

What happens at the unit level on RMA receipt

Section titled “What happens at the unit level on RMA receipt”

Whether in-system or out-of-system, the converging behavior is:

  • WorkTracking.StationID is set to Dock.
  • The following fields are cleared: ReceivedDate, GradingID, OutboundPalletID, ItemTypeID, UnitSoldDate, UnitShippedDate, SentToSalesDate.
  • LabelSKU is preserved (carries the original outbound label).
  • The unit re-enters the normal Route from the start. There is no “RMA-specific” route — the same Route is reused, gated by WorkOrder.RouteID.

⚠️ Important quirk: RmaHeader / RmaItemsRelation do not have a foreign key to RepairCenter. The linkage is implicit — when the RMA’d unit re-enters the flow and later reaches Repair Center, the join is on WorkTracking.SerialNumber == RepairCenter.SerialNumber (a string match).

🔧 Refactor finding: no enforceable referential integrity between RMA and the subsequent repair record. Recommend adding RepairCenter.RmaItemsRelationID or routing repair lookups through WorkTrackingID.

  • 🔧 No RmaOutcome field on WorkTracking — there’s no way to tell whether the RMA was successfully re-processed or returned a second time.
  • 🔧 Reset logic duplicated across CreateNewRma, AddItemToExistingRMA, and the out-of-system GenerateRMA old-path branch.
  • 🔧 Magic station IDs like item.StationID.Equals(21) instead of using StationTypeEnum.
  • 🔧 RmaNotInsystemPreAlert.IsValid can be true while ErrorDescription != "OK" — ambiguous semantics.
  • 🔧 Nothing prevents one unit being attached to multiple RmaHeaders simultaneously.

6. Repair Center flow (with purchased + harvested parts)

Section titled “6. Repair Center flow (with purchased + harvested parts)”

The Repair Center is the operational hub for everything that fails the happy path — Triage failures, in-flow defects, RMA returns, and re-test failures. It is also the destination for inventory in the form of purchased parts (PO receipts) and harvested parts (from cannibalized BER units), and the source of consumption against that inventory.

6.1. What happens at the Repair Center station

Section titled “6.1. What happens at the Repair Center station”
flowchart TD
A[Unit arrives at RC<br/>scanned in via SearchSN] --> B[Technician documents errors]
B --> C[Errors written to:<br/>RepairCenterError + RepairCenter]
C --> D{Repair attempt}
D -->|Repaired = true| E[RepairDate, RepairDescription,<br/>RepairTechUser, ReplacementParts set<br/>RepairCenterError.Status = 'Issue Fixed']
D -->|Repaired = false| F[Unit stays at RC for retry]
E --> G[WorkTracking.ErrorFlag<br/>recalculated]
G -->|no unresolved errors| H[Unit re-enters route<br/>⚠️ exact re-entry point<br/>not enforced in code]
F --> I{RepairedErrorCount<br/>>= 3 on same error?}
I -->|yes| J[Auto-send to BER<br/>sendToBER lines 955-999]
I -->|no| D
J --> K[Grading = TYPE BER<br/>LabelSKU updated<br/>StationID = BER station]
K --> L[Eligible for Harvesting]

Key implementation references:

  • RepairCenterStationControllerService.cs lines 339–665 (intake), 1418–1452 (error creation), 1578–1802 (addError repair recording), 1697–1705 (ErrorFlag recalculation), 955–999 (sendToBER).
  • RepairCenter.RepairedErrorCount increments per re-repair attempt; 3 strikes = BER (line 1438).
  • RepairCenterError.Status uses magic strings like "PENDING", "Issue Fixed" — not an enum.

⚠️ Genuinely unclear from the code. When the technician marks the unit Repaired=true, WorkTracking.ErrorFlag is cleared but there is no explicit code that returns the unit to a specific station in the Route. The implication is that the unit is physically walked back to the appropriate station and the operator there re-scans it — but the scan dispatcher (§2) enforces strict sequential ordering and there’s no special-case bypass for “returning from RC.”

Possibilities, all needing confirmation:

  1. RC is itself a RouteFlow entry in some Routes, so the unit naturally proceeds to the next station on its next scan.
  2. The operator manually overrides via ProcessItemToStation (which exists in TriageEvaluationService and may have a sibling in RC).
  3. The unit’s StationID is reset to a specific predecessor station (e.g., Imaging) by some service method I haven’t traced.

🔧 Refactor finding (high priority): the post-repair re-entry path is undocumented and likely fragile. Worth investigating with operators before any refactor.

6.3. Purchased parts: from PO to repair-ready inventory

Section titled “6.3. Purchased parts: from PO to repair-ready inventory”
flowchart LR
A[PurchaseOrder created<br/>status DRAFT] --> B[PO approved<br/>status APPROVED]
B --> C[Parts arrive<br/>ReceivePurchaseOrderAsync]
C --> D[For each PurchaseOrderItem<br/>with FulfilledQuantity > 0]
D --> E[CreateParts: Part rows created<br/>Source = 'PO'<br/>PurchaseOrderItemID FK]
E --> F[InventoryTransaction<br/>TransactionType = 'IN']
F --> G[InventoryTracker<br/>AvailableQuantity updated]
G --> H{High value?}
H -->|yes| I[Each Part gets unique<br/>PartSerialNumber<br/>format P-yyMMdd-A00000]
H -->|no| J[Aggregate quantity tracking]

Key implementation references in PurchaseOrderControllerService.cs: lines 284–358 (PO creation), 827 (receipt), 940 (CreateParts), 1637–1678 (high-value branch), 1680–1716 (standard branch), 671–821 (GetAvailableKeypart).

Note: PendingPartsCatalog despite its name is not a list of parts pending procurement. It’s a BOMHeader-linked reference of parts associated with a product family. Naming is misleading — see glossary and 🔧 refactor findings.

6.4. Harvested parts: from BER unit to repair-ready inventory

Section titled “6.4. Harvested parts: from BER unit to repair-ready inventory”
flowchart LR
A[BER unit at Harvesting station] --> B[Technician CheckSerialNumberAsync<br/>validates eligibility<br/>RC or BER grading]
B --> C[HarvestingEvent created]
C --> D[Technician lists extracted parts:<br/>KeypartNumberID, Quantity, NeedsRepairing]
D --> E[CreateParts: Part rows created<br/>Source = 'Harvesting'<br/>HarvestingEventID FK]
E --> F[InventoryTransaction TransactionType = 'IN']
F --> G[InventoryTracker AvailableQuantity updated]
G --> H{High value?}
H -->|yes| I[Unique PartSerialNumber<br/>format T-yyMMdd-A00000]
H -->|no| J[Aggregate quantity tracking]

Key implementation references in HarvestingControllerService.cs: lines 65 (eligibility), 137–445 (overall flow), 158–170 (component extraction), 175 (CreateParts invocation), 356–395 (high-value branch).

Distinct serial prefix: purchased parts get P{yyMMdd}A...; harvested parts get T{yyMMdd}A.... ⚠️ Assumption — the “T” likely stands for “Take/Tear-down”. This is useful provenance.

Condition flag: HarvestingEvent.NeedsRepairing lets a harvested part be flagged as “needs to be reconditioned before use.” Repair Center availability lists filter on this flag (see RepairCenterStationControllerService lines 792–794).

⚠️ Unresolved. RecoveryEvent exists alongside HarvestingEvent with similar shape, but RecoveryEvent ties to a WarehouseID instead of a unit. Hypotheses to confirm:

  • RecoveryEvent is a legacy entity superseded by HarvestingEvent.
  • RecoveryEvent represents warehouse-level manual stock-in (parts found loose, parts received without a PO).
  • RecoveryEvent is a planned-but-incomplete feature.

This needs a definitive answer before any refactor touches either entity. See 20-refactor-findings.md.

When a part is used in a repair, two entities track it — but the actual inventory write-down appears to be incomplete.

EntityWhat it doesStatus
RCReplacementPartsRecords that part X was used in repair Y. Fields: RepairID, HarvastingSN (sic, string not FK), PartNumberExtracted, PartNumberForReparation.✅ Recorded
ConsumptionRequest + ConsumptionRequestItemsA request to consume specific parts from inventory. Status APPROVED exists.⚠️ Created by PurchaseOrderControllerService.CreateConsumptionForRepairCenter() but the inventory write-down (InventoryTracker.AvailableQuantity decrement, Part.ConsumedByWorkTrackingID set) is not visibly wired up.
InventoryTransaction with TransactionType="OUT"The proper ledger entry for consumption.⚠️ Not observed being created on repair-part consumption.

🔧 Refactor finding (high priority — inventory integrity): the visible code creates a ConsumptionRequest and records RCReplacementParts but never decrements available inventory. GetAvailableKeypart() (line 686) filters Part rows on ConsumedByWorkTrackingID == null, implying parts should be marked consumed, but no service method writing that field was found. Either this logic lives in a code path I haven’t traced (a Hangfire job? a database trigger?) or it is a real bug that has been masked by manual inventory adjustments. Confirm before refactor.

6.7. BER and Harvesting as the “graveyard” path

Section titled “6.7. BER and Harvesting as the “graveyard” path”
flowchart LR
A[Unit at Repair Center] -->|3 failed repair attempts| B[Auto-BER<br/>sendToBER]
B --> C[Unit at BER station<br/>GradingTypes = TYPE BER<br/>LabelSKU marked]
C --> D{Salvageable?}
D -->|yes| E[Harvesting station]
D -->|no| F[Scrap / disposal<br/>outside this system]
E --> G[Parts extracted<br/>added to inventory]
G --> H[Remaining hulk → Scrap]

BER is not a separate Route — it’s an explicit station re-assignment. The unit’s lifecycle effectively ends at BER unless Harvesting extracts value from it.

⚠️ Assumption — once a unit hits BER, it never re-enters the happy path. No code path was found that would re-grade a BER unit back to a sellable state.


BranchTriggerWhat writes itTerminal?
QuarantineRTV flag in Triage, suspicious SKU, manual holdQuarantine row + UnitNotesNo — can be released back to flow
Repair CenterTriage failure, in-flow failure, RMA receiptRepairCenter + RepairCenterErrorNo — Repaired=true returns unit to flow
BER3 failed repairs on same error codeWorkTracking.GradingID = TYPE BER, LabelSKU updated⚠️ Effectively yes
HarvestingBER unit deemed salvageableHarvestingEvent + Part rowsYes — donor unit is consumed
As-Is ShipmentsSpecial inbound typeAuto-graded AS-IS at DockPath leads to a different outbound treatment ⚠️ details TBD
Internal TransferMovement between facilitiesInternalTransfer + InternalTransferPallet + InternalTransferSerialNo — unit re-emerges at destination
ScrapHand-off to physical recyclingSoldFulfilledOrScrapedLogYes

These are the things I could not answer from the code alone. Each one materially affects refactor planning.

  1. Triage → Imaging gating: is StationTypeID == Deboxing(10) really the only valid predecessor to Triage? Where does Sorting fit?
  2. Post-repair re-entry: how does a Repaired=true unit actually return to the happy path? Operator-driven scan? Special service method? See §6.2.
  3. RecoveryEvent vs. HarvestingEvent: what is the operational distinction? See §6.5.
  4. Inventory write-down on consumption: where is Part.ConsumedByWorkTrackingID set, and where is InventoryTracker.AvailableQuantity decremented? See §6.6.
  5. IMC / FLIP grades: what do these mean operationally and what’s different in the route for these units?
  6. AS-IS path: does an AS-IS unit skip Cleaning/Grading entirely, or just visit modified versions of them? Where in the Route does the divergence happen?
  7. PromoteImageDownloadUnit vs. MoveFromWindowsTestSystem: are these genuinely two stations with two purposes, or duplication?
  8. DPK injection trigger: when is WorkOrders.DoesBolNeedDigitalKeyInjection set, and by what (UI, API, upload)?
  9. Pallet Station vs. Finished Good: is Pallet Station always present, or only for projects that batch units onto pallets vs. ship individually?
  10. Pre-FGI vs FGI boundary: at exactly which station does StationCategory transition from 9 (Pre-FGI) to 10 (FGI)? Implementation suggests “when pallet closes” — confirm.