Appearance
Buylist Customer-Page Admin Extension (PR 2) Implementation Plan โ
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: A read-only block on the Shopify admin customer detail page listing that customer's buylist orders (status, totals, settlement summary), deep-linking into the app's review screen.
Blocked on: PR 1 merged (provides customer.shopifyCustomerId + indexes); the app being granted the new scopes on ufkes-dev; Partner-dashboard/CLI linkage for extension deploys. Task 1 (the endpoint) is buildable immediately after PR 1; Tasks 2-3 wait.
Architecture: One new authenticated endpoint serving orders by shopifyCustomerId (email fallback), plus a first-ever extensions/ workspace: an Admin UI extension targeting admin.customer-details.block.render, built/deployed with the Shopify CLI. The extension calls our API with the admin session token; dualModeAuth already validates session tokens.
Tech Stack: Express/Zod/Vitest (server), @shopify/ui-extensions-react/admin (extension), Shopify CLI for build/deploy.
Spec: docs/superpowers/specs/2026-07-29-buylist-shopify-native-settlement-design.md ยง5
Global Constraints โ
Same as PR 1 (CommonJS server, Zod + validate(), DI tests, ยง5.1 exempt-by-construction, no identity defaults). Extension code is a separate workspace with its own package.json โ it must NOT import from client/src.
Task 1: GET /api/buylist/customer-orders endpoint โ
Files:
- Modify:
server/schemas/buylistOrder.js(addcustomerOrdersQuerySchema) - Modify:
server/routes/buylist.js - Test:
server/routes/buylist.customerOrders.test.js
Interfaces:
- Consumes: PR 1 indexes
{ shop, 'customer.shopifyCustomerId', createdAt }and{ shop, 'customer.email' }. - Produces:
GET /api/buylist/customer-orders?customerId=gid://shopify/Customer/N&email=x@y.z&page=&limit=โ{ orders, total, page, limit }where each order is trimmed to{ _id, status, createdAt, payout: { method, cashTotal, creditTotal }, settlement, lineCount }(no full lines โ the block is a summary; details live in the app).
Behavior:
Zod query schema:
customerId(string,startsWith('gid://shopify/Customer/')) and/oremail(email) โ at least one required (.refine);page/limitper the existing pagination schema,limitmax 25.Query:
$orof{ 'customer.shopifyCustomerId': customerId }and{ 'customer.email': email }(only the params provided), always{ shop: req.store.shop }, sortedcreatedAt: -1. Both provided โ orders linked by id AND legacy email-only orders come back in one list, deduped naturally (single query).Empty result โ
{ orders: [], total: 0 }(extension renders nothing).[ ] Failing route tests (handler-export pattern): 400 with neither param; id-only, email-only, both; shop scoping; trimmed shape (no
linesin payload).[ ] Implement handler +
validate(customerOrdersQuerySchema, 'query')(checkvalidate()'s query-mode signature inmiddleware/validate.jsโ if it only handles bodies, do inlinesafeParse(req.query)like the config routes and note why).[ ]
npm testgreen; commit โgit commit -m "Serve a customer's buylist order history for the admin customer page"
Task 2: Extension workspace scaffold โ
Files (paths follow CLI output โ adjust to what shopify app generate extension actually emits):
- Create:
shopify.app.toml(app config linked to the existing app's client_id โshopify app config link) - Create:
extensions/buylist-customer-orders/shopify.extension.tomlโtype = "ui_extension", targetadmin.customer-details.block.render - Create:
extensions/buylist-customer-orders/src/BlockExtension.jsx - Modify: root
package.jsonscripts (extension:dev,extension:deployโshopify app dev/shopify app deploy) - Modify:
.github/workflows/README.mdnote โ extension deploys are manual via CLI for now (no CI job in this PR)
Steps:
- [ ]
shopify app config linkagainst the existing app (requires Partner access โ Brent may need to run the auth'd CLI commands; the plan's code steps are still automatable). - [ ]
shopify app generate extension --template admin_block --name buylist-customer-orders --flavor react - [ ] Verify the generated toml targets
admin.customer-details.block.render; commit scaffold as-is โgit commit -m "Scaffold the admin customer-page extension workspace"
Task 3: Block implementation โ
Files:
- Modify:
extensions/buylist-customer-orders/src/BlockExtension.jsx
Behavior:
useApi(TARGET)givesdata.selected[0].id(the customer GID) andsessionToken.- Fetch
${APP_URL}/api/buylist/customer-orders?customerId=<gid>withAuthorization: Bearer ${await sessionToken.get()}. The API origin comes from the extension's app settings/env โ hardcode the production API URL in a smallconfig.jsnext to the block (extensions can't read server env). - Render with
@shopify/ui-extensions-react/admincomponents (AdminBlock,BlockStack,InlineStack,Text,Badge,Link): one row per order โ created date, status badge, payout method, total, settlement note if present โ each row aLinktohttps://admin.shopify.com/store/<store-handle>/apps/<app-handle>/buylist/orders/<id>(verify the app-handle deep-link format against how the embedded app builds its own URLs during implementation). - Zero orders or fetch error โ render null (block stays invisible; spec: invisible where beta feature off/no data).
- CORS: confirm the extension origin (
extensions.shopifycdn.com/ admin) passes the server allowlist โ if not, add it to the CORS config wheredualModeAuth-protected routes are served.
Verification (needs ufkes-dev + scopes):
- [ ]
shopify app devagainst ufkes-dev; open a customer with buylist history โ orders listed; customer without โ no block; store credit balance visible natively above. - [ ]
npm test/ lint green; commit โgit commit -m "Show a customer's buylist history on their Shopify admin profile"; PR tomain.
Self-Review Notes โ
- Spec ยง5 fully covered: toml/target/build (Task 2), endpoint + both join keys + trimmed payload (Task 1), session-token auth + CORS check (Task 3), invisible-when-empty (Task 3).
- Extension unit testing is thin by design (Shopify's extension test harness is heavyweight); the endpoint carries the test weight, the block is verified live in Task 3.
