---
phase: 12.1.1-shopify-first-card-source-pipeline
plan: 03
subsystem: sync
tags: [shopify, pipeline, matching, unassigned-cards, live-client, rust]

# Dependency graph
requires:
  - phase: 12.1.1-01
    provides: orders_by_tag on ShopifyOrderFulfillmentClient, shopify_profile_url on GithubMappedRecipient, is_unassigned/unassigned_customer_name on RecipientCardSnapshot
  - phase: 12.1.1-02
    provides: HttpShopifyClient orders_by_tag implementation with pagination

provides:
  - extract_customer_id_from_shopify_profile_url helper in live_client.rs
  - Shopify-first run_sync_cycle returning Result<Vec<RecipientCardSnapshot>, String>
  - unassigned_cards field on LiveClient (Arc<Mutex<Vec<RecipientCardSnapshot>>>)
  - fetch_card_snapshots includes both assigned (repo) and unassigned (Shopify-only) cards
  - SyncUpdateCallback receives both assigned and unassigned cards after each sync

affects: [UI rendering of unassigned cards, dashboard card grid, 12.1.1-04]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - Shopify-first sync: orders_by_tag as primary source, GH recipients as lookup table
    - HashMap<customer_id, recipient_idx> keyed by extracted customer ID for O(1) matching
    - Unassigned cards as ephemeral Vec (not persisted to repo) stored in Arc<Mutex<>>
    - GH-only recipients merged via link_recipients fallback after Shopify order pass

key-files:
  created: []
  modified:
    - crates/app/src/live_client.rs

key-decisions:
  - "run_sync_cycle returns Result<Vec<RecipientCardSnapshot>, String> — unassigned snapshots returned to caller rather than stored internally in function"
  - "unassigned_cards stored as Arc<Mutex<Vec<RecipientCardSnapshot>>> on LiveClient for thread-safe sharing between sync thread and event loop"
  - "GH-only recipients (no Shopify order match) still merged via link_recipients fallback to preserve existing recipient data"
  - "Unassigned snapshots use stable recipient_id format: unassigned:order:{order_id}"
  - "First match wins on duplicate customer IDs in customer_id_to_recipient_idx HashMap"
  - "unassigned_cards field added to LiveClient struct (requires updating both new() and new_for_test() constructors)"

patterns-established:
  - "Shopify-first pipeline: fetch orders -> build recipient lookup -> match by customer_id -> unmatched = unassigned"
  - "LiveClient.fetch_card_snapshots: assigned (repo) + unassigned (Arc<Mutex<>>) concatenated on read"
  - "Background thread: run_sync_cycle -> store unassigned -> invoke_from_event_loop with combined snapshots"

requirements-completed: [PIPE-01, PIPE-02, PIPE-03, PIPE-04]

# Metrics
duration: 18min
completed: 2026-03-22
---

# Phase 12.1.1 Plan 03: Shopify-First Sync Pipeline with Recipient Matching Summary

**Shopify-first run_sync_cycle: fetches tagged orders, matches to GH recipients via customer ID from Shopify Profile URL, delivers assigned + unassigned snapshots to SyncUpdateCallback**

## Performance

- **Duration:** 18 min
- **Started:** 2026-03-22T03:02:00Z
- **Completed:** 2026-03-22T03:20:10Z
- **Tasks:** 2
- **Files modified:** 1

## Accomplishments

- Added `extract_customer_id_from_shopify_profile_url` helper with 5 unit tests covering all URL variants and edge cases
- Refactored `run_sync_cycle` to Shopify-first pipeline: GH rows → customer_id HashMap → orders_by_tag("wit-what") → match → assigned + unassigned
- Added `unassigned_cards: Arc<Mutex<Vec<RecipientCardSnapshot>>>` field on `LiveClient` for thread-safe sync result sharing
- `fetch_card_snapshots` now returns assigned (repo-backed) + unassigned (Shopify-only) cards combined
- Background thread stores unassigned from each sync cycle and includes in `invoke_from_event_loop` callback
- All 148 lib tests pass; workspace builds without errors

## Task Commits

1. **Task 1: Customer ID extraction helper (TDD)** - `c7dcf3a` (feat)
2. **Task 2: Shopify-first run_sync_cycle refactor** - `6752bfe` (feat)

## Files Created/Modified

- `crates/app/src/live_client.rs` - Shopify-first pipeline, unassigned_cards field, updated constructors and fetch_card_snapshots

## Decisions Made

- `run_sync_cycle` returns `Result<Vec<RecipientCardSnapshot>, String>` — unassigned snapshots returned to caller for storage in `LiveClient.unassigned_cards`
- `unassigned_cards` stored as `Arc<Mutex<Vec<RecipientCardSnapshot>>>` on `LiveClient` for thread-safe sharing between the background sync thread and the Slint event loop
- GH-only recipients (no Shopify order match) still merged via `link_recipients` fallback to preserve existing recipient data in repo
- Stable recipient_id format for unassigned: `unassigned:order:{order_id}`
- First match wins on duplicate customer IDs in `customer_id_to_recipient_idx` HashMap (logged as design intent)

## Deviations from Plan

None — plan executed exactly as written.

## Issues Encountered

A Slint build error (`Unknown property is-unassigned in RecipientCard`) appeared on first `cargo test -p app --lib` run. Investigation showed this was a cached/stale build artifact — the actual Slint files (`card.slint`, `dashboard.slint`) already had the correct properties from Phase 12.1.1-02. The second compile attempt succeeded. The `cargo build --workspace` binary build revealed a separate issue: `CardData` struct in `main.rs` needed `is_unassigned` and `unassigned_customer_name` fields filled — these were already present in `main.rs` from prior work.

## Next Phase Readiness

- Shopify-first pipeline complete — `run_sync_cycle` now drives from tagged Shopify orders
- Unassigned cards delivered to UI callback alongside assigned cards
- Ready for Plan 04: UI rendering of unassigned card state (pick-recipient flow, visual differentiation)

---
*Phase: 12.1.1-shopify-first-card-source-pipeline*
*Completed: 2026-03-22*
