Files
route-commerce/docs/qa/audit-2026-06-25/ACCEPTANCE-CRITERIA.md
T
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00

22 KiB
Raw Blame History

Acceptance Criteria — 2026-06-25

Format per item: AC-NN.SS (module.section) — Given/When/Then or assertion. Edge cases follow each AC block prefixed EC-NN.SS.x.

Risk tier definitions

  • C — Critical: any data loss, RBAC bypass, payment error, auth failure, or cross-tenant data leak. Must pass before sign-off.
  • H — High: core CRUD flows used daily by admin/brand_admin. Pass-blocking.
  • M — Medium: secondary features (filters, exports, analytics). Pass-blocking for release quality, acceptable for hot-fix.
  • L — Low: cosmetics, marketing pages, static content. Best-effort.

ROLES & IDENTITY (C)

  • Given NODE_ENV !== "production"
  • When request carries Cookie: dev_session=platform_admin
  • Then getAdminUser() returns { role: 'platform_admin', brand_id: null, brandIds: [...], email: 'qa-platform@...' }
  • And every /admin/* route renders without <AdminAccessDenied />

EC-R1:

  • EC-R1.1: invalid value (dev_session=hacker) → returns null (no impersonation)
  • EC-R1.2: missing cookie → null → /admin/* shows Access Denied
  • EC-R1.3: NODE_ENV=production → dev_session is ignored, real Neon Auth required

AC-R2 — RBAC enforcement

  • Given admin user with can_manage_orders=false
  • When visiting /admin/orders/[id]
  • Then redirect to /admin/pickup
  • And /admin/orders shows Access Denied or redirects similarly

EC-R2:

  • EC-R2.1: any permission flag can_manage_X=false enforces redirect on /admin/X
  • EC-R2.2: all flags true → no redirects
  • EC-R2.3: brand_admin without brand link → null user → Access Denied

AC-R3 — Tenant scoping

  • Given brand_admin of brand A
  • When calling RPCs with p_brand_id=brand_A_id
  • Then only brand A rows are returned
  • When calling with p_brand_id=brand_B_id
  • Then zero rows or 403/empty depending on the action

EC-R3:

  • EC-R3.1: platform_admin with p_brand_id=null returns all brands
  • EC-R3.2: brand_admin calling with p_brand_id=null falls back to their brand_id
  • EC-R3.3: brand_admin calling with p_brand_id=other_brand_id → zero/empty

ADMIN SHELL (C)

AC-A1 — /admin/page.tsx redirects to /admin/v2

  • Then 307 → /admin/v2

AC-A2 — /admin/v2 dashboard renders for platform_admin

  • Then shows "Today" heading + stat cards + stops + orders
  • And no Access Denied

EC-A2:

  • EC-A2.1: 0 stops today → "No stops scheduled today" empty state
  • EC-A2.2: 0 pending orders → empty state
  • EC-A2.3: activeBrandId null → "No brand selected" empty state
  • Then every Workspace/Operations/Communications/Growth/Tracking/Insights/Settings link navigates to its target route without error

EC-A3:

  • EC-A3.1: Tracking → Water Log hidden when enabledAddons["water_log"]=false
  • EC-A3.2: Tracking → Route Trace hidden when enabledAddons["route_trace"]=false
  • EC-A3.3: requiresPlatformAdmin items hidden for non-platform users
  • EC-A3.4: mobile menu closes on link click
  • EC-A3.5: ESC closes mobile menu
  • EC-A3.6: focus moves to close button when mobile menu opens
  • EC-A3.7: arrow-key navigation loops top-to-bottom-to-top

AC-A4 — MobileTabBar navigates and shows active state

  • Then clicking each tab navigates + highlights active tab matching pathname

EC-A4:

  • EC-A4.1: clicking More opens MoreSheet
  • EC-A4.2: clicking a MoreSheet link closes sheet and navigates

AC-A5 — CommandPalette opens on Cmd/Ctrl+K

  • Then modal appears, search input focused

EC-A5:

  • EC-A5.1: ↑↓ navigation, Enter selects, ESC closes
  • EC-A5.2: body scroll locked while open
  • EC-A5.3: backdrop click closes
  • EC-A5.4: empty results → "No results for 'query'"
  • EC-A5.5: navigate via result updates URL and closes palette

AC-A6 — BrandSelector switches active brand

  • Then selecting a brand sets cookie + refreshes page; BrandSelector reflects new active

EC-A6:

  • EC-A6.1: "All brands" option visible only to platform_admin
  • EC-A6.2: multi-brand admin → "multi" badge
  • EC-A6.3: outside click closes
  • EC-A6.4: ESC closes

AC-A7 — OfflineBanner shows when offline or pending

  • Then offline → danger-soft banner; pending sync → warning-soft banner
  • When online + 0 pending → banner hidden

EC-A7:

  • EC-A7.1: pre-mount → null (no hydration mismatch)

ORDERS (C)

AC-M1.1 — /admin/orders legacy list loads

  • Then status tabs (all/pending/picked_up) render with counts
  • And table shows orders sorted by placed_at desc

EC-M1.1:

  • EC-M1.1.1: empty state when 0 orders → "No orders yet" + Create CTA
  • EC-M1.1.2: filter by status → list filters correctly
  • EC-M1.1.3: search by customer name → matches substring
  • EC-M1.1.4: search by phone → matches digits
  • EC-M1.1.5: search by order id prefix → matches
  • EC-M1.1.6: pagination next/prev changes page
  • EC-M1.1.7: stop filter multi-select → list filtered by selected stops
  • EC-M1.1.8: clearing filters shows all orders
  • EC-M1.1.9: bulk select + Mark All Picked Up → all selected marked, toast shows counts
  • EC-M1.1.10: partial bulk failure → some succeed, others revert, error toast
  • EC-M1.1.11: ?new=true opens modal on mount
  • EC-M1.1.12: row click navigates to /admin/orders/[id]

AC-M1.2 — /admin/orders/new modal

  • Then modal opens with required: customer name; optional: email, phone, stop
  • And items table with at least 1 row

EC-M1.2:

  • EC-M1.2.1: empty customer name → submission blocked
  • EC-M1.2.2: 0 items → Create Order disabled
  • EC-M1.2.3: invalid price (< 0 or non-numeric) → submission blocked
  • EC-M1.2.4: qty < 1 → blocked
  • EC-M1.2.5: success → full-page reload to /admin/orders, modal closes
  • EC-M1.2.6: server error → red error banner inside modal
  • EC-M1.2.7: ESC closes modal
  • EC-M1.2.8: backdrop click closes modal

AC-M1.3 — /admin/orders/[id] detail + edit

  • Then shows customer, stop, items, totals, payment section, edit form

EC-M1.3:

  • EC-M1.3.1: invalid id → "Order not found"
  • EC-M1.3.2: empty customer name on save → "Customer name is required"
  • EC-M1.3.3: negative discount → "Discount cannot be negative"
  • EC-M1.3.4: remove item → marked removed, deleted on save
  • EC-M1.3.5: save success → toast + page refreshes
  • EC-M1.3.6: status set to fulfilled (not exposed in UI) → should not be reachable
  • EC-M1.3.7: pickup toggle: not picked up → "Mark Picked Up"; picked up → button hidden
  • EC-M1.3.8: refund amount = 0 → button disabled
  • EC-M1.3.9: refund amount > remaining balance → validation error
  • EC-M1.3.10: refund amount = remaining balance → success

AC-M1.4 — /admin/v2/orders mobile list

  • Then cards sorted; status filter via ?status=

EC-M1.4:

  • EC-M1.4.1: 0 orders → "No orders yet"
  • EC-M1.4.2: status=placed → only placed orders shown
  • EC-M1.4.3: card click → /admin/v2/orders/[id]

AC-M1.5 — /admin/v2/orders/[id] mobile detail

  • Then shows timeline + sticky action bar with status buttons

EC-M1.5:

  • EC-M1.5.1: status transitions: placed → ready → picked-up; cancel from any state
  • EC-M1.5.2: invalid id → notFound()

STOPS (H)

AC-M2.1 — /admin/stops list

  • Then table sorted by date asc; tabs filter by status

EC-M2.1:

  • EC-M2.1.1: search matches city/location substring
  • EC-M2.1.2: Add Stop modal opens
  • EC-M2.1.3: Upload Schedule modal opens
  • EC-M2.1.4: Edit → /admin/stops/[id]
  • EC-M2.1.5: Publish (draft only) → status changes to active, refreshes
  • EC-M2.1.6: Duplicate → /admin/stops/new?duplicate={id}
  • EC-M2.1.7: Delete → confirm popover → confirm → row removed

AC-M2.2 — /admin/stops/new form

  • Then required: city/state/location/date/time/brand

EC-M2.2:

  • EC-M2.2.1: missing required field → red border + helper
  • EC-M2.2.2: ?duplicate={id} pre-fills from existing stop
  • EC-M2.2.3: brand selector hidden for non-platform_admin

AC-M2.3 — AddStopModal

  • Then opens with autofocus on city

EC-M2.3:

  • EC-M2.3.1: missing city/state/location/date → submission blocked
  • EC-M2.3.2: state uppercased on input
  • EC-M2.3.3: state max 2 chars
  • EC-M2.3.4: ESC closes
  • EC-M2.3.5: success → modal closes + list refreshes

AC-M2.4 — LocationsTab

  • Then table with search + status tabs + pagination

EC-M2.4:

  • EC-M2.4.1: Add Venue modal opens
  • EC-M2.4.2: Edit modal opens with pre-filled fields
  • EC-M2.4.3: Delete with confirm → row removed
  • EC-M2.4.4: search matches name/address/city/contact_name

AC-M2.5 — /admin/v2/stops mobile

  • Then cards bucketed by Morning/Afternoon/Evening/Anytime

EC-M2.5:

  • EC-M2.5.1: ?date=YYYY-MM-DD filters
  • EC-M2.5.2: invalid date → empty or error
  • EC-M2.5.3: 0 stops → "No stops scheduled"
  • EC-M2.5.4: card with address → maps link works

PRODUCTS (H)

AC-M3.1 — /admin/products legacy list

  • Then shows all brand-scoped products

EC-M3.1:

  • EC-M3.1.1: 0 products → empty state
  • EC-M3.1.2: platform_admin sees products from all brands
  • EC-M3.1.3: brand_admin sees only their brand

AC-M3.2 — /admin/products/new form

  • Then name required, type/brand/taxable/pickup_type/active selects

EC-M3.2:

  • EC-M3.2.1: missing name → blocked
  • EC-M3.2.2: image upload: drag-drop or click → preview
  • EC-M3.2.3: oversized image (>5MB) → error
  • EC-M3.2.4: wrong type → error
  • EC-M3.2.5: success → /admin/products
  • EC-M3.2.6: brand locked for non-platform_admin

AC-M3.3 — /admin/products/import CSV

  • Then preview table shows parsed rows

EC-M3.3:

  • EC-M3.3.1: malformed CSV → parse errors listed
  • EC-M3.3.2: missing brand ID → Import disabled
  • EC-M3.3.3: success → counts panel

AC-M3.4 — /admin/v2/products mobile

  • Then cards with status pill + StockAdjustButton

EC-M3.4:

  • EC-M3.4.1: ?filter=in-stock → only products with inventory > 0
  • EC-M3.4.2: ?filter=out → only inventory = 0
  • EC-M3.4.3: ?filter=hidden → only active=false
  • EC-M3.4.4: StockAdjustButton increments/decrements inventory

COMMUNICATIONS (H)

AC-M6.1 — Campaigns list + create + edit

  • Then list with type/status filters

EC-M6.1:

  • EC-M6.1.1: New Campaign → name + type required → Create
  • EC-M6.1.2: Save Draft → status=draft, no send
  • EC-M6.1.3: Schedule Campaign with future datetime → status=scheduled
  • EC-M6.1.4: Send Campaign → calls sendCampaign → status=sent
  • EC-M6.1.5: Schedule with past datetime → blocked (datetime-local min = now)
  • EC-M6.1.6: missing subject/body → blocked
  • EC-M6.1.7: previewAudience → shows count + sample emails

AC-M6.2 — Templates list + edit

  • Then edit fields: subject, body_text, body_html, template_type, campaign_type

EC-M6.2:

  • EC-M6.2.1: missing subject → blocked (subject is NOT NULL)
  • EC-M6.2.2: success → list refreshes

AC-M6.3 — Contacts list

  • Then search + source filter + pagination

EC-M6.3:

  • EC-M6.3.1: delete with confirm → row removed
  • EC-M6.3.2: Export CSV → file downloads
  • EC-M6.3.3: pagination prev/next

AC-M6.4 — Message logs

  • Then status filter + search + pagination (1-5 pages)

EC-M6.4:

  • EC-M6.4.1: filter by status=failed → only failed shown
  • EC-M6.4.2: stats cards reflect filtered count

AC-M6.5 — Abandoned Carts

  • Then stats + filter (all/active/recovered) + pagination

EC-M6.5:

  • EC-M6.5.1: View → detail modal with items + timestamps
  • EC-M6.5.2: Close → status=manually_closed
  • EC-M6.5.3: Resend → calls resendAbandonedCartEmail

AC-M6.6 — Communication Settings

  • Then senderEmail/senderName/replyTo/footerHtml form

EC-M6.6:

  • EC-M6.6.1: invalid email → error
  • EC-M6.6.2: {unsubscribe_url} placeholder preserved in footerHtml

AC-M6.7 — ContactImportForm (multi-step)

  • Then drag/drop or click → preview → import

EC-M6.7:

  • EC-M6.7.1: file >5MB → bucket path
  • EC-M6.7.2: missing required mapping → blocked
  • EC-M6.7.3: success → counts panel

WHOLESALE (H)

AC-M7.1 — Wholesale dashboard loads

  • Then shows tabs for Customers / Orders / Pricing / Settings

EC-M7.1:

  • EC-M7.1.1: 0 customers → empty state
  • EC-M7.1.2: 0 orders → empty state

AC-M7.2 — Wholesale Customers

  • Then list with search + status filter

EC-M7.2:

  • EC-M7.2.1: bulk "Send Price Sheet" → calls sendPriceSheetToCustomer N times

AC-M7.3 — Wholesale Orders

  • Then list with status filter

EC-M7.3:

  • EC-M7.3.1: bulk Fulfill → multiple fulfillments
  • EC-M7.3.2: bulk Deposit → multiple deposits

AC-M7.4 — Wholesale Settings

  • Then form saves via updateWholesaleSettings

EC-M7.4:

  • EC-M7.4.1: invalid email → error
  • EC-M7.4.2: min_order_amount < 0 → validation

SETTINGS (H)

AC-M8.1 — BrandSettingsForm

  • Then all sections save atomically

EC-M8.1:

  • EC-M8.1.1: logo upload → preview + URL stored
  • EC-M8.1.2: nexus state pill: add via Enter or comma → chip appears
  • EC-M8.1.3: nexus state pill: click ✕ → chip removed
  • EC-M8.1.4: state field: lowercased input → uppercased display
  • EC-M8.1.5: brand colors: text input + color picker stay in sync
  • EC-M8.1.6: feature toggles: each toggle saves independently

AC-M8.2 — BrandFeatureCards

  • Then each card shows status + Enable/Disable

EC-M8.2:

  • EC-M8.2.1: Enable → confirmation modal → success toast + card shows Active
  • EC-M8.2.2: Disable → optimistic toggle → rollback on failure
  • EC-M8.2.3: "Open →" link visible only when enabled + adminRoute set

AC-M8.3 — CreateUserModal

  • Then email/password (≥6) required

EC-M8.3:

  • EC-M8.3.1: invalid email (no @) → blocked
  • EC-M8.3.2: password < 6 → blocked
  • EC-M8.3.3: missing brand for brand_admin → blocked
  • EC-M8.3.4: success → temp password surfaced + email-sent status
  • EC-M8.3.5: Copy button copies temp password
  • EC-M8.3.6: role options depend on caller role

AC-M8.4 — PaymentSettingsForm

  • Then provider radio + Stripe/Square OAuth buttons + Square Location ID

EC-M8.4:

  • EC-M8.4.1: Square Location ID without L prefix → blocked
  • EC-M8.4.2: Save disabled when not dirty
  • EC-M8.4.3: Stripe OAuth flow → return with ?stripe_connected=true → URL param stripped
  • EC-M8.4.4: Square OAuth flow → return with ?square_connected=true → URL param stripped
  • EC-M8.4.5: Sync Products/Orders/All Now → shows result banner

AC-M8.5 — AdvancedSettingsClient tabs

  • Then tabs render Integrations / AI / Square / Shipping / Webhooks / Payments

EC-M8.5:

  • EC-M8.5.1: Integrations: Resend + Twilio save independently
  • EC-M8.5.2: AI Tools: provider selection changes default model
  • EC-M8.5.3: AI Tools: API key required for Test
  • EC-M8.5.4: Square Sync: App ID + Access Token required for Test
  • EC-M8.5.5: Shipping: Test Connection (simulated) shows success after 1s
  • EC-M8.5.6: Webhooks tab shows "Coming Soon"
  • EC-M8.5.7: Payments: Connect with Stripe → redirects to Stripe

ANALYTICS (M)

AC-M9.1 — AnalyticsDashboard loads

  • Then KPI cards + revenue chart + top products + recent orders + customer growth + funnel

EC-M9.1:

  • EC-M9.1.1: period selector change → chart re-renders (visual only, no refetch)
  • EC-M9.1.2: Refresh → fetches all
  • EC-M9.1.3: Export Report → no-op
  • EC-M9.1.4: 0 revenue → "No revenue data available yet"
  • EC-M9.1.5: error → retry button works

TIME TRACKING (H)

AC-M11.1 — TimeTrackingAdminPanel tabs

  • Then Summary / Workers / Tasks / Logs / Settings

EC-M11.1:

  • EC-M11.1.1: 0 workers → empty
  • EC-M11.1.2: Add Worker → name/role/language/active → Create
  • EC-M11.1.3: Reset PIN → calls resetTimeWorkerPin
  • EC-M11.1.4: Delete Worker → confirm → removed
  • EC-M11.1.5: Logs filter by worker + task + date range → list updates
  • EC-M11.1.6: Logs pagination 50/page
  • EC-M11.1.7: Export → CSV downloads from /api/time-tracking/export

WATER LOG (H)

AC-M11.2 — WaterLogAdminPanel

  • Then Add Headgate inline + Add User inline + filter chips + Export CSV

EC-M11.2:

  • EC-M11.2.1: missing name → blocked
  • EC-M11.2.2: PIN auto-generated on user create → PinBanner shows
  • EC-M11.2.3: Rotate token → token changes
  • EC-M11.2.4: Filter chips → entries filtered
  • EC-M11.2.5: Export CSV → downloads
  • EC-M11.2.6: Preview Report → window.alert

AC-M11.3 — HeadgatesManager bulk QR

  • Then select multiple + Print → new window with QR sheet

EC-M11.3:

  • EC-M11.3.1: 0 selected → Print disabled
  • EC-M11.3.2: regenerate token → existing printed QRs invalidated
  • EC-M11.3.3: per-headgate QR Download PNG

AC-M11.4 — Water Admin Settings

  • Then toggles + session duration slider + alert phone

EC-M11.4:

  • EC-M11.4.1: Regenerate PIN → confirm → PIN revealed
  • EC-M11.4.2: session duration 1-168 hours only
  • EC-M11.4.3: Save Settings → success banner

ROUTE TRACE (M)

AC-M11.5 — RouteTracePage loads

  • Then shell renders with stat cards + lot list

EC-M11.5:

  • EC-M11.5.1: missing feature flag → redirect
  • EC-M11.5.2: lot detail → timeline + orders

LAUNCH CHECKLIST (L — known cosmetic)

AC-M11.6 — LaunchChecklist renders

  • Then 8 categories × 4 tasks

EC-M11.6:

  • EC-M11.6.1: "Mark All Complete" → no-op (documented limitation)
  • EC-M11.6.2: "Export PDF" → no-op
  • EC-M11.6.3: Per-task checkbox → no-op
  • EC-M11.6.4: "Go →" links work

IMPORT CENTER (M)

AC-M11.7 — ImportCenterClient wizard

  • Then Upload → Analyze → Preview → Import

EC-M11.7:

  • EC-M11.7.1: drag/drop or click upload
  • EC-M11.7.2: file >10MB → blocked
  • EC-M11.7.3: AI detection confidence <80% → type override buttons shown
  • EC-M11.7.4: column mapping dropdowns
  • EC-M11.7.5: Import disabled when type=unknown
  • EC-M11.7.6: success → counts + "View" link

PICKUP (C)

AC-M4.1 — DriverPickupPanel

  • Then pending orders + picked-up orders collapsible

EC-M4.1:

  • EC-M4.1.1: Pick Up → calls markPickupComplete → toast 3s
  • EC-M4.1.2: stop filter → list filtered
  • EC-M4.1.3: search → matches name/phone/order id prefix
  • EC-M4.1.4: 0 pending → empty state
  • EC-M4.1.5: picked-up older than 72h → hidden

SHIPPING (C)

AC-M5.1 — ShippingFulfillmentPanel

  • Then shipping orders list

EC-M5.1:

  • EC-M5.1.1: 0 shipping orders → empty
  • EC-M5.1.2: filter by status

PUBLIC/AUTH (H)

AC-P1 — Login page

  • Then email + password submit to /api/auth/sign-in

EC-P1:

  • EC-P1.1: missing email → HTML5 validation blocks submit
  • EC-P1.2: missing password → blocked
  • EC-P1.3: invalid credentials → error banner
  • EC-P1.4: dev_session=platform_admin → sets cookie and redirects /admin
  • EC-P1.5: Google button disabled while loading
  • EC-P1.6: Forgot? link → /forgot-password (or /api/forgot-password?)

AC-P2 — Change password

  • Then new password + confirm match + ≥8 chars

EC-P2:

  • EC-P2.1: passwords don't match → error
  • EC-P2.2: <8 chars → error
  • EC-P2.3: success → /admin
  • EC-P2.4: "Sign out instead" → /logout

AC-P3 — Reset password

  • Then new password + confirm

EC-P3:

  • EC-P3.1: same as AC-P2

AC-P4 — Logout

  • Then spinner → /api/auth/sign-out → /login

AC-P5 — Maintenance splash

  • Then no controls, only mailto + /contact links

AC-P6 — Homepage

  • Then hero + CTA links

AC-P7 — Pricing

  • Then BillingToggle, FAQ accordion, Compare table expand

EC-P7:

  • EC-P7.1: Monthly ↔ Annual swap content
  • EC-P7.2: FAQ accordion: each opens/closes independently
  • EC-P7.3: Compare table expand/collapse

AC-P8 — Contact

  • Then form simulated submit (setTimeout)

EC-P8:

  • EC-P8.1: missing required field → blocked
  • EC-P8.2: success card replaces form

AC-P9 — Blog

  • Then static + decorative newsletter form

AC-P10 — Changelog

  • Then static timeline + RSS link

AC-P11 — Roadmap

  • Then 3-column + Suggestion form (no submit) + Upvote buttons (no handlers)

EC-P11:

  • EC-P11.1: form submit does nothing (reloads page)
  • EC-P11.2: upvote buttons no-op

AC-P12 — Security

  • Then static + mailto

AC-P13 — Privacy / Terms

  • Then static text

AC-P14 — Brands showcase

  • Then per-brand visit links

AC-P15 — Waitlist

  • Then form posts to /api/waitlist

EC-P15:

  • EC-P15.1: missing email → blocked
  • EC-P15.2: success card replaces form
  • EC-P15.3: server error → inline error

AC-P16 — Cart

  • Then items list + qty controls + stop picker

EC-P16:

  • EC-P16.1: button decreases qty; at qty=1, "Remove" is the only path
  • EC-P16.2: + increases qty
  • EC-P16.3: Remove → item disappears
  • EC-P16.4: stop mismatch → "Choose Correct Stop" alert
  • EC-P16.5: incompatible items → "Remove Incompatible Items First" CTA
  • EC-P16.6: empty cart → "Cart is Empty" CTA
  • EC-P16.7: availability error → "Remove Incompatible Items First"
  • EC-P16.8: ESC closes stop picker
  • EC-P16.9: backdrop closes stop picker

AC-P17 — Checkout

  • Then customer details + stop select + shipping (if ship) + Stripe Express iframe

EC-P17:

  • EC-P17.1: missing email → blocked
  • EC-P17.2: no stop + ship items → blocked (?)
  • EC-P17.3: state field uppercased
  • EC-P17.4: "Use secure hosted checkout" → Stripe redirect (uses placeholder key, will fail in QA)
  • EC-P17.5: empty cart → "Back to storefront" link

AC-P18 — Auth API routes

  • /api/auth/sign-in: 400/401/500 surfaces error
  • /api/auth/sign-out: redirect /login
  • /api/auth/forgot-password: always success (anti-enumeration)
  • /api/auth/reset-password: 400 on <8 chars
  • /api/auth/change-password: auth required

Known cosmetic-only behaviors (NOT bugs)

  • /admin/launch-checklist checkboxes, "Mark All Complete", "Export PDF" are all no-ops (M11.6)
  • /admin/analytics "Export Report" is no-op (M9.1 EC-3)
  • /admin/analytics period selector doesn't refetch (M9.1 EC-1)
  • /admin/me profile + email change forms always show "temporarily unavailable" (AdminMeClient)
  • /admin/advanced/shipping and /admin/settings/square-sync/advanced are static mocks with setTimeout simulators
  • /admin/communications/compose, /contacts, /segments, /logs, /analytics all redirect to ?tab= query (preserved backward compat)
  • /blog/newsletter and /blog/download buttons have no handlers
  • /roadmap/suggestion form and upvotes have no handlers
  • /admin/orders/[id] does NOT expose "fulfilled" status (deliberate)

These are documented limitations, not bugs. Any change to them should be reviewed for product intent first.