# Phase 20.2: Sync Integrity and Shipment State Fixes - Context

**Gathered:** 2026-04-13
**Status:** Ready for planning

<domain>
## Phase Boundary

Fix sync pipeline bugs: shipment state derivation from Shopify fulfillments, ephemeral products from lookup modal (missing ww-product GH Issue), ww-card GH Issues not updating on card data changes, and product-aware serial unit state cascading from card shipment state. Verify whether Shopify image fetch and card image display bugs are still present (may have been fixed in Phase 20.1).

</domain>

<decisions>
## Implementation Decisions

### Shipment State Derivation
- **D-01:** Replace the broken order-level `fulfillment_status` mapping (live_client.rs:915-921) with per-fulfillment tracking events via `shopify_projection.rs`. The current mapping incorrectly treats `"fulfilled"` as "Delivered" when it only means "fulfillment records created."
- **D-02:** Card shipment states are: **Preparing** (initial, no fulfillment), **Packing** (label_created tracking event), **In Transit** (in_transit tracking), **Delivered** (delivered tracking), **Return In Transit** (reverse fulfillment in_transit), **Returned** (reverse fulfillment delivered). These replace the old states (Not Shipped, Label Created, etc.).
- **D-03:** SQLite migration renames existing shipment_status values to the new names (Not Shipped→Preparing, Label Created→Packing, etc.). Clean cutover, no legacy strings in DB.

### Serial Unit State Cascade
- **D-04:** When a card's shipment state changes, assigned serial units cascade to matching states. **Serial units do NOT have a "Created" state.** Initial states are: **Available** (unassigned) or **Assigned** (on a card).
- **D-05:** Updated serial unit state machine: Available → Assigned → **Packing (NEW)** → In Transit → Delivered → Return In Transit → Returned → Available.
- **D-06:** Cascade is **product-aware**: only serial units whose Shopify product appears in the relevant fulfillment get the state change, PLUS units with no associated Shopify product (always cascade). Units whose Shopify product is missing from the fulfillment stay at their current state.
- **D-07:** Card state → serial unit state mapping: Preparing→no change (stay Assigned/Available), Packing→Packing, In Transit→In Transit, Delivered→Delivered, Return In Transit→Return In Transit, Returned→Returned.

### Product Creation Durability
- **D-08:** When creating a product from the lookup modal, immediately create the ww-product GH Issue synchronously (same pattern as product catalog view). This prevents the product from vanishing on next sync when stale-product cleanup runs.

### ww-card GH Issue Updates
- **D-09:** When `detect_card_changes` finds a diff (product_refs, shipment_status, shipment_status_date), queue a pending edit to update the existing ww-card GH Issue body. Every detected change triggers an update — keeps cloud of record in sync.

### Image Pipeline (Verify First)
- **D-10:** Card item squares already have image rendering, and Shopify image fetch may already work. Research phase must verify whether these bugs still exist before planning fixes. If already fixed in Phase 20.1, drop from scope.

### Claude's Discretion
- Exact implementation of fulfillment-to-card-state mapping (which Shopify API fields to use for each state)
- How to match serial units to fulfillment line items by Shopify product
- SQLite migration strategy for adding "Packing" to serial unit states
- PendingEdit type for ww-card body updates (new edit_type discriminator)
- Whether reverse fulfillment tracking needs additional API calls or can reuse existing GraphQL client

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Data Architecture
- `.planning/DATA-FLOW.md` — Authoritative reference for all data entities, field sources, sync directions, and agent rules

### Shopify Integration (shipment state)
- `crates/service/src/sync/shopify_projection.rs` — Existing ShipmentProjection with fulfillment/tracking normalization (to be wired into main sync)
- `crates/integrations/src/shopify/http_client.rs` — `fulfillments_for_order()`, `tracking_events_for_order()`, `parse_fulfillments_response()`
- `crates/integrations/src/shopify/graphql_client.rs` — `reverse_fulfillment_tracking()` for return states
- `crates/integrations/src/shopify/order_fulfillment_client.rs` — `Fulfillment`, `TrackingEvent`, `ShopifyOrder` types

### Sync Pipeline
- `crates/app/src/live_client.rs` — Main sync cycle, shipment derivation (lines 914-921, TO BE REPLACED), `detect_card_changes` (line 1448), `sync_card_issues` (line 1340)
- `crates/service/src/db/sqlite.rs` — SqliteStore, pending_edits table, product/serial unit storage
- `crates/app/src/dashboard/pending_edit_flusher.rs` — PendingEditFlusher with exponential backoff

### Serial Unit State
- `crates/app/src/dashboard/assignment.rs` — Unit assignment logic, state transitions
- `crates/integrations/src/github/issues_client.rs` — GH Issues client for ww-card and ww-product operations

### Prior Phase Context
- `.planning/phases/20-offline-mode-hardening/20-CONTEXT.md` — Offline-first architecture (D-03/D-04: local-first writes + queue)
- `.planning/phases/19-serial-instance-tracking-and-start-return/19-CONTEXT.md` — Serial lifecycle, Start Return flow
- `.planning/phases/18-gh-issues-write-back-notes-and-card-cloud-storage/18-CONTEXT.md` — ww-card issue creation, detect_card_changes, PendingEditFlusher

### Code Tips
- `code_tips/` — SQLite gotchas, Slint quirks (must read before modifying code)

### Requirements
- `.planning/REQUIREMENTS.md` — SERIAL-02 (serial states), CARD-05 (shipment status enum)

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **shopify_projection.rs**: Complete `project_shipment_for_customer()` with `normalize_fulfillment_status()` and `normalize_tracking_state()` — needs to be wired into the sync pipeline instead of the broken inline mapping
- **graphql_client.rs**: `reverse_fulfillment_tracking()` already parses reverse fulfillment statuses for return tracking
- **detect_card_changes()**: Already diffs product_refs and shipment_status between old/new cards — needs to also queue GH Issue body updates for existing ww-card issues
- **PendingEditFlusher**: Established pattern for queueing and flushing edits with retry/backoff

### Established Patterns
- **PendingEditRow**: `edit_type` discriminator determines flush behavior — new types needed for ww-card body updates
- **Sync pipeline order**: fetch → upsert → detect_card_changes → sync_card_issues (Step 5b→5c in live_client.rs)
- **ww-product issue creation**: `GhIssuesClient::create_issue()` with "ww-product" label — same pattern for lookup modal products
- **Serial unit state transitions**: State machine in assignment.rs with GH Issue write-back

### Integration Points
- **Fulfillment matching**: Need to match serial units to fulfillment line items via Shopify product ID to implement product-aware cascading
- **SQLite migrations**: New migration for shipment_status rename + "Packing" serial unit state
- **Card-to-unit cascade**: After card shipment state updates in SQLite, iterate assigned units and update matching ones

</code_context>

<specifics>
## Specific Ideas

- User explicitly corrected: serial units do NOT have a "Created" state. Initial states are Available (unassigned) or Assigned (on a card). This is a hard constraint.
- "Packing" is a new state for both cards and serial units — represents label created but not yet in transit
- Product-aware cascade: fulfillment line items contain Shopify product info. Units whose product isn't on the fulfillment are unaffected. Units with no Shopify product association always cascade (fallback).
- User believes Shopify image fetch and card image display may already work — verify before planning fixes.

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope.

</deferred>

---

*Phase: 20.2-sync-integrity-and-shipment-state-fixes*
*Context gathered: 2026-04-13*
