Repairs

Two lanes that must never mix. The customer lane holds the customer's own item, which is not company stock and may never enter a balance. The stock lane holds company stock pulled from QUARANTINE or DISPLAY_DAMAGED, which stays owned and stays counted. The page is split, not filtered, because a shared table with a lane column is exactly the affordance that lets somebody pick the wrong one.

2 tickets have reached round trip 3. A fourth send is refused by the API and by the round_trips <= 3 CHECK, matching the guard CSD already enforces in repair-lifecycle.ts. Both need a replace or scrap decision today; RPR-00298 is 31 days old. Decide →
5 tickets are past their vendor SLA across 3 vendors, AED 4,210 of stock-lane value held at a vendor and still counted in owned_qty. LC Straps is the worst at a 21-day median against a 14-day SLA. Vendor scorecard →
← Return queue
customer lane

The customer's own item

14 open · 0 units in any balance
never sellable · never restockable
TicketCaseSKUSerialVendorIssueAgeDueRTStateStock effect
RPR-00311CSD-4803MSR31-07 E280…218F3CLC StrapsCrown seized 6d08d left at vendornone · customer owned
RPR-00310CSD-4798MSR31-01 E280…21A991Bench 319Strap pin loose 3d11d left in repairnone · customer owned
RPR-00308CSD-4776LBA01 E280…225A19Bench 319Handle stitching 24d10d over overduenone · customer owned
RPR-00306CSD-4770FYN01 E280…22C201Bench 319Clasp replacement 2d12d left re-QC passnone · returns to customer
RPR-00303CSD-4744JAE17 E280…22B740VJ JewelsStone reset 12d2d left at vendornone · customer owned
RPR-00301CSD-4738TNS01 E280…22A118Bench 319Lens scratch 5d9d left in repairnone · customer owned
RPR-00299CSD-4721MST-CRM-42-BLK E280…229C04ORO LeatherEdge paint lifting 15d1d over overduenone · customer owned
RPR-00298CSD-4702MIN-CHR-001 E280…227E11LC StrapsFrame crack 31d17d over RT3 · no 4thnone · replace or scrap
RPR-00295CSD-4688JRY01 E280…226044VJ JewelsClasp fatigue 9d5d left at vendornone · customer owned
RPR-00292CSD-4671FIS12 E280…224F80VJ JewelsUnrepairable 18d4d over replaced-1 FIS12 @ HQ_319 · replacement_out
RPR-00289CSD-4655MSR31-11 E280…223C22Bench 319Battery 4dclosed returnednone · handed back to owner
RPR-00286CSD-4641CDW03 E280…222188LC StrapsBuckle replacement 7d7d left at vendornone · customer owned
RPR-00284CSD-4629LP_MSR31-07 not taggedBench 319Engraving redo 2d12d left in repairnone · customer owned
RPR-00281CSD-4614MST-CRM-44-BLK E280…220994ORO LeatherZip failure 22d8d over escalated L3none · offer refund or replace
Every row in this lane shows none in the stock effect column, and that is not a display convention. A repair_tickets row with lane='customer' cannot name a holding location or a holding stock type, cannot close with outcome restocked, and no repair_events row for it may carry a ledger_entry_id. Three CHECK constraints and one trigger. The only ledger row this lane can ever produce is a replacement_out when the repair fails.
stock lane

Company stock, held while repaired

