# 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.