---
status: resolved
trigger: "Serial unit lifecycle state not syncing properly when unit exists across multiple cards. Example: BS2H Prototype 002 is on Mark's card (currently assigned) and Tupper's card (archived, not assigned). Lifecycle state shows Returned (matching Tupper's archived card) instead of Delivered (matching Mark's assigned card)."
created: 2026-04-16
updated: 2026-04-16
---

# Debug: Unit Lifecycle State Multi-Card Sync

## Symptoms

- **Expected behavior:** Unit lifecycle state should match the currently-assigned card's state. If unassigned from all cards, state should not be influenced by previously-assigned cards.
- **Actual behavior:** Unit lifecycle state reflects an archived/unassigned card's state instead of the currently-assigned card's state.
- **Error messages:** None visible in stderr or console.
- **Timeline:** Unknown — may have never worked correctly for multi-card units.
- **Reproduction:** Any unit that appears on more than one card shows incorrect lifecycle state.

## Current Focus

- hypothesis: sync_return_states processes units by serial_id from product_refs_json without checking assigned_card_id
- test: build + all 206 lib tests pass
- expecting: unit state reflects assigned card only
- next_action: done
- reasoning_checkpoint: null

## Evidence

- timestamp: 2026-04-16T00:00:00Z
  observation: >
    `sync_return_states` collects serial_ids from `card.product_refs` (which comes from
    `product_refs_json` in SQLite), then in `read_units` closure queries each unit by
    serial_id from SQLite. There was NO filter checking that the unit's `assigned_card_id`
    matches the current card being processed. A unit on Tupper's old card (archived, Returned)
    but now assigned to Mark's card (Delivered) would be found by serial_id lookup and
    incorrectly transitioned via Tupper's closed return → "Returned".
  file: crates/app/src/live_client.rs
  lines: 947-952

- timestamp: 2026-04-16T00:00:00Z
  observation: >
    `upsert_product_unit` uses ON CONFLICT DO UPDATE preserving `assigned_card_id` via COALESCE,
    so once a unit is assigned to a card its assignment persists. The state is also preserved
    locally and not overwritten by sync. This confirms the unit's SQLite row correctly reflects
    Mark's card, but sync_return_states was ignoring that and re-reading the unit for Tupper's
    card's return events.
  file: crates/service/src/db/sqlite.rs
  lines: 1066-1084

## Eliminated

- GH Issue body sync overwriting state: upsert_product_unit preserves state (COALESCE, no state update on conflict)
- cascade_shipment_state_to_units: correctly uses read_units_by_card which filters by assigned_card_id
- enrich_view_with_unit_state: reads from SQLite correctly, not the source of incorrect state

## Resolution

- root_cause: >
    `sync_return_states` in `live_client.rs` read eligible units from `product_refs_json` by
    serial_id without checking `assigned_card_id`. A unit visible on an old card's
    `product_refs_json` (historical reference) would be picked up and transitioned to "Returned"
    when that old card had a closed Shopify return, even though the unit was currently assigned
    to a different card.
- fix: >
    Added `.filter(|u| u.assigned_card_id.as_deref() == Some(card_id.as_str()))` in the
    `read_units` closure inside `sync_return_states`, before the state-eligibility filter.
    This ensures only units currently assigned to the card being processed are affected by
    that card's return lifecycle events.
- verification: cargo build succeeds; all 206 lib tests pass
- files_changed:
    - crates/app/src/live_client.rs (lines 947-957)
