Returns and exchanges

Every unit coming back: courier RTO, customer return, store walk-in, exchange leg · the 4-bucket scan queue, the paired exchange ledger, the IN_TRANSIT_RETURN and LOST_IN_TRANSIT custody types, and the claim table that makes a double deduction a constraint violation rather than a convention

31 units are sitting in QUARANTINE past the 3-day inspection SLA, oldest 19 days, AED 14.2K at retail. They are counted in on_hand and owned_qty and excluded from sellable, so nothing is invented and nothing is lost. Under ops2 today these parcels would have no ledger record at all until somebody dock-scanned them, which for Aramex RTOs has happened 3 times out of 146.
4 exchange pairs are out-only past SLA. The replacement shipped, the original never arrived, and the pair cannot balance. Each has a drift case naming both SKUs. This is the failure mode that produced INV1's 812 ghost exchanges, except here it is a visible queue with a count instead of a warning in a sync log. Drift cases →
Repairs board →

Return queue

scan → receive → inspect → dispose → claim → post · 69 rows today
RefKindReturn AWBSKU QtyOrderCaseReason BucketAnchor keyLegLedger effect AgeStatus
RET-04812Customer return DHL 4548827193 MIN-21591 #MIN10482 CSD-4821size_too_small STOCK csd:4821:88214 return_in +1 QUARANTINE→STOCK @ HQ_319 2dposted
RET-04811Exchange leg DHL 4548827104 MST-CRM-42-BLK1 #MIN10466 CSD-4809size_too_large REPLACEMENT csd:4809:88102 return_in exchange_out +1 @ HQ_319 · pair -1 MST-CRM-44-BLK 3dbalanced
RET-04810Customer return Aramex 77201884 MSR31-071 #MIN10451 CSD-4803quality_fault REPAIR csd:4803:87994 none no stock row · customer owned 4dRPR-00311 →
RET-04809Customer return DHL 4548826998 JAE172 #MIN10440 CSD-4798not_as_pictured STOCK DAMAGE csd:4798:87881 return_in return_in +1 STOCK, +1 WRITTEN_OFF @ HQ_319 4dsplit posted
RET-04808Courier RTO Aramex 77198042 FYN011 #MIN10402 noneaddress_unreachable STOCK ops2:77198042:1841 return_in +1 QUARANTINE→STOCK @ HQ_319 6dposted
RET-04807Store walk-in MCC counter TNS011 #MIN10388 CSD-4791changed_mind STOCK csd:4791:87702 return_in +1 @ MCC not HQ_319 · mismatch flag 1dposted
RET-04806Customer return PorterEx 44104882 PP021 #MIN10371 CSD-4788scent_disliked DAMAGE csd:4788:87655 return_in +1 QUARANTINE→WRITTEN_OFF · opened consumable 5dawaiting finance
RET-04805Replacement not expected LBA011 #MIN10344 CSD-4776damaged_in_transit REPLACEMENT csd:4776:87511 replacement_out -1 STOCK @ HQ_319 · parcel_expected=false 8dposted
RET-04804Courier RTO Swftbox 90218841 CDW011 #MIN10329 CSD-4770damaged_in_transit REPAIR ops2:90218841:2204 return_in +1 held QUARANTINE @ HQ_319 · still owned 9dRPR-00309 →
RET-04803Courier RTO Aramex 77190118 MIN-VAS-0221 #MIN10287 nonenot set unclassified ops2:77190118:1902 unclaimed held QUARANTINE @ HQ_319 · 0 to sellable 19dSLA breach
RET-04802Courier RTO Aramex 77188402 MSR31-112 #MIN10271 nonenot set unclassified ops2:77188402:1877 unclaimed held QUARANTINE @ HQ_319 · 0 to sellable 17dSLA breach
RET-04801Customer return DHL 4548826114 JRY011 #MIN10240 CSD-4744size_too_small in transit csd:4744:87104 reserved IN_TRANSIT_RETURN · no location yet 6dpicked up
RET-04800Customer return DHL 4548825902 GB_MSR311 #MIN10218 CSD-4738gift_unwanted STOCK csd:4738:86991 return_in 3 recipe_transform rows · recipe v4 pinned at sale 7dposted
RET-04799Customer return Transcorp MN-65887058 MST-CRM-38-TAN1 #MIN10188 CSD-4721size_too_large lost csd:4721:86802 never claimed IN_TRANSIT_RETURN→LOST_IN_TRANSIT 23dlost in transit
RET-04798Exchange leg Aramex 77177204 FIS121 #MIN10142 CSD-4702wrong_item_received REPLACEMENT csd:4702:86544 exchange_out in missing -1 FIS14 posted · +1 FIS12 never arrived 28dpair broken
RET-04797Replacement not expected MIN-21591 #MIN10118 CSD-4688missing_from_shipment REPLACEMENT csd:4688:86402 suppressed 0 rows · leg already claimed by csd 14:02:11 11dno double deduct
RET-04796Store walk-in BAS counter LP_MSR31-071 #MIN10094 CSD-4671personalisation_error DAMAGE csd:4671:86188 return_in +1 WRITTEN_OFF @ BAS · no quantity recipe exists 2dunmeasurable SKU
42 of today's 69 dispositions posted a positive stock row. 11 posted nothing because the unit is customer-owned, 9 wrote it off, and 7 were replacement legs of which 1 was suppressed because the leg was already claimed. Under ops2's classify_item the suppressed row would have credited a second time, and under ops2_dispatch._decide the replacement legs would have deducted a second time unless somebody had typed the right Shopify tag.

