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