Appearance
Buylist Shopify-Native Settlement + Customer Profile Integration โ
Date: 2026-07-29 Status: Approved Supersedes/extends: Issue #495 item 1 (customer-profile view) โ reframed around native settlement first. Related: 2026-07-28-buylist-independent-cash-credit-design.md (payout tracks this builds on)
Problem โ
Buylist orders live entirely in buylist_orders (Mongo), keyed by a free-typed email. The paid status exists in the enum but no endpoint produces it โ "payout" has no representation anywhere, on our side or Shopify's. Meanwhile Shopify now has first-class primitives for exactly this: native store credit attached to the customer account (rendered on the admin customer profile, redeemable at POS and online checkout) and an Admin UI extension surface on the customer detail page.
Decision (Brent, 2026-07-29): use the platform. Store-credit payouts are issued as native Shopify store credit (not gift cards โ those are the legacy workaround). Cash payouts settle in person and get a durable settlement record in our app.
Goal โ
When a merchant settles an accepted buylist order:
- Store credit: the amount lands on the customer's Shopify store-credit balance via
storeCreditAccountCredit, visible natively on their admin profile and spendable at checkout/POS. - Cash: the order carries a settlement record (amount, timestamp, note) as the audit trail for in-person payment.
- Either way the order is linked to a real Shopify customer, and the customer's buylist history appears on their admin customer page via a read-only Admin UI extension.
Non-Goals โ
- Inventory intake of purchased cards into Shopify.
- Cash reconciliation reporting (fast-follow candidate, not this design).
- Store-credit expiration configuration.
- Gift-card payout vehicle.
- Any change to quoting/offer logic or payout-track math.
- Settling from the extension itself (read-only; actions deep-link into the app).
Decisions Made โ
| Decision | Choice |
|---|---|
| Payout vehicle | Native Shopify store credit (storeCreditAccountCredit) |
| Customer linking | Resolve by email at settlement; create the customer (with portal disclosure) if missing |
| Cash tracking | Settlement record: amount, timestamp, optional note |
| Extension scope | Read-only order list on the customer profile, deep-link to app |
| Sequencing | PR 1 settlement + linking, PR 2 admin extension |
Design โ
1. Data model (BuylistOrder) โ
Both additions declared field-by-field (CLAUDE.md ยง5.2) with a schema-shape round-trip test:
customer.shopifyCustomerIdโ String, Shopify customer GID. Set at settlement when the customer is resolved or created. This is the join key the PR 2 extension queries on.settlementsub-doc:method:'store_credit' | 'cash'amount: Number โ the order'screditTotal/cashTotalfor its payout method at settle timecurrencyCode: String (shop currency)settledAt: Datenote: String, optional (e.g. "paid from register 1")storeCreditTransactionId: String โ store-credit settlements only; absence on apaidstore-credit order means the credit was never confirmed (see ยง3 failure handling)customerCreated: Boolean โ whether settlement created the Shopify customer record
New indexes:
{ shop: 1, 'customer.shopifyCustomerId': 1, createdAt: -1 }โ extension's primary query{ shop: 1, 'customer.email': 1 }โ extension's fallback for pre-settlement orders (closes the missing-index note from issue #495)
Orders paid without a settlement sub-doc read as "settled before tracking existed."
2. Settlement endpoints โ
GET /api/buylist/orders/:id/settlement-preview Resolves the order's email against Shopify (customers search โ emails are unique per customer, so 0 or 1 match). Returns matched customer { id, displayName } or willCreateCustomer: true, plus the settle amount and method. The settle modal shows this so the merchant confirms the identity match before any credit is issued โ wrong-email orders are caught here, since store credit is only spendable by someone checking out with that email.
POST /api/buylist/orders/:id/settle โ Zod schema in server/schemas/, wired via validate(). Body: { note? }. Allowed only from accepted (409 otherwise). Amount is not overridable in v1; adjustments happen in the existing review/edit flow before accepting.
- Cash path: stamp
settlement, set statuspaid. No Shopify call. - Store-credit path:
- Atomically claim:
findOneAndUpdate({ _id, shop, status: 'accepted' }, โ status 'paid' + settlement stub). A concurrent settle loses the race and 409s โ this is the double-issue guard. - Resolve customer by email;
customerCreateif none. storeCreditAccountCredit(account auto-created on first credit).- Write
storeCreditTransactionIdandcustomer.shopifyCustomerId.
- Shopify failure: revert status to
accepted, clear the stub, surface the error. - Crash window (claimed, no transaction id): detectable โ the review UI flags "credit not confirmed" and offers retry; the retry re-issues safely because the claim (status
paid,settlement.method: 'store_credit', no transaction id) survives and the retry path only runs the Shopify steps.
- Atomically claim:
The walk-in register flow ("customer here with cards, pay now") is the client chaining accept โ settle; no combined endpoint.
3. Shopify integration โ
- Three new methods on
server/services/shopifyAPI.js(rule 6 โ nothing else calls Shopify):findCustomerByEmail,createCustomer,creditStoreCredit. API version is the sharedconstants.jsAPI_VERSION. - New scopes:
read_customers,write_customers,write_store_credit_account_transactions. Updated in the same sitting (ยง5.7 split-brain rule) in:server/config/shopify.jsfallback,server/config/constants.jsSCOPES, the runtimeSHOPIFY_SCOPESenv on Cloud Run/worker, and the GH Actions secret consumed by deploy workflows. - Re-consent rollout: existing installs lack the new scopes. The settle endpoint detects Shopify's missing-scope error and returns a distinct error code the client renders as a "re-authorize LGS Forge" prompt (not a raw 500). Buylist is beta, so the affected-store count is small.
- GDPR:
customers/redacthandling must extend tobuylist_ordersfor the shop+email. Verify current webhook behavior during implementation โ likely a pre-existing gap, fix rides along in PR 1.
4. Client (review page) โ
Settle button on accepted orders opens a modal: customer match from the preview endpoint (or "a new customer record will be created"), amount, method, optional note โ confirm. Success links to the customer's Shopify admin page. The public portal gains a one-line disclosure that completing a sale may create a customer record at the store.
5. PR 2 โ customer-page Admin UI extension โ
- New
extensions/buylist-customer-orders/workspace:shopify.extension.tomltargetingadmin.customer-details.block.render, Shopify CLI build step, app-config change so it registers on deploy. First extension in this app. - Read-only block: the customer's buylist orders (status, totals, settlement summary), each deep-linking to the app's review screen.
- Backed by
GET /api/buylist/customer-orders(Zod-validatedcustomerIdand/oremail): queriesshopifyCustomerIdfirst, email fallback for orders that predate settlement linking. Auth via session token throughdualModeAuth; confirm the extension origin passes the CORS allowlist. - The block renders nothing for customers with no buylist history and for stores without buylist enabled โ the extension deploys app-wide but must stay invisible where the beta feature is off.
6. Testing โ
- Settlement route/service tests via
_setDepsinjection (mockshopifyAPI): cash path, credit path, double-settle race (second call 409s), Shopify failure โ revert, crash-window retry, missing-scope โ distinct error code, customer-create path. - ยง5.2 round-trip test for
settlementandcustomer.shopifyCustomerId. customer-ordersendpoint tests for both join keys and the empty case.- Game parity (ยง5.1): settlement is order-level and game-agnostic; no plugin, per-game model, or importer is touched. All three plugins (mtg, pokemon, riftbound) exempt-by-construction โ stated in each PR.
Open Items Deliberately Left Behind โ
- Issue #495 items 2โ4 (payout migration run, retroui
Inputcn() fix, ManaBox vocabulary) are untouched by this design and remain tracked there. - Cash reconciliation view โ revisit after settlement records accumulate.