Paired exchange ledger

An exchange is one event with two legs, never two unrelated postings. The pair reaches balanced only when both legs hold a granted claim, which is enforced by a CHECK, so a half pair can never be marked done.

EXG-00412
In leg · returned
MST-CRM-42-BLK ×1
+1 QUARANTINE→STOCK @ HQ_319 · claim 88214a
Out leg · dispatched
MST-CRM-44-BLK ×1
-1 STOCK @ HQ_319 · AWB DHL 4548827250
csd_claim · 1.00
size swap · same style
balanced
EXG-00411
In leg · returned
MSR31-07 ×1
+1 QUARANTINE→STOCK @ HQ_319 · claim 88102a
Out leg · dispatched
MSR31-08 ×1
-1 STOCK @ HQ_319 · AWB DHL 4548827188
line_gap · 0.90
line_item_id gap 1,842,004
balanced
EXG-00410
In leg · returned
JAE17 ×1
+1 QUARANTINE→STOCK @ MCC · claim 87881a
Out leg · dispatched
JAE19 ×1
-1 STOCK @ MCC · handed over at counter
variant_swap · 0.85
same style group
balanced
EXG-00409
In leg · expected
FIS12 ×1
never arrived · 28d past 14d carrier SLA
Out leg · dispatched
FIS14 ×1
-1 STOCK @ HQ_319 · AWB Aramex 77177290
csd_claim · 1.00
drift case DRF-2214
broken
EXG-00408
In leg · in transit
MSR31-11 ×1
IN_TRANSIT_RETURN · 6d of 14d SLA
Out leg · dispatched
MSR31-13 ×1
-1 STOCK @ HQ_319 · AWB DHL 4548827311
csd_claim · 1.00
within SLA
out only
EXG-00407
In leg · in transit
CDW01 ×1
IN_TRANSIT_RETURN · 4d of 14d SLA
Out leg · dispatched
CDW03 ×1
-1 STOCK @ HQ_325 · AWB Aramex 77201990
order_tag · 0.60
tag beaten by no higher evidence
out only
EXG-00406
In leg · returned
TNS01 ×1
+1 QUARANTINE→STOCK @ YAS · claim 85994a
Out leg · dispatched
TNS03 ×1
-1 STOCK @ YAS · handed over at counter
operator · 1.00
manually paired by store manager
balanced
EXG-00405
In leg · expected
GB_MSR31 ×1
zero-line refund · SKU unknown · DRF-2208
Out leg · dispatched
GB_MSR31 ×1
-1 via 3 recipe_transform rows @ HQ_319
order edit · pending
the 812 shape, now visible
out only

Return reason Pareto · 30 days

1,742 returns · 80% line at reason 5
80% cumulative size_too_small · 468 returns · 26.9% · sizing signal size_too_large · 356 returns · 20.4% · sizing signal not_as_pictured · 244 returns · 14.0% · expectation quality_fault · 188 returns · 10.8% · supplier signal changed_mind · 146 returns · 8.4% · customer damaged_in_transit · 112 returns · 6.4% · logistics wrong_item_received · 84 returns · 4.8% · logistics address_unreachable · 62 returns · 3.6% · logistics other · 82 returns · 4.7% cumulative 26.9% cumulative 47.3% cumulative 61.3% cumulative 72.1% cumulative 80.5% · five reasons cover four fifths of all returns cumulative 86.9% cumulative 91.7% cumulative 95.3% cumulative 100%
sz smallsz largenot picqualityminddamagedwrongaddressother
returns per reason cumulative share 80% crossover 824 of 1,742 carry a sizing signal · feeds the SKU size report

