Skip to content

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 (add customerOrdersQuerySchema)
  • 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/or email (email) โ€” at least one required (.refine); page/limit per the existing pagination schema, limit max 25.

  • Query: $or of { 'customer.shopifyCustomerId': customerId } and { 'customer.email': email } (only the params provided), always { shop: req.store.shop }, sorted createdAt: -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 lines in payload).

  • [ ] Implement handler + validate(customerOrdersQuerySchema, 'query') (check validate()'s query-mode signature in middleware/validate.js โ€” if it only handles bodies, do inline safeParse(req.query) like the config routes and note why).

  • [ ] npm test green; 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", target admin.customer-details.block.render
  • Create: extensions/buylist-customer-orders/src/BlockExtension.jsx
  • Modify: root package.json scripts (extension:dev, extension:deploy โ†’ shopify app dev / shopify app deploy)
  • Modify: .github/workflows/README.md note โ€” extension deploys are manual via CLI for now (no CI job in this PR)

Steps:

  • [ ] shopify app config link against 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) gives data.selected[0].id (the customer GID) and sessionToken.
  • Fetch ${APP_URL}/api/buylist/customer-orders?customerId=<gid> with Authorization: Bearer ${await sessionToken.get()}. The API origin comes from the extension's app settings/env โ€” hardcode the production API URL in a small config.js next to the block (extensions can't read server env).
  • Render with @shopify/ui-extensions-react/admin components (AdminBlock, BlockStack, InlineStack, Text, Badge, Link): one row per order โ€” created date, status badge, payout method, total, settlement note if present โ€” each row a Link to https://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 where dualModeAuth-protected routes are served.

Verification (needs ufkes-dev + scopes):

  • [ ] shopify app dev against 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 to main.

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.