# Phase 12.1.1: Shopify-First Card Source Pipeline — Context

**Gathered:** 2026-03-21
**Status:** Ready for planning

<domain>
## Phase Boundary

Refactor the data pipeline so Shopify orders tagged "wit-what" are the sole card source (replacing the current GitHub-Project-first flow). Match order customers to GH Project recipients via their "Shopify Profile URL" field. Show unmatched orders as "Unassigned" cards with visual warning treatment. Provide a "Pick Recipient" assignment flow with searchable modal, GH Project write-back, and "Create New Recipient" option.

</domain>

<decisions>
## Implementation Decisions

### Order Fetching Strategy
- New Shopify API endpoint: `GET /admin/api/2024-01/orders.json?status=any&tag=wit-what&limit=250`
- Must support pagination (Link header / page_info cursor) for >250 orders
- Three sync modes:
  - **5-minute auto-sync**: since-based incremental (`updated_at_min={last_sync_timestamp}`). Merge updates into existing card set
  - **Manual refresh** (button/F5): full re-fetch of all tagged orders
  - **Startup/runtime sync**: full re-fetch of all tagged orders
- One card per order (not per customer). A customer with 3 tagged orders gets 3 separate cards

### Recipient Matching Logic
- Parse Shopify customer ID from the GH Project recipient's "Shopify Profile URL" field (last path segment of `https://admin.shopify.com/store/{slug}/customers/{id}`)
- Match the parsed customer ID against `order.customer.id` from the Shopify order
- **Research needed**: verify that the numeric customer ID in the admin URL matches the REST API customer.id. If they differ, fallback to API lookup
- First match wins when multiple recipients have the same customer ID (log warning about duplicates)
- Recipients without a Shopify Profile URL are not considered for auto-matching

### Unassigned Card UX
- Unassigned cards look identical to assigned cards — same layout, same data fields
- Name field shows the Shopify customer first+last name in **italics + warning color**
- No order number displayed on cards (neither assigned nor unassigned)
- Unassigned cards sort into the same status groups as assigned cards (In Transit, Delivered, etc.) — no separate section
- The italic/warning name treatment is the only visual differentiator
- "Pick Recipient" appears in the ellipsis menu with **warning color text** (matching the italic name color)

### Assignment Flow
- "Pick Recipient" opens a searchable list modal
- Modal title: "Pick Recipient for {Shopify Customer Name}"
- Search input at top with fuzzy filtering
- Scrollable list of GH Project recipients — **only shows entries where Shopify Profile URL is empty/unset** (already-linked recipients are excluded)
- Selecting a recipient immediately assigns and closes the modal
- "Create New Recipient" button at bottom of the list
- **Immediate GH Project write-back**: after assignment, update the recipient's "Shopify Profile URL" field via GH Project mutation API with the customer's profile URL
- Card updates locally immediately (name changes from italic customer to normal recipient name)
- Next sync auto-matches via the now-populated Shopify Profile URL

### Create New Recipient
- Pre-fill name from Shopify customer first+last name
- Pre-fill Shopify Profile URL from the order's customer
- Create a new GH Project item (row) via API
- Assign the order to this new recipient immediately
- Card updates immediately

### Claude's Discretion
- GraphQL mutation shape for GH Project field updates
- Exact fuzzy search algorithm for recipient filtering
- Shopify pagination implementation (cursor-based vs page_info)
- How `run_sync_cycle` is refactored to be Shopify-first
- Error handling for failed GH Project write-backs (retry? toast?)
- Whether `orders_for_customer` is removed or kept as secondary enrichment

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Current Data Pipeline
- `crates/app/src/live_client.rs` — `run_sync_cycle()` at line 248: current GitHub-first sync pipeline to be refactored
- `crates/app/src/dashboard/mod.rs` — `DashboardView::from_snapshots()`: card creation from sync snapshots
- `crates/app/src/dashboard/projection.rs` — `project_snapshot()`: snapshot-to-card projection logic

### Shopify Client
- `crates/integrations/src/shopify/http_client.rs` — `HttpShopifyClient` with existing `orders_for_customer()`. Needs new `orders_by_tag()` method
- `crates/integrations/src/shopify/order_fulfillment_client.rs` — `ShopifyOrderFulfillmentClient` trait definition
- `crates/integrations/src/shopify/types.rs` — `ShopifyOrder` struct (has `tags: Vec<String>`, `customer` field)

### GitHub Project Client
- `crates/integrations/src/github/gh_cli_project_client.rs` — GH CLI project client, needs mutation support for Shopify Profile URL field update
- `crates/integrations/src/github/types.rs` — GitHub Project field types, `Recipient` struct

### UI Components
- `crates/app/ui/dashboard.slint` — Card grid rendering, ellipsis menu
- `crates/app/src/main.rs` — Callback wiring, sync trigger patterns

### Prior Phase Context
- `.planning/phases/12.1-.../12.1-CONTEXT.md` — Phase 12.1 deferred ideas section documents the Shopify-first pipeline decision

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `HttpShopifyClient`: already has HTTP/ureq setup, auth header, and order parsing — extend with `orders_by_tag()` method
- `ShopifyOrder.tags`: already parsed from JSON, can filter by "wit-what"
- `ShopifyOrder.customer`: already available with id, email, first_name, last_name
- `DashboardCardViewModel`: existing card view model can be extended with "unassigned" flag
- Ellipsis menu: already exists with action callbacks
- `SyncUpdateCallback`: existing callback pattern for sync results

### Established Patterns
- `run_sync_cycle()` returns `SyncResult` with snapshots — refactor to produce Shopify-first snapshots
- `from_snapshots()` creates `DashboardView` — extend to handle unassigned cards
- Toast pattern for user feedback (3s auto-dismiss)
- LiveClient reconnect on settings save (immediate sync trigger)

### Integration Points
- `run_sync_cycle()`: primary refactor target — flip from GH-first to Shopify-first
- `ShopifyOrderFulfillmentClient` trait: add `orders_by_tag()` method
- Dashboard card rendering: add italic/warning styling for unassigned
- Ellipsis menu: add "Pick Recipient" action with warning color
- New modal: recipient picker (searchable list)
- GH Project mutations: new API for updating field values on existing items

</code_context>

<specifics>
## Specific Ideas

- "Pick Recipient" text in the ellipsis menu must use the same warning color as the italic unassigned name — makes it obvious what action resolves the warning
- Recipient picker only shows GH Project entries where Shopify Profile URL is empty/unset — prevents double-assignment
- The admin URL customer ID vs REST API customer ID equivalence must be researched before implementation
- Since-based sync uses `updated_at_min` parameter on the orders endpoint — only for the 5-minute interval, not for manual/startup sync

</specifics>

<deferred>
## Deferred Ideas

- Order number display on cards — user explicitly said order number is not displayed on cards
- Batch assignment (assigning multiple unassigned cards at once) — not needed for v1
- Customer merge handling (Shopify customer merge events) — edge case for future

</deferred>

---

*Phase: 12.1.1-shopify-first-card-source-pipeline*
*Context gathered: 2026-03-21*