Authorised but not received · aging river

8 weeks · is the pile draining
0 to 3 days: healthy, in flight 4 to 7 days 8 to 14 days: at risk 15 days and over: LOST_IN_TRANSIT candidates total open parcels 78 open 18 open
W-8W-7W-6W-5W-4W-3W-2W-1
0-3d 4-7d 8-14d 15d+ a flat or rising 15d+ band is the signal that returns are being authorised and never arriving

Return rate by location and channel

30 days · denominator printed on every row
8.0% target ONLINE ONLINE · 1,208 returns of 8,842 shipped · 13.7% 13.7% · 1208/8842 MCC MCC · 142 returns of 2,264 sold · 6.3% 6.3% · 142/2264 YAS YAS · 98 returns of 1,864 sold · 5.3% 5.3% · 98/1864 CCZ CCZ · 118 returns of 1,712 sold · 6.9% 6.9% · 118/1712 AJM AJM · 54 returns of 1,338 sold · 4.0% 4.0% · 54/1338 BAS BAS · 62 returns of 1,410 sold · 4.4% 4.4% · 62/1410 HQ_319 HQ_319 · warehouse, sells nothing directly · rate not meaningful n/a · fulfilment only HQ_325 HQ_325 · warehouse · rate not meaningful n/a · fulfilment only
Online is 2.2× the retail average, which is the sizing gap. A store rate is computed on units sold at that store, never on units returned to it, because 6 returns this month arrived at a location that did not ship them.

Disposition mix · 30 days

1,742 lines disposed
STOCK · 1,010 lines · 58.0% REPAIR · 279 lines · 16.0% DAMAGE · 226 lines · 13.0% REPLACEMENT · 157 lines · 9.0% Unclassified · 70 lines · 4.0% · the number that must trend to zero 1,742 lines · 30d
  • STOCK · sellable again58.0%
  • REPAIR · two lanes16.0%
  • DAMAGE · written off13.0%
  • REPLACEMENT · paired9.0%
  • Unclassified · still boxed4.0%
Claims granted
1,819
30d
Suppressed
37
2.0% of attempts
Blocked
14
payload refused
Orphan legs
0
AC-08.2
Double claims
0
AC-08.3

Aging board · authorised, not yet physically received

18 parcels · IN_TRANSIT_RETURN · 2 past SLA
CaseReturn AWBCarrierSKUUnitsCarrier saysPhysically receivedAgeSLABandCustody
CSD-4744DHL 4548826114DHLJRY011In transitno6d14d4-7dIN_TRANSIT_RETURN
CSD-4739DHL 4548826002DHLMSR31-111In transitno6d14d4-7dIN_TRANSIT_RETURN
CSD-4731Aramex 77188114AramexCDW011Picked upno4d14d4-7dIN_TRANSIT_RETURN
CSD-4728PorterEx 44104112PorterExMIN-21592Out for pickupno2d10d0-3dIN_TRANSIT_RETURN
CSD-4722Swftbox 90218114SwftboxTNS011In transitno9d14d8-14dIN_TRANSIT_RETURN
CSD-4721Transcorp MN-65887058TranscorpMST-CRM-38-TAN1no scan 23dno23d14d15d+LOST_IN_TRANSIT
CSD-4718Aramex 77184002AramexFYN011no scan 18dno18d14d15d+LOST_IN_TRANSIT
CSD-4714DHL 4548825114DHLJAE171Delivered backnot yet11d14d8-14dIN_TRANSIT_RETURN
CSD-4709Aramex 77181882AramexMSR31-071Delivered backnot yet10d14d8-14dIN_TRANSIT_RETURN
CSD-4703SHIPA SD029458473SHIPAPP021In transitno3d10d0-3dIN_TRANSIT_RETURN
CSD-4698DHL 4548824772DHLGB_MSR311In transitno5d14d4-7dIN_TRANSIT_RETURN
CSD-4691PorterEx 44103882PorterExLBA011Out for pickupno1d10d0-3dIN_TRANSIT_RETURN
Two parcels crossed their carrier SLA and were promoted to LOST_IN_TRANSIT by the nightly cron, which raised an alert.raise rather than posting to Slack directly. Two more are carrier-confirmed back and still not physically received: the carrier string alone can never credit stock, so they stay in custody until a human or a gate read says otherwise. That single rule is what keeps the 197 false-RTS parcels in ops2 from becoming phantom credits here.

The rule this page enforces