9 open · 16 units held
QUARANTINE or DISPLAY_DAMAGED · still owned
TicketSourceSKUQtyHeld atStock typeVendorAgeRTStateLedger effect
RPR-00309INF-2318 QC PP026HQ_319 QUARANTINEORO Leather 9d at supplier6 held · owned_qty 6
RPR-00307IWMS QC reject JRY013HQ_319 QUARANTINEVJ Jewels 14d at supplier3 held · owned_qty 3
RPR-00305IWMS QC reject TNS012HQ_319 QUARANTINEBench 319 1d re-QC pass+2 QUARANTINE→STOCK queued
RPR-00304RET-04804 CDW011HQ_319 QUARANTINELC Straps 9d at vendor1 held · owned_qty 1
RPR-00302Display damage MSR31-081MCC DISPLAY_DAMAGEDBench 319 6d in repair1 held at MCC · not sellable
RPR-00300Count SC-1142 FYN011YAS DISPLAY_DAMAGEDBench 319 4d in repair1 held at YAS · not sellable
RPR-00297Supplier defect MST-CRM-38-TAN4HQ_325 QUARANTINEORO Leather 21d overdue4 held · RTS candidate
RPR-00294IWMS QC reject JAE172HQ_319 QUARANTINEVJ Jewels 17d RT3 · no 4thscrap decision · -2 WRITTEN_OFF
RPR-00291IWMS QC reject MIN-21593HQ_319 QUARANTINEBench 319 5d restocked+3 QUARANTINE→STOCK @ HQ_319
RPR-00288Supplier defect PP0212HQ_319 QUARANTINEORO Leather 26d to RTS-12 rts_out · RTS-8821
RPR-00285Display damage TNS031CCZ DISPLAY_DAMAGEDBench 319 3d re-QC pass+1 DISPLAY_DAMAGED→DISPLAY_GOOD
RPR-00283INF-2302 QC GB_MSR311HQ_319 QUARANTINEBench 319 8d in repair3 component rows held · parent balance 0
Every row in this lane names a location and a stock type, and that is also a constraint: lane='stock' requires both to be non-null, so a company unit at a vendor can never go missing from the balance sheet the way it does in ops2 today. In ops2 a repair-bucket classification writes one retail_operations row with from_location_id NULL and no inventory movement in either direction, so both lanes look identical and neither is counted.

Vendor turnaround against SLA · 90 days

median days out · bar past the tick is a breach
LC Straps LC Straps · median 21d against a 14d SLA · 18 tickets · 5 breached SLA 14 days 21d · SLA 14d · 18 tix ORO Leather ORO Leather · median 18d against a 14d SLA · 22 tickets · 4 breached SLA 14 days 18d · SLA 14d · 22 tix VJ Jewels VJ Jewels · median 13d against a 14d SLA · 16 tickets · 1 breached SLA 14 days 13d · SLA 14d · 16 tix Bench 319 Bench 319 · internal · median 5d against a 7d SLA · 44 tickets · 0 breached SLA 7 days, internal 5d · SLA 7d · 44 tix · internal Bench YAS Bench YAS · internal · median 4d against a 7d SLA · 9 tickets · 0 breached SLA 7 days, internal 4d · SLA 7d · 9 tix · internal 0d 7d 14d 21d 28d
within SLA median past SLA the black tick is each vendor's own repair_vendors.sla_days, not one global number

Round-trip funnel · 90 days

109 tickets · what a third round resolves to
Round 1 · 109 tickets sent 109 round 1 82 closed after round 1 · 75% 82 closed · 75% Round 2 · 27 tickets returned to a vendor 27 round 2 19 closed after round 2 · 70% 19 closed · 70% Round 3 · 8 tickets · a fourth send is refused 8 round 3 · no 4th 3 closed after round 3 3 fixed 3 replaced · a replacement_out claim 3 repl 2 scrapped · written off with finance approval 2 scrap 25% 30%
round 1round 2round 3 · terminal
closed fixed replaced scrapped a fourth send is refused by CHECK, not by policy

Vendor loop

5 vendors · 2 internal benches · AED 8.4K spend 30d
LC Straps
18 tix · 21d med · AED 2,940
ORO Leather
22 tix · 18d med · AED 3,120
VJ Jewels
16 tix · 13d med · AED 1,480
Bench 319
44 tix · 5d med · AED 620
Bench YAS
9 tix · 4d med · AED 240

RPR-00305 · stock lane · TNS01 ×2

every hop names the ledger row it did or did not write
Opened from IWMS QC reject21-07 09:14
2 units failed the local QC bucket repair_local. Ledger: STOCK -2 → QUARANTINE +2 @ HQ_319, claim repair_out. Still owned, still in on_hand.
Triaged to Bench 31921-07 11:02
Internal vendor, so no custody transfer and no location change. Ledger: no row, correctly.
Sent to vendor21-07 14:30
Round trip 1 of a maximum 3. Due 28-07 from repair_vendors.sla_days = 7. Ledger: no row, an internal bench does not move stock.
Vendor returned · re-QC opened28-07 10:44
6 days out, inside SLA. A repair_qc_checks row is now required before anything can restock.
Re-QC PASS round 128-07 15:12
Inspector Subesh · 4 photos · checklist 9 of 9. The pass is recorded against round trip 1; a pass from an earlier round could not authorise this restock.
Restock queued28-07 15:13
Ledger: QUARANTINE -2 → STOCK +2 @ HQ_319, claim repair_in. The two units re-enter sellable for the first time in 8 days.

