0ac4beaaa8
- 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)
671 lines
22 KiB
Markdown
671 lines
22 KiB
Markdown
# 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)
|
||
|
||
### AC-R1 — dev_session cookie impersonates roles in dev mode
|
||
- **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
|
||
|
||
### AC-A3 — AdminSidebar nav links navigate correctly
|
||
- **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. |