A return may never deduct or credit twice. Every stock movement caused by a reverse event must first claim a (anchor_key, leg) pair, and a partial unique index makes the second claim a database error rather than an application check.
-- the anchor
csd:<case_id>:<line_id>
-- the nine legs
return_in · exchange_out · replacement_out
refund_credit · repair_out · repair_in
scrap · rts_out · credit_note

-- the whole mechanism
UNIQUE (anchor_key, leg)
  WHERE superseded_by IS NULL
The exchange_out leg is claimed by whichever arrives first: the CSD exchange.issued event, the Shopify sale line for the replacement item, or the ops2 dispatch scan. The other two find it claimed, post nothing, and are written to reverse_claim_attempts as suppressed_duplicate so the suppression is a visible number rather than a silent no-op.

Why this is not how it works today

ops2 decides whether a dispatch deducts in iwms/bridges/ops2_dispatch.py::_decide, and that function reads a free-text Shopify order tag. If nobody typed replacement, the replacement deducts a second time. If the first dispatch predates the audit table, the prior-dispatch check misses and it deducts anyway. The tag exchange deducts unconditionally with no paired credit at all. And the decision runs once per parcel, before the item loop, so a mixed AWB is either wholly deducted or wholly excluded.

Meanwhile the return side writes the tag replace_pending, which is not the string the dispatch side reads. The two halves of the mechanism do not share a vocabulary.

That is INV1 scenario 32: 111 orders flagged for "potential double-deduction if re-shipped". i3 replaces the tag with a lookup: ops2 calls GET /api/reverse/claims/<anchor> and i3 answers.

Custody, in order

Receipt and disposition are two separate events. Nothing may do both, which is what keeps a carrier status from becoming a credit.
SOLD→ IN_TRANSIT_RETURN→ QUARANTINE→ STOCK
QUARANTINE→WRITTEN_OFF damage or scrap
IN_TRANSIT_RETURN→LOST_IN_TRANSIT past carrier SLA
A unit in QUARANTINE is present and owned. It counts in on_hand and owned_qty, and it is excluded from sellable. Nothing on this page computes those numbers: they all come from GET /api/inventory/projection.

Known inherited exposure

812 INV1 ghost exchanges: a refund with monetary transactions and zero line items, which the sync loop skips because it cannot resolve a SKU. i3 imports all 812 as open drift cases and runs the pairing detector over them.

393 ops2 return-scan rows never classified, 273 of them 30 to 90 days old. Aramex RTOs have been dock-scanned 3 times out of 146.

197 parcels marked returned that the carrier actually delivered to the customer.

1,205 cross-location attributions on the forward side, with an uncounted reverse twin: ops2's add_back picks a carton by SKU with no location predicate at all.

Return queue

Dock is clear

Every authorised return is either received and disposed, or inside its carrier SLA. Zero units are sitting uninspected and no exchange pair is out-only past SLA.
CSD event feed →

No returns match these filters

6 filters are active including Consumable and Location mismatch. Removing either would show 4 rows.
Claim service unreachable. Dispositions are refused rather than posted unclaimed, because an unclaimed posting is exactly the double-deduct this page exists to prevent. Receiving still works and parcels still land in QUARANTINE.
Service health →

Disposition requires manager

You can view the queue and the aging board. Setting a disposition needs role >= manager plus the reverse_dispose feature flag; scrap needs role >= ops_manager plus reverse_scrap. Store scope comes from your rota-effective locations.
Reverse bridges are paused for a scheduled HMAC key rotation. The previous key stays valid for 60 minutes, so nothing is rejected during the overlap. Receiving and disposition still work locally; CSD and ops2 events queue at the sibling and resume from the ack cursor.
csd bridge
paused
since 04:00 Dubai
ops2 bridge
paused
since 04:00 Dubai
Ack cursor
88,214
resume point
CSD backfill
0d
no outbox at the sibling
Key overlap
60m
prev key still accepted
GATE 2 has held for 4 of 7 consecutive days. Reverse-flow drift is 0.11% against a 0.2% ceiling and every claim identity check returns zero rows. The page is complete and correct; the gate simply has not aged yet. Phase 3 cannot start until day 7.
Days clean
4
of 7
Reverse drift
0.11%
ceiling 0.20%
Identity AC-08.1
0
rows differ
Replay delta
0
new rows on 24h replay
Open drift
6
all explained

Connected to

Repairs board → CSD events → ops2 events → Damages → Incidents → Return to supplier → Drift → Shopify events → All sibling events → Classifier rules → Classifier stats → SKU detail → Locations → Corrections → Approvals → Audit log → Reports →