The lane invariant, in the schema

CHECK (lane <> 'customer' OR
  (holding_location_id IS NULL
   AND holding_stock_type_id IS NULL))

CHECK (lane <> 'stock' OR
  (holding_location_id IS NOT NULL
   AND holding_stock_type_id IS NOT NULL))

CHECK (lane <> 'customer' OR
  outcome NOT IN ('restocked','rts'))

CHECK (round_trips <= 3)
A customer's item cannot enter a balance because the columns that would put it there cannot be populated. A company unit cannot fall out of a balance because the same columns cannot be null. Neither is a policy anybody has to remember.

Why the split is a page layout, not a filter

In ops2 today the returns-scan repair bucket calls one dispatcher, _dispatch_repair, which inserts a single retail_operations row with operation_type='repair', direction='to_wh' and from_location_id NULL. There is no lane field, no owner flag, and no inventory movement in either direction.

So a customer's own watch and a company carton of six defective bottles produce identical rows, and neither is counted. The dangerous half is that the operator's dropdown also offers stock: choosing it for a customer-owned item calls add_back, which credits a unit the company does not own into a carton and makes it sellable. There is no barrier of any kind between those two menu items.

i3 answers with three CHECKs, a trigger on repair_events.ledger_entry_id, and this page, which never puts the two lanes in the same table.

QC 5-bucket and where each lands

accept→ STOCK
reject_local→ stock lane · Bench 319
reject_supplier→ RTS · rts_out claim
repair_local→ stock lane · internal
repair_supplier→ stock lane · external vendor
All five come from the inbound QC path (module 06) and all five land in the stock lane. The customer lane can only be reached from a CSD case, a return-scan disposition or a store walk-in. Nothing in the inbound pipeline can ever create a customer-lane ticket.

Escalation ladder

Day 7: ping the vendor, escalation_level = 1.
Day 14: escalate to the ops owner, level 2.
Day 21: offer the customer a replacement or a refund, level 3.

The ladder is CSD's, taken verbatim from its PRD so the two systems escalate on the same day rather than on two different clocks. i3 raises alert.raise on the internal bus at each step and never posts to Slack itself; module 01 owns dedupe as a database constraint.
customer lane

Loading

stock lane

Loading

No open repairs in either lane

Zero customer items at a vendor and zero company units held in QUARANTINE for repair. Closed tickets from the last 30 days are still available.
Return queue →

No tickets match these filters

Filtering to Round trip 2+ and Internal bench together returns nothing: the internal benches have never needed a second round.
Ticket write refused: lane constraint violated. The request carried lane='customer' together with holding_location_id='HQ_319'. The database rejected it, which is the intended behaviour: a customer-owned item may never name a holding location. Nothing was written and nothing was partially written.
ERROR: new row for relation "repair_tickets" violates check constraint "ck_rt_customer_never_stock"
DETAIL: Failing row contains (00312, customer, ..., HQ_319, QUARANTINE, ...)
Source event →

Scrap requires ops manager

You can view both lanes and record a re-QC result. Sending to a vendor needs role >= manager plus reverse_dispose; scrapping a stock-lane ticket needs role >= ops_manager plus reverse_scrap, and above AED 500 it also needs a finance signature.
Vendor portal sync is paused for maintenance. Vendor acknowledgements will not land until 06:00 Dubai. Local actions (open, triage, re-QC, close) all still work and will push on resume from the ack cursor.
GATE 2 has held for 4 of 7 consecutive days. Acceptance check AC-08.6 returns zero rows: no customer-lane ticket names a holding location, no customer-lane event carries a ledger row, and no unit in any balance traces to a customer-lane ticket. The page is correct; the gate has not aged.

Connected to

Return queue → Return to supplier → Damages → Incidents → GRN and QC → Suppliers → Vendor detail → CSD events → ops2 events → Locations → Stock count → SKU detail → Approvals → Unit events → Reports → Audit log →