SAMPLE DOCUMENT — this is an interactive demonstration. Feel free to comment and test-sign; submissions are marked as demo.

Demosite — Full Scope & Requirements Register

"Your Brand" multi-entity fashion ops platform (Datacator demo site)

v1.2 2026-07-12 As-built baseline
DocumentDemosite — Full Scope & Requirements Register
Deploymenthttps://demo.stillnotbald.com (demo-mode, seeded, zero secrets)
Version / Date1.2 · 2026-07-12
MethodReverse-engineered from source code by 6 parallel read-only audit agents (18 pages + 2 API routes, domain stores, governance seams, packages). Every requirement is as-built and traces to file:line evidence recorded in audit/*.md. Priorities reflect the role each requirement plays in the product as implemented.

How to read this document

A scope area is the fence — what a capability covers, and what it deliberately doesn't. A requirement is a specific, testable behaviour inside that fence, checked directly against the running product. This is an as-built baseline: everything described here is already true of the product today. You can leave a comment anywhere on this page, and sign at the bottom once you're ready to approve it.

Document control

VersionDateChanges
1.02026-07-12Initial as-built baseline: 17 scope items, 97 requirements, 24 findings from the 6-agent reverse-engineering audit.
1.12026-07-12Findings cross-linked to the exact requirements they affect; deviation markers added to scope tree, workbook and sign-off; document control added.
1.22026-07-12Plain business-English layer added to every scope and requirement (client-facing interactive site); technical wording preserved for the collapsible detail views.

Scope summary

IDScope areaFunctionalNon-functional
SCP-0141
SCP-0262
SCP-0351
SCP-0452
SCP-0561
SCP-0671
SCP-0741
SCP-0841
SCP-0951
SCP-1031
SCP-1141
SCP-1240
SCP-1331
SCP-1450
SCP-1542
SCP-1651
SCP-1706

17 scope areas · 97 requirements (74 functional / 23 non-functional)

Scope areas

SCP-01 Home & Action Center 5 requirements

Gives every user a single prioritized to-do list of what needs attention right now, built automatically from real business data, with one click to jump straight to where the work happens.

What's included

  • Derived action items from 9 operational sources (stock-outs, low stock, pending approvals, open threads, draft orders, draft broadcasts, draft campaigns, stale customers, draft content)
  • Priority ranking and capping of the feed
  • Snapshot KPI tiles (products, units in stock, orders, customers)

What's not included

  • Any write action from the home page (all rows deep-link out)
  • User-configurable rules or thresholds
  • Push/email digest of the to-do list
REQ-01-F-01 Must Functional Your daily to-do list, built automatically

The home page automatically builds a to-do list from what's happening across your business — items out of stock or running low, approvals waiting on you, open customer conversations, and unfinished orders, broadcasts, campaigns, or content. Each item links straight to where you can act on it.

  • You can check this by confirming items that are completely out of stock, or down to 5 units or fewer, take you to the Inventory page when clicked
  • You can check this by confirming approvals waiting on you, open customer conversations, and unfinished orders each show up as one line with a live count
  • You can check this by confirming customers who haven't ordered in 45 days link straight to their customer page
Technical detail

The home page shall derive action items from nine sources — out-of-stock variants, low-stock variants, pending approvals, open customer threads, draft orders, draft broadcasts, draft campaigns, stale customers, and draft content pieces — each deep-linking to the owning page.

  • Out-of-stock (stock = 0) and low-stock (0 < stock <= 5) items link to /inventory
  • Pending approvals, open threads and draft orders each surface as one aggregated row with a live count
  • Stale customers (no order within 45 days) link to their /customers/{id} page
Evidence: src/app/page.tsx:116-253
REQ-01-F-02 Must Functional Most urgent items always float to the top

The to-do list is sorted so the most urgent problems appear first, no single type of issue can flood the list, and only the 8 most important items show at once.

  • You can check this by confirming urgent issues always appear above warnings, which appear above opportunities
  • You can check this by confirming the list never shows more than 8 items, with the very first one visually highlighted
  • You can check this by confirming no single category (like low stock or draft broadcasts) can take over more than 2 slots on the list (drafts are capped at 1)
Technical detail

Action items shall be ranked critical > warning > opportunity, capped per category (2 per item-level source, 1 for content drafts) and truncated to 8 total, with the top item visually emphasised.

  • Priority order is fixed: critical=0, warning=1, opportunity=2
  • Never more than 8 rows render; rank chips are 1-based with #1 inverted
  • Category caps hold: out-of-stock 2, low-stock 2, broadcasts 2, campaigns 2, stale customers 2, content 1
Evidence: src/app/page.tsx:47-55 · src/app/page.tsx:255-257
REQ-01-F-03 Should Functional At-a-glance business numbers

The home page shows four quick numbers: how many products you carry, how many units you have in stock, how many orders you've had, and how many customers you have.

  • You can check this by confirming all four number tiles appear below the to-do list
  • You can check this by confirming the numbers update correctly when switching between 'all locations' and a single location
Technical detail

The home page shall show four KPI tiles — product count, total units in stock, order count, customer count — computed from the actor's in-scope data.

  • Four tiles render below the priorities card
  • Values reflect entity scoping (All-entities mode widens them)
Evidence: src/app/page.tsx:434-439
REQ-01-F-04 Should Functional A reassuring message when there's nothing to do

When there's nothing that needs your attention, the page shows a friendly 'all caught up' message with a checkmark instead of leaving the screen looking empty or broken.

  • You can check this by clearing every action item and confirming the checkmark message appears
  • You can check this by confirming the 'start here' hint only shows when there are actual items to work through
Technical detail

When zero action items exist, the page shall show a positive empty state (checkmark + copy) instead of an empty list.

  • Empty state renders when items.length = 0
  • 'Start at top' hint only renders when items exist
Evidence: src/app/page.tsx:284-288 · src/app/page.tsx:305-328
REQ-01-NF-01 Must Non-functional Everyone only sees their own data, and the page works for everyone

Each staff member only sees to-do items for the business location(s) they're responsible for. The page is also built to be easy to tap on a phone and works fully in both English and Chinese.

  • You can check this by confirming a staff member assigned to one location never sees another location's items, while a head-office user viewing 'all locations' does see items across locations
  • You can check this by confirming every item on the list is large enough to comfortably tap on a phone screen
  • You can check this by confirming every piece of text on the page is available in both English and Chinese
Technical detail

All derivations shall respect entity/role scoping before computation; rows shall be tap-target compliant (min 44px) with combined aria-labels; all copy via typed i18n keys (EN/中文).

  • Entity-scoped roles only ever see their home entity's items; All-entities mode surfaces cross-entity items for group roles
  • Every row is a full-width button with minHeight 44 and an aria-label
  • No hardcoded user-facing strings
Evidence: src/app/page.tsx:96-103 · src/app/page.tsx:336-422
0 / 2000
Thanks — your comment was received.
SCP-02 Dashboard & Analytics 8 requirements

One dashboard that shows how the whole business, or any single location, is performing — sales, targets, top products, and trends — in whatever currency you choose.

What's included

  • Period selection (11 named ranges + custom) with comparison modes
  • KPI tiles with delta chips and sparklines
  • Charts: monthly revenue by entity, channel breakdown, best/slow sellers
  • Targets vs actual per entity; per-entity breakdown table for group roles
  • Reporting-currency switching with historical FX

What's not included

  • Forecasting or predictive analytics
  • Real COGS/margin accounting (margin is a simulated 40% constant)
  • Export of dashboard data
REQ-02-F-01 Must Functional Pick any time period to view

You can view performance for today, yesterday, this week, this month, this quarter, this year, all time, or any custom date range you choose. Your last choice is remembered the next time you visit.

  • You can check this by picking a quick option like 'this month' and confirming the numbers update instantly
  • You can check this by entering a custom range with the end date before the start date and confirming it falls back to showing all-time data rather than guessing
  • You can check this by refreshing the page and confirming your last-picked time period is still selected
Technical detail

The dashboard shall offer 11 named period ranges (today, yesterday, wtd, mtd, qtd, q1-q4, ytd, all) plus a custom inclusive date range; selections persist across reloads.

  • 6 one-tap segments + full select with all 11 keys + custom
  • Invalid/reversed custom ranges resolve to all-time, never a guessed range
  • Period and compare choices rehydrate from localStorage SSR-safely
Evidence: src/app/dashboard/page.tsx:446-507 · src/lib/demo/periods.ts:13-25 · src/lib/demo/periods.ts:169-176
REQ-02-F-02 Must Functional See whether you're doing better or worse than before

The dashboard can compare the current period to the previous period or to the same period last year, showing whether each number went up or down. If there's nothing to compare against, it honestly shows a dash instead of a misleading percentage.

  • You can check this by selecting a quarter and confirming it compares against the correct prior quarter, including across year boundaries
  • You can check this by picking a period with zero prior sales and confirming it shows a dash, not a fake percentage
  • You can check this by confirming the margin and target tiles never display a comparison arrow
Technical detail

The dashboard shall support vs-previous, vs-same-period-last-year and custom comparisons, calendar-aware (whole-month MoM, year-wrapping quarters), rendering delta chips on revenue/units/orders/AOV and an em-dash when the baseline is zero.

  • QTD Q1 'prev' compares to Q4 of the previous year
  • Baseline 0 renders '—', never a fake infinite percentage
  • Margin and target tiles never show a delta
Evidence: src/lib/demo/periods.ts:110-194 · src/lib/demo/periods.ts:207-210 · src/app/dashboard/page.tsx:391-406
REQ-02-F-03 Must Functional Six key numbers on your business health

The dashboard displays six core numbers for the selected period: total sales, units sold, number of orders, average order value, profit margin percentage, and how close you are to your sales target.

  • You can check this by confirming all six tiles show a value for whatever period you've selected
  • You can check this by confirming the sales and units tiles include a small trend line
Technical detail

The dashboard shall render six KPI tiles — revenue, units sold, orders, AOV, gross margin %, target achievement % — with sparklines on revenue/units.

  • All six tiles render for the selected period
  • Sparklines derive from monthly series summed across entities
Evidence: src/app/dashboard/page.tsx:582-632 · src/app/dashboard/page.tsx:323-335
REQ-02-F-04 Must Functional Trend, sales channel, and top/bottom product charts

The dashboard shows a long-term monthly sales trend for each location, a breakdown of where sales come from (retail store, website, social platforms, etc.), and your 5 best-selling and 5 slowest-selling products.

  • You can check this by confirming the monthly trend chart always shows the full history, regardless of which time period is selected elsewhere on the page
  • You can check this by confirming the sales-channel chart only shows real recorded sales, never made-up numbers
  • You can check this by confirming products with zero sales still appear in the slow-movers list, since that's useful to know
Technical detail

The dashboard shall show a long-window monthly revenue line per entity (deliberately not period-filtered), a channel breakdown from real per-order attribution (channels with revenue > 0 only), and best-sellers top 5 / slow-movers bottom 5 panels.

  • Monthly chart uses all orders regardless of period picker
  • Channel chart never shows synthetic data
  • Slow-movers includes 0-unit stocked products (zero rows are the story)
Evidence: src/app/dashboard/page.tsx:337-387 · src/lib/demo/dashboard.ts:264-371
REQ-02-F-05 Must Functional Targets versus what you actually sold

The dashboard shows each location's sales target against actual sales, color-coded green when you've hit the target, yellow when you're close, and red when you're falling short.

  • You can check this by confirming a head-office user sees one row per location, while a location-level user only sees their own location, in their own local currency
  • You can check this by confirming the monthly targets are Malaysia RM500,000, China ¥150,000, and International $20,000
Technical detail

The dashboard shall show per-entity target achievement with colour thresholds (>=100% success, >=70% warning, <70% danger) and an overshoot marker beyond 100%.

  • One row per entity for group roles; entity roles see only their own entity in local currency
  • Monthly targets: MY MYR 500,000; CN CNY 150,000; INTL USD 20,000
Evidence: src/app/dashboard/page.tsx:161-228 · src/lib/demo/dashboard.ts:32-36
REQ-02-F-06 Must Functional View company-wide numbers in the currency you prefer

Head-office users can switch the whole dashboard between US dollars, Malaysian ringgit, Chinese yuan, or Singapore dollars. Each past sale is converted using the exchange rate from the date it actually happened, not today's rate, so historical numbers stay accurate.

  • You can check this by switching currencies and confirming every figure on the dashboard updates
  • You can check this by confirming a sale from months ago converts using the exchange rate from that date, not today's
Technical detail

Group users shall switch reporting currency (USD/MYR/CNY/SGD); conversion uses the historical rate on-or-before each transaction date with USD as pivot when a direct pair is missing.

  • Currency pills switch all group figures
  • FX lookup is by transaction date, not today's rate
Evidence: src/app/dashboard/page.tsx:556-579 · src/lib/demo/fx.ts:111-175
REQ-02-NF-01 Must Non-functional Only real, completed sales count toward revenue

Every revenue figure on the dashboard is calculated the same consistent way: only orders that have been paid or fulfilled count as revenue. Draft, cancelled, and refunded orders never inflate the numbers.

  • You can check this by confirming only paid and fulfilled orders are counted in any revenue figure
  • You can check this by confirming a draft, cancelled, or refunded order never adds to revenue anywhere on the dashboard
Technical detail

All dashboard math shall count only paid/fulfilled orders via the shared isRevenueOrder() predicate — one definition for every revenue-reporting surface.

  • REVENUE_STATUSES = {paid, fulfilled} filters orders before any aggregation
  • Draft/cancelled/refunded orders never contribute to revenue
Evidence: src/lib/domain/types.ts:164-177 · src/lib/demo/dashboard.ts:150
REQ-02-NF-02 Must Non-functional Accurate money math and clear loading/empty states

All money calculations are done in whole cents internally so there's never a rounding error, and every section of the dashboard shows a loading animation while data loads and a clear message when there's no data to show.

  • You can check this by confirming money figures never show rounding artifacts
  • You can check this by picking a period with zero orders and confirming the charts show a clear 'no data' message instead of appearing broken or blank
Technical detail

All money math shall be integer minor units (float only at final display); every section shall have a loading skeleton and a descriptive empty state; reduced-motion preferences disable bar animations.

  • No float arithmetic before the final /100 display step
  • Zero-order periods render ChartEmpty, never NaN or blank panels
Evidence: src/lib/demo/dashboard.ts:4-5 · src/app/dashboard/page.tsx:141-159 · src/app/dashboard/page.tsx:430-432
0 / 2000
Thanks — your comment was received.
SCP-03 Goals & Tasks 6 requirements

Lets managers set sales targets for their team members and track progress, plus a simple task board for assigning and tracking day-to-day work — all with a full history of who did what.

What's included

  • Goal creation, legal-transition status updates and idempotent period evaluation
  • Kanban task board with assignment, priority, due dates and linked goals
  • Recent goal/task audit activity

What's not included

  • Automatic bonus payout (bonus multiplier is recorded, not paid here)
  • Cross-period goal rollups or team-level OKRs
REQ-03-F-01 Must Functional Managers set goals for their team

Managers can set a monthly goal for a team member — a sales target, a units-sold target, or a custom goal — and optionally attach a bonus multiplier between 100% and 200% of the standard bonus.

  • You can check this by confirming non-manager staff see a read-only view instead of the option to create or evaluate goals
  • You can check this by confirming the team member, a label, and the month are always required when creating a goal
Technical detail

Managers (group_admin, entity_manager) shall create member goals of type revenue_target, units_target or custom for a YYYY-MM period, with an optional bonus multiplier constrained to 10000-20000 bps.

  • Non-managers see a read-only badge instead of create/evaluate actions
  • Member, label and period are required; target field is conditional on type
Evidence: src/app/goals/page.tsx:34 · src/app/goals/page.tsx:408-558 · src/lib/people/goals-store.ts:43-46
REQ-03-F-02 Must Functional Goals can only move through valid stages

A goal starts active and can only end as achieved, missed, or cancelled — once it reaches one of those, it's locked and can't be changed again.

  • You can check this by confirming the status options disappear once a goal reaches achieved, missed, or cancelled
  • You can check this by confirming the system rejects any attempt to move a goal to an invalid stage
Technical detail

Goals shall transition only per GOAL_TRANSITIONS: active -> achieved | missed | cancelled, all three terminal; the UI offers only legal next statuses.

  • Status menu disappears once a goal is terminal
  • Illegal transitions are impossible from the UI and rejected by the store
Evidence: src/lib/domain/people-goals-types.ts:93-98 · src/components/people/GoalCard.tsx:199-271
REQ-03-F-03 Must Functional One-click goal check-up, safe to run more than once

Managers can recalculate whether each active goal was hit, based on that month's actual sales, and the system automatically marks it achieved or missed. Running the check again doesn't create duplicate records or change anything if nothing new has happened.

  • You can check this by confirming a sales-target goal is marked achieved only once actual sales reach the target amount
  • You can check this by running the evaluation twice in a row with no new sales and confirming nothing changes the second time
Technical detail

Evaluate-period shall recompute active goals' actuals from that entity's orders for the current month and flip status to achieved (actual >= target) or missed; custom goals are never auto-evaluated; re-running only writes when something changed.

  • revenue_target achieved iff period revenue >= targetMinor; units analogous
  • Second evaluation run with no data change writes no audit rows
Evidence: src/lib/people/goals-store.ts:346-435
REQ-03-F-04 Must Functional A simple task board for the team

Tasks move through a fixed set of stages — to do, in progress, blocked, and done — and the person a task is assigned to can update its status themselves, even without manager permissions. Overdue tasks are flagged automatically.

  • You can check this by confirming the board always shows 4 columns with a live count of tasks in each
  • You can check this by confirming the assigned person can move their own task without needing manager access
  • You can check this by confirming a task past its due date and not yet done is visibly flagged as overdue
Technical detail

Tasks shall move only per TASK_TRANSITIONS (open <-> in_progress <-> blocked, any-to-done paths as defined, done terminal); the task's own assignee may update status even without a manager role; overdue open tasks are flagged.

  • 4 fixed columns with live counts
  • Assignee (matched by email) can transition their own task
  • Overdue flag when dueDate < now and status != done
Evidence: src/lib/domain/people-goals-types.ts:102-107 · src/lib/people/goals-store.ts:592-603 · src/components/people/TaskBoard.tsx:60-63
REQ-03-F-05 Should Functional Recent activity log

The page shows the 10 most recent goal and task changes for whatever team or location you're viewing, newest first.

  • You can check this by confirming the activity list never shows more than 10 rows
  • You can check this by confirming you only see activity for your own location unless you have company-wide access
Technical detail

The page shall show the last 10 goal.*/task.* audit rows for the actor's scope, newest first.

  • Panel capped at 10 rows
  • Rows respect entity scoping
Evidence: src/app/goals/page.tsx:273-287
REQ-03-NF-01 Must Non-functional You only see and change your own team's goals

Staff can only see goals and tasks for their own location, unless they have company-wide access. Any change they make is automatically tied to their own workspace, so it can't accidentally affect another location.

  • You can check this by confirming finance and support staff never see options to create or change goals/tasks, regardless of what they can view
  • You can check this by confirming every change made is automatically recorded against the correct location and person
Technical detail

Goal/task reads shall be entity-scoped with group-read widening for group roles; writes always pin to the actor's home workspace via assertCanWriteEntity.

  • finance/ops_cs never see write actions regardless of scope
  • All store writes assert workspace + role
Evidence: src/lib/people/goals-store.ts:210-238 · src/lib/people/goals-store.ts:259
0 / 2000
Thanks — your comment was received.
SCP-04 Inventory Management 7 requirements

One place to manage every product, its stock levels, and its price across colors and sizes, with built-in safety checks so stock never goes negative and price changes get a second sign-off.

What's included

  • Unified view with 3 view modes, filters and bulk selection
  • Colour x size stock matrix with low-stock signalling
  • Product composer and per-product variant creation
  • Role-routed cell editing and bulk edit (stock direct, price four-eyes gated)

What's not included

  • Purchase orders / replenishment
  • Warehouse locations or transfers between entities
  • Cost-price (COGS) management
REQ-04-F-01 Must Functional One screen for products, stock, and prices

The inventory page shows every product with its stock and price together, in your choice of three layouts (large cards, medium cards, or a list), with filters for text, category, color, size, and stock status.

  • You can check this by switching layouts and confirming your choice is remembered next time you visit
  • You can check this by confirming the page tells you clearly whether there are simply no products yet, or your filters just don't match anything
  • You can check this by confirming a product with different prices across its variants shows a price range rather than one misleading number
Technical detail

Inventory shall present products, variants, stock and price in one unified view with XL-card / medium-card / list modes (persisted per page), text/category/colour/size/stock-status filters, removable chips and a live result count.

  • View mode persists in localStorage across reloads
  • Empty state distinguishes 'no products' from 'no filter matches'
  • Price shows a min-max range when variants differ
Evidence: src/app/inventory/page.tsx:233-309 · src/components/ViewToggle.tsx:16-39 · src/app/inventory/page.tsx:547-553
REQ-04-F-02 Must Functional See stock by color and size at a glance

Each product can be expanded into a grid showing stock for every color and size combination, with red highlighting sold-out items and yellow highlighting low stock (5 units or fewer).

  • You can check this by confirming any combination with 5 or fewer units is highlighted yellow, and 0 units is highlighted red
  • You can check this by confirming a color/size combination the product doesn't even offer shows a dash, not a misleading zero
  • You can check this by confirming sizes always appear in standard order from smallest to largest
Technical detail

Each product shall expand to a colour x size matrix, one cell per variant, colour-coded (0 = danger, 1-5 = warning, else neutral) and distinguishing 'not offered' (em-dash) from 'sold out' (0).

  • LOW_STOCK_THRESHOLD = 5 drives the warning tint
  • Combinations the product doesn't offer render as em-dash, not zero
  • Sizes sort in fixed garment order (XXS..3XL, OS last)
Evidence: src/components/inventory/StockMatrix.tsx:10-81 · src/lib/domain/variant-attrs.ts:38-55
REQ-04-F-03 Must Functional Add a new product in one step

Admins can create a brand-new product and generate every color and size combination it comes in, with starting stock levels for each, all in a single submission. The system shows a live preview of how many variants will be created and won't allow two products with the same product code.

  • You can check this by creating a product with 3 colors and 4 sizes and confirming 12 variants are created in one go
  • You can check this by confirming trying to reuse an existing product code within the same company is rejected
Technical detail

Admins shall create a product in one submit that generates a variant for every selected colour x size pair plus an initial inventory row per variant, with a live variant-count preview; duplicate SKUs per group are rejected.

  • One submit creates product + N variants + N inventory rows
  • createProduct throws duplicate_sku on (groupId, sku) collision
Evidence: src/components/inventory/ProductComposer.tsx:56-108 · src/lib/demo/repository.ts:853-859
REQ-04-F-04 Must Functional Different staff get different editing tools

Clicking a stock cell gives managers a full edit form for stock and price, gives support staff a simpler +/- adjustment tool that won't let stock go negative, and shows finance staff a read-only view.

  • You can check this by confirming finance staff can never edit a stock cell
  • You can check this by confirming support staff can't submit an adjustment that would push stock below zero
Technical detail

Clicking a matrix cell shall route by role: group_admin/entity_manager get the stock+price upsert form; ops_cs gets a delta-adjust panel that blocks negative resultant stock; finance sees read-only cells.

  • Finance never gets an editable cell
  • Adjust panel validates currentStock + delta >= 0 before submit
Evidence: src/app/inventory/page.tsx:823-843 · src/components/inventory/StockAdjustPanel.tsx:64-99
REQ-04-F-05 Must Functional Bulk updates, with a second sign-off required for price changes

Staff can update stock or prices for many items at once. Stock changes apply immediately, but price changes must be requested by a manager and approved by a different person (a manager or finance) before they take effect — no one can approve their own price request.

  • You can check this by confirming a bulk stock change applies right away, while a bulk price change goes into an approval queue first
  • You can check this by confirming the person who requested a price change cannot also approve it
  • You can check this by confirming an approved price change applies correctly, and any item that was deleted in the meantime is reported as skipped rather than silently ignored
Technical detail

Bulk edit shall support setStock/adjustStock (direct writes) and setPrice/adjustPricePct (routed into the four-eyes approvals queue); only group_admin/entity_manager may request price changes, only group_admin/finance may approve, and the approver must differ from the requester.

  • Stock modes write immediately; price modes create an approval request
  • Approving your own request throws a four-eyes violation
  • Approved intents re-apply as the original requester's authority; deleted variants are reported as skipped, not silently ignored
Evidence: src/components/inventory/BulkEditForm.tsx:89-114 · src/lib/people/approvals-store.ts:306-341 · src/lib/people/approval-appliers.ts:88-110
REQ-04-NF-01 Must Non-functional Stock can never go negative, checked three separate times

The system checks in three different places that stock can never drop below zero, so even if one check were somehow bypassed, the others would still catch it.

  • You can check this by attempting to remove more stock than is available through any of the editing tools and confirming it's always blocked
  • You can check this by confirming this holds true no matter which of the three editing paths (form, adjustment panel, or bulk edit) is used
Technical detail

Stock shall never go negative: clamped in the bulk form, validated in the adjust panel, and re-enforced by the repository which throws if a delta would result below zero.

  • All three layers verified in code
  • UI bypass still cannot produce negative stock
Evidence: src/components/inventory/BulkEditForm.tsx:128-131 · src/lib/demo/repository.ts:1728-1733
REQ-04-NF-02 Must Non-functional Product catalogue is shared, but stock is per-location

The product catalogue (names, colors, sizes) is shared across all your locations, but each location has its own separate stock and pricing. Every change requires the right permissions for that specific location.

  • You can check this by confirming finance staff can view but never edit anything on this page
  • You can check this by confirming a head-office user viewing 'all locations' can see everything but still can't edit a location they don't manage
Technical detail

Products/variants shall be group-scoped (shared catalogue across entities); inventory rows (stock+price) shall be entity-scoped; all writes require workspace match plus a write role.

  • finance is read-only everywhere on this surface
  • All-entities mode widens reads for group roles without widening writes
Evidence: src/lib/seam/rls.ts:36-40 · src/lib/domain/types.ts:4-13
0 / 2000
Thanks — your comment was received.
SCP-05 Data Import & Ingestion 7 requirements

Bring in bulk data from spreadsheets or a connected sales channel without creating duplicates, with a safe preview step before anything is saved.

What's included

  • 5-step CSV wizard (upload, map, validate, dry run, commit) for 5 entity types
  • Column alias auto-mapping (EN + 中文 + Shopify headers)
  • Two-pass dedup and idempotent commit
  • Demo Shopify connector with 2 routing topologies
  • Manual SKU creation and stock adjustment

What's not included

  • Real marketplace/storefront API calls (connector is a static fixture)
  • Scheduled/automatic imports
  • Four-eyes approval on import commits (deliberate: direct writes once role-gated — see FND-05)
REQ-05-F-01 Must Functional Guided 5-step spreadsheet import

You can upload a spreadsheet (CSV file, up to 5MB) of products, stock, customers, or orders, match its columns to the right fields, preview exactly what will change, and only then confirm the import. Nothing is saved until you approve the final step.

  • You can check this by confirming a file that isn't a CSV, or is over 5MB, is rejected upfront
  • You can check this by re-running an already-completed import and confirming it tells you it was already done instead of importing everything twice
  • You can check this by starting an import, leaving the page, and confirming your progress is saved when you return
Technical detail

The bulk importer shall guide upload (CSV only, 5MB cap) -> column map (alias auto-suggest) -> validate (dry-run, per-row errors, downloadable error CSV) -> dry-run summary (new/updated/duplicate/invalid/conflict) -> idempotent commit, for entity types product, variant, inventory, customer, order.

  • Non-CSV or >5MB uploads are rejected
  • Already-committed batches surface 'already committed' instead of re-running
  • Wizard draft persists per entity and survives reload
Evidence: src/components/import/BulkImportWizard.tsx · src/lib/demo/repository.ts:1616-1648
REQ-05-F-02 Must Functional Bad rows are caught before anything is saved

Each type of import checks for the required information (for example, products need a product code and name; customers need a name and a valid email) and flags any row that's missing something, so you can fix it before importing. Column headers in English, Chinese, or from common sales platforms are recognized automatically.

  • You can check this by uploading a row missing a required field and confirming it's flagged with a clear reason
  • You can check this by uploading a spreadsheet with Chinese column headers and confirming they're matched automatically
Technical detail

Each entity type shall enforce required fields (product/variant: sku + name; inventory: sku with non-negative stock/price if present; customer: name + valid email; order: order_no or external_ref); prices parse via a symbol-stripping heuristic supporting European formats and accounting negatives.

  • Rows failing validation classify as invalid with row-level errors
  • Alias dictionary maps EN/中文/Shopify headers case-insensitively
Evidence: src/lib/ingest/staging.ts:38-73 · src/lib/ingest/staging.ts:156-204 · src/lib/ingest/aliases.ts:23-190
REQ-05-F-03 Must Functional Duplicates are caught automatically

The import process automatically detects duplicate rows within the same file and checks against existing records, so you always see clearly which rows are brand new, which are updates to existing records, and which are exact duplicates that will be skipped.

  • You can check this by including the same row twice in one file and confirming only the first copy is kept
  • You can check this by re-importing a record with no changes and confirming it's marked as a duplicate, not imported again
  • You can check this by running a preview and confirming it never actually saves anything until you commit
Technical detail

Resolution shall run two passes: intra-batch content-hash dedup (first occurrence wins) then cross-run natural-key lookup classifying rows as new / updated / duplicate / invalid; natural keys are namespaced by source so the same order from CSV and Shopify never collides.

  • Identical rows within one batch mark later copies duplicate
  • Existing record with no material diff classifies duplicate; with a diff, updated
  • dry_run never writes; commit failures downgrade the row to invalid with the error attached
Evidence: src/lib/ingest/resolve.ts · src/lib/ingest/staging.ts:122-152
REQ-05-F-04 Should Functional Demo connector to a sales platform

The system includes a demo connection that simulates pulling in orders and products from an online store, with a preview mode and a choice of how to route the incoming data across your locations. This is a demonstration only — no real store is contacted.

  • You can check this by confirming no live internet connection to a real store is ever made
  • You can check this by running the sync twice and confirming the second run correctly identifies already-imported records as duplicates
Technical detail

The Shopify panel shall sync a deterministic mock payload in dry-run or commit mode with two routing topologies: per_entity (all records to one workspace) or shared_split (route by tag), reporting summary pills.

  • No real network calls are made
  • Repeat syncs classify re-ingested rows as duplicates via natural-key dedup
Evidence: src/components/import/ShopifyConnectorPanel.tsx
REQ-05-F-05 Should Functional Add a single product or adjust stock by hand

Staff can manually create a single new product code with its stock, or adjust an existing item's stock up or down with an optional note explaining why. Every manual change is recorded in the history.

  • You can check this by confirming a new product code requires at minimum a code, name, and variant label
  • You can check this by confirming a stock adjustment requires an actual non-zero amount, and the reason you enter is saved with it
Technical detail

Manual entry shall create a SKU (product + variant + optional inventory in one audited call tagged provenance:manual) and adjust stock by a signed delta with an optional reason threaded into the audit row.

  • New SKU requires sku + name + variant label
  • Stock adjust requires a non-zero integer delta; audit row records {delta, prevStock, newStock, reason}
Evidence: src/lib/demo/repository.ts:1660-1704 · src/lib/demo/repository.ts:1712-1765
REQ-05-F-06 Should Functional History of past imports

A read-only list shows every past import for your current location, so you can see what was imported and when.

  • You can check this by confirming the history only shows imports for the location you're currently viewing
Technical detail

A read-only batch history table shall list past ingest runs for the current entity below all tabs.

  • History is scoped to the current entity only, even for group roles
Evidence: src/components/import/IngestHistoryTable.tsx:55-58
REQ-05-NF-01 Must Non-functional Import permissions are enforced, not just suggested

Only the right roles can import or adjust data, and this is checked both on the screen and again behind the scenes, so it can't be bypassed. Finance staff can only view, never import.

  • You can check this by confirming finance staff see a locked, disabled import screen
  • You can check this by confirming every import is tied to the correct location and can't accidentally affect another one
Technical detail

Import writes shall be role-gated (group_admin/entity_manager/ops_cs; finance read-only) at BOTH the UI layer and inside every repository method, so a bypassed UI still cannot write; every batch is pinned to the actor's workspace.

  • bulkIngest/shopifyIngest/commitBatch/createSkuWithVariant/adjustStock all assert workspace + role server-side
  • Read-only roles see a lock banner and disabled controls
Evidence: src/app/import/page.tsx:33 · src/lib/seam/rls.ts:77-79
0 / 2000
Thanks — your comment was received.
SCP-06 Sales Ledger & Order Lifecycle 8 requirements

A complete, trustworthy record of every sale — who sold it, to whom, through which channel — with safe order creation, a controlled order lifecycle, and refunds that always require a second person's sign-off.

What's included

  • Attributed order list with search, filters and inspector/detail views
  • Order creation with stock-safe lines across 7 channels
  • Order state machine with stock restoration on cancel/refund
  • Refunds via the four-eyes approvals queue
  • Per-order audit trail and notifications

What's not included

  • Payment processing / gateways (statuses model payment, no charge occurs)
  • Shipping/fulfilment logistics beyond the fulfilled status
  • Invoice/receipt document generation
REQ-06-F-01 Must Functional Every sale traced to who made it and how

The sales list shows every order with the date and time, customer, which staff member made the sale (or a clear 'self-serve' label if no one did), which of the 7 sales channels it came through, how many items, the total, and its current status.

  • You can check this by confirming self-serve orders always show a clear label instead of a blank space
  • You can check this by confirming all 7 channels — retail store, website, TikTok Shop, Shopee, Lazada, Instagram, and WhatsApp — display with their own distinct badge
Technical detail

The ledger shall list in-scope orders newest-first with full attribution per row: date+time, order id, customer, salesperson (or an explicit 'unattributed/self-serve' badge), channel badge (7 channels), item count, total and status.

  • salespersonId = null renders the unattributed badge, never a blank
  • All 7 channels (retail-store, website, tiktok-shop, shopee, lazada, instagram, whatsapp) render distinct badges
Evidence: src/app/sales/page.tsx:150-215 · src/components/sales/ChannelBadge.tsx:13-43
REQ-06-F-02 Must Functional Find any order quickly

You can search orders by order number or customer name as you type, and filter by sales channel, salesperson, status, or date range, with a running count of matching results.

  • You can check this by confirming there's a specific filter option for orders with no salesperson attached
  • You can check this by confirming the page tells you clearly whether there are simply no orders yet, or your filters just don't match anything
Technical detail

The ledger shall support debounced (200ms) text search over order id and customer name, plus channel/salesperson/status/date-range filters with removable chips, a result count, and a mobile filter sheet.

  • Salesperson filter includes an explicit 'unattributed' bucket
  • Distinct empty states for 'no orders' vs 'no filter matches'
Evidence: src/app/sales/page.tsx:86-143 · src/app/sales/page.tsx:243
REQ-06-F-03 Must Functional Order details look the same everywhere

Clicking any order opens the same detailed view whether it opens as a side panel or its own full page — order summary, items, status history, and activity log are identical either way.

  • You can check this by opening the same order both ways and confirming they show identical information
  • You can check this by looking up an order that doesn't exist and confirming it shows a clear 'not found' message
Technical detail

Clicking a row shall open a 3-panel inspector; a deep-link icon opens /sales/{id}; both surfaces render the identical shared blocks (summary, lines, transitions, audit trail, notifications) from the same components.

  • Same component set on both surfaces
  • Unknown order id renders a dedicated not-found card
Evidence: src/components/sales/OrderInspectorBody.tsx:27-58 · src/app/sales/[id]/page.tsx:62-119
REQ-06-F-04 Must Functional Orders can't be created if stock isn't available

Before an order is created, the system checks that every item on it is actually in stock. Once confirmed, stock is reduced and the order is recorded as one single, safe step, then the customer and staff are notified.

  • You can check this by attempting to order more of an item than is in stock and confirming it's blocked with a clear message before anything is saved
  • You can check this by confirming finance staff don't see the option to create a new order
Technical detail

Order creation shall validate stock across ALL lines before any mutation, price lines from live inventory, create at draft, atomically decrement stock + write the audit row as one transaction, then notify (in-app + email); quantities are UI-bounded by live stock AND re-validated server-side.

  • Oversell attempt throws before any mutation and maps to a translated error
  • Order creation is hidden for the finance role
Evidence: src/lib/demo/repository.ts:1312-1399 · src/components/CreateOrderForm.tsx:53-167
REQ-06-F-05 Must Functional Orders move through a controlled set of stages

An order can only move from draft to paid or cancelled, then from paid to fulfilled or refunded, and from fulfilled to refunded. Cancelling or refunding an order automatically puts the stock back, and every single stage change is recorded in the history.

  • You can check this by confirming an invalid stage change (like going straight from draft to fulfilled) is blocked
  • You can check this by cancelling or refunding an order and confirming the stock is restored as part of that same action, not a separate step that could be missed
Technical detail

Orders shall transition only per ORDER_TRANSITIONS (draft -> paid | cancelled; paid -> fulfilled | refunded; fulfilled -> refunded; cancelled/refunded terminal); reaching cancelled or refunded restores each line's stock inside the SAME atomic transition, and every transition writes one audit row and fires a notification.

  • Illegal transitions throw
  • Stock restoration is part of the same transaction, flagged stockRestored in the audit payload
Evidence: src/lib/domain/types.ts:156-186 · src/lib/demo/repository.ts:1228-1303
REQ-06-F-06 Must Functional Refunds always need a second person's approval

No refund happens automatically. A manager requests it, and it only goes through once a different manager or finance staff member approves it. While it's waiting, the order clearly shows 'refund pending.'

  • You can check this by confirming there is no way to refund an order without going through the approval request
  • You can check this by confirming the person who requested the refund cannot also be the one who approves it
Technical detail

Refunds shall never transition directly: selecting 'refunded' files an order.refund approval request (group_admin/entity_manager only); while one is pending the refund chip is hidden with a 'refund pending' notice; approval (group_admin/finance, different person) re-applies the transition under the maker's authority.

  • No code path transitions an order to refunded without an approved request
  • Approver = requester throws a four-eyes violation
Evidence: src/components/sales/OrderTransitionSection.tsx:110-149 · src/lib/people/approvals-store.ts:336-341 · src/lib/people/approval-appliers.ts:63-85
REQ-06-F-07 Should Functional Returning customers are automatically marked active again

If a customer who had gone quiet places a new order, the system automatically marks them as active again — this is the only status change in the whole system that happens automatically, and it's fully recorded.

  • You can check this by confirming the customer's activity history shows the reason as 'won back by order' with the order number
  • You can check this by confirming this doesn't trigger a duplicate or unnecessary notification
Technical detail

After a sale, if the customer's status is not active/vip and 'active' is a legal next state, the system shall silently transition the customer to active via the audited funnel (the only automatic status change in the system).

  • Transition is audited with reason 'won back by order {id}'
  • No redundant notification fires
Evidence: src/lib/demo/repository.ts:1401-1412
REQ-06-NF-01 Must Non-functional Every order change is recorded in a tamper-proof history

Every change to an order and its history entry are saved together as one action — if anything goes wrong partway through, nothing is saved at all, so the records can never end up out of sync. Money is always calculated in exact cents, never rounded incorrectly.

  • You can check this by confirming that if the history entry can't be written, the order change itself is also cancelled, not left half-done
  • You can check this by confirming order totals never show rounding errors
Technical detail

Every order mutation shall run through the transitionStatus seam (mutation + audit all-or-nothing with rollback); money is integer minor units end to end; notifications are fail-soft after commit.

  • Audit-write failure rolls back the mutation and rethrows
  • No float math in order totals
Evidence: src/lib/seam/transition.ts:36-55 · src/lib/domain/money.ts
0 / 2000
Thanks — your comment was received.
SCP-07 Sales Commission 5 requirements

Automatically calculates each salesperson's commission based on rules you set, then routes it through an approve-and-pay workflow.

What's included

  • Commission rules (3 bases, member-specific or entity-wide, effective windows)
  • Idempotent period computation
  • Entry status workflow (pending/approved/paid/disputed)
  • Member summaries and drill-down

What's not included

  • Payout execution (payroll consumes approved commission)
  • Clawbacks or negative commission
  • Tiered/accelerator commission structures
REQ-07-F-01 Must Functional Flexible commission rules

Managers can set commission rules as a percentage of sales, a flat amount per order, or a flat amount per unit sold — for the whole team or one specific person — with an optional start and end date. A rule for a specific person always takes priority over a general team rule.

  • You can check this by confirming a rule set for one specific salesperson overrides the general team rule for that person
  • You can check this by confirming the commission rate must be a positive number, and creating a rule is recorded in the history
Technical detail

Managers shall create rules with basis pct_revenue (basis points), flat_per_order or flat_per_unit, optionally member-specific (member rule beats entity-wide) and bounded by effective-from/to dates.

  • Rule resolution honours workspace, member-over-entity precedence and the effective window
  • Rate must be a positive integer; creation is audited
Evidence: src/lib/people/commission-store.ts:207-228 · src/lib/people/commission-store.ts:311-358
REQ-07-F-02 Must Functional One-click commission calculation, safe to re-run

Managers can calculate commissions for the month with one click. Running it again doesn't create duplicate payouts for the same order and salesperson, and anyone with no applicable rule or a zero payout is simply skipped.

  • You can check this by clicking calculate twice in a row and confirming no duplicate entries are created
  • You can check this by confirming a percentage-based commission always rounds down, in the business's favor
Technical detail

Compute-period shall create at most one entry per (order, member) pair for the month — re-runs are safe — skipping members with no applicable rule and amounts <= 0; pct_revenue amounts floor-round in the business's favour.

  • Double-clicking compute creates no duplicates
  • pct_revenue = floor(orderTotal * bps / 10000)
Evidence: src/lib/people/commission-store.ts:369-455 · src/lib/people/commission-store.ts:230-241
REQ-07-F-03 Must Functional Commission payouts go through approval before payment

Each commission payout starts as pending, can be approved or disputed, and only an approved payout can be marked as paid. Only a manager or finance can approve or pay, and the salesperson is notified at each step.

  • You can check this by confirming an invalid change (like marking a pending entry as paid without approving it first) is blocked
  • You can check this by confirming the salesperson gets a notification once their commission is approved and again once it's paid
Technical detail

Entries shall transition only per ENTRY_TRANSITIONS (pending -> approved | disputed; approved -> paid | disputed; disputed -> pending; paid terminal); approve/pay requires group_admin or finance and notifies the member.

  • Illegal transitions throw
  • approved/paid fire an in-app + email notification to the member
Evidence: src/lib/people/commission-store.ts:61-66 · src/lib/people/commission-store.ts:462-547
REQ-07-F-04 Should Functional Commission totals per salesperson

The page shows each salesperson's total commission for the selected month, with a status badge showing their most urgent item (a disputed entry always takes priority for visibility), and lets you drill into the details.

  • You can check this by confirming a salesperson with even one disputed entry shows a disputed badge, not a paid one
  • You can check this by confirming the recent activity log shows up to 8 recent commission changes
Technical detail

The page shall summarise totals per member for the selected month with a worst-case status badge (disputed ranks worst) and drill into per-entry detail with inline approve/pay actions.

  • Severity order paid < approved < pending < disputed
  • Recent commission.* audit strip caps at 8 rows
Evidence: src/lib/people/commission-store.ts:272-303 · src/app/commission/page.tsx:346-352
REQ-07-NF-01 Must Non-functional Only the right people can set rules or approve payouts

Only managers can create commission rules and run calculations. Only managers or finance can approve and pay out commissions. Everyone else can only view, and always only for their own location unless they have company-wide access.

  • You can check this by confirming support staff can view but never create rules or approve payouts on this page
  • You can check this by confirming the same role rules apply consistently everywhere this page is used
Technical detail

Rule creation and computation shall require group_admin/entity_manager; entry approval/payment shall require group_admin/finance; all reads entity-scoped with group-read widening.

  • Role sets mirrored identically in page and store
  • ops_cs is read-only on this surface
Evidence: src/lib/people/commission-store.ts:39-54
0 / 2000
Thanks — your comment was received.
SCP-08 Payroll 5 requirements

Runs payroll for each location, generating a payslip for every team member from their salary plus approved commission, with a required second sign-off before anyone gets paid.

What's included

  • Run creation with payslip generation (base + commission - deductions)
  • Run state machine with store-enforced four-eyes approval
  • Payslip drawer with transitions, reasons and run-scoped audit log
  • Payroll config display (frequency, day, currency)

What's not included

  • Statutory deductions/tax computation (deductions are 0 in the demo)
  • Bank file generation or actual disbursement
  • Payroll config editing (display only; changes are a gated approval action type)
REQ-08-F-01 Must Functional One click generates every team member's payslip

Creating a payroll run generates a payslip for every team member at that location, combining their base salary with their approved commission for the month. Once created, a payslip is locked in — it won't change even if commission figures change later. You can't accidentally create two payroll runs for the same location and month.

  • You can check this by confirming a payslip's numbers stay the same even if commission amounts are later recalculated
  • You can check this by trying to create a second payroll run for a location/month that already has one and confirming it's blocked with a reference to the existing run
Technical detail

Creating a run shall generate one payslip per entity member: base salary from the seeded table (entity default fallback) + commission pulled from the commission store for that period - deductions (0), computed once as a snapshot; a non-voided run already existing for (entity, month) blocks creation.

  • gross = base + commission - deductions at creation time; later commission changes don't mutate existing runs
  • Duplicate-run guard throws with the existing run's id
Evidence: src/lib/people/payroll-store.ts:335-439 · src/lib/people/payroll-store.ts:343-354
REQ-08-F-02 Must Functional Payroll runs require a second person's sign-off, enforced by the system

A payroll run moves from draft to pending approval, then to approved, then to paid (or can be voided at most stages). Whoever created the run can never approve their own run — this is enforced by the system itself, not just hidden in the interface.

  • You can check this by confirming the person who created a payroll run sees the Approve button disabled with a clear explanation
  • You can check this by confirming the system itself rejects an approval attempt by the run's creator, even if the button were somehow bypassed
Technical detail

Runs shall transition only per PAYROLL_RUN_TRANSITIONS (draft -> pending_approval | voided; pending_approval -> approved | voided; approved -> paid | voided; paid/voided terminal); approve/pay require group_admin/finance, and approving your own created run throws — enforced in the store, not just the UI.

  • run.createdBy === actor.userId on approve throws forbidden_role
  • UI mirrors with a disabled Approve button + blocked-warning banner for the creator
Evidence: src/lib/people/payroll-types.ts:26-32 · src/lib/people/payroll-store.ts:512-522
REQ-08-F-03 Must Functional Payslip details in one panel

Clicking a payroll run opens a panel showing the run's details, only the actions you're allowed to take next, an optional field to note why you're taking an action, the full payslip table with a total, and the run's recent history.

  • You can check this by confirming you only ever see buttons for actions you're actually allowed to take
  • You can check this by confirming the activity history shown is specific to that payroll run
Technical detail

Selecting a run shall open a drawer with run header, four-eyes hints, role-and-four-eyes-filtered transition buttons, an optional reason field, the payslip table with total footer, and the run's last 5 audit rows.

  • Transition buttons only show legal, role-permitted moves
  • Audit log scoped to the run
Evidence: src/components/people/PayslipDrawer.tsx:84-564
REQ-08-F-04 Should Functional Everyone is notified once payroll is paid

The moment a payroll run is marked as paid, every team member at that location gets an in-app notification. If a notification fails to send for any reason, it never blocks or delays the payment itself.

  • You can check this by confirming all team members at the location receive a notification once a run is marked paid
  • You can check this by confirming a failed notification never prevents the payroll run from completing
Technical detail

Transitioning a run to paid shall notify every member of the entity in-app, fire-and-forget.

  • Notification failure never blocks the transition
Evidence: src/lib/people/payroll-store.ts:564-580
REQ-08-NF-01 Must Non-functional Payroll still runs even if commission numbers are temporarily unavailable

If the commission figures can't be retrieved for some reason when creating a payroll run, the system doesn't fail — it simply uses zero for that portion rather than blocking payroll entirely. All money on payslips is calculated in exact cents.

  • You can check this by confirming payroll can still be created even if the commission calculation temporarily fails
  • You can check this by confirming all payslip amounts are exact, with no rounding errors
Technical detail

The commission pull during run creation shall degrade gracefully to 0 if the commission store is unavailable rather than failing run creation; money is minor-units throughout.

  • try/catch wraps the cross-store call with a 0 fallback
  • All payslip fields are integer minor units
Evidence: src/lib/demo/repository.ts:238-264
0 / 2000
Thanks — your comment was received.
SCP-09 Customer Lifecycle & Engagement 6 requirements

A simple CRM that tracks each customer's relationship stage, gently suggests when it should change, and lets staff log every touchpoint — while catching duplicate customers without ever mixing up records between locations.

What's included

  • Customer list with search/status filters and inspector + detail-page parity
  • Add customer with same-entity duplicate blocking and cross-entity soft hints
  • Lifecycle state machine (new/active/vip/at_risk/dormant) with audited transitions
  • Advisory status suggestions and 4 engagement touchpoint kinds

What's not included

  • Automatic lifecycle transitions from data (suggestions are advisory only; the sole automatic transition is won-back, owned by SCP-06)
  • Customer merge/dedup execution across entities
  • Consent/marketing-preference management
REQ-09-F-01 Must Functional Search and browse every customer

You can search customers by name or email, filter by their relationship stage, and see each customer's order count, total spend (always in their own location's currency), and last order date at a glance.

  • You can check this by confirming a customer's total spend always shows in their own location's currency, never mixed with another currency
  • You can check this by looking up a customer outside your access and confirming a clear 'not found' message appears instead of an error
Technical detail

The list shall support case-insensitive name/email search, a 6-way status filter with live counts, and rows showing status badge, order count, lifetime value (always in the customer's own entity currency), last-order date, and cross-entity hint; the inspector and the standalone detail page render the same shared blocks.

  • LTV never renders in a foreign order currency
  • Out-of-scope customer id renders a not-found card
Evidence: src/app/customers/page.tsx:248-379 · src/app/customers/[id]/page.tsx:150-158
REQ-09-F-02 Must Functional Duplicate customers are caught, but never across locations

Adding a customer requires a name and valid email. If that email already exists at the same location, it's blocked as a duplicate. If it exists at a different location, you get a gentle heads-up — but it's never automatically merged and never blocks you from adding them.

  • You can check this by trying to add a customer with an email already used at the same location and confirming it's blocked
  • You can check this by adding a customer whose email exists at a different location and confirming you see a soft warning, not a block
Technical detail

Adding a customer shall require name + valid email; a duplicate email is hard-blocked within the same entity but allowed in a sibling entity, where it surfaces as a debounced (300ms) cross-entity soft hint that never blocks or auto-merges and never leaks across groups.

  • Same-entity duplicate throws a translated error
  • Cross-entity hint appears in the form, the table rows and the profile card
Evidence: src/lib/demo/repository.ts:1015-1061 · src/lib/demo/repository.ts:998-1013
REQ-09-F-03 Must Functional Customer status can only move through valid stages

Every customer is at one of five relationship stages — new, active, VIP, at risk, or dormant — and can only move between the stages that make sense (for example, a dormant customer can only come back to active). Every change is checked by the system itself, recorded, and the customer is notified unless it's meant to be silent.

  • You can check this by confirming staff only ever see valid next-stage options as buttons
  • You can check this by confirming a dormant customer's only way back is to active — there's no dead end and no invalid jump
Technical detail

Customers shall transition only per CUSTOMER_TRANSITIONS (new -> active|at_risk; active -> vip|at_risk; vip -> active|at_risk; at_risk -> active|dormant; dormant -> active), enforced server-side regardless of UI, with every transition atomic, audited and notified (unless silent).

  • UI renders only legal next-status buttons; the store re-validates anyway
  • dormant's only exit is back to active; no terminal state exists
Evidence: src/lib/domain/types.ts:99-108 · src/lib/demo/repository.ts:1085-1124
REQ-09-F-04 Should Functional Helpful (but never automatic) status suggestions

The system watches customer activity and suggests a status change when the data suggests one is due — for example, suggesting 'at risk' after 45 days of no orders. These are only suggestions; staff always have to apply them manually.

  • You can check this by confirming a suggestion appears only when the customer's actual status doesn't match what the data suggests
  • You can check this by confirming no suggestion is ever applied automatically without a staff member clicking to accept it
Technical detail

A data-derived suggestion chip shall appear when the computed status differs from the actual one — no revenue orders: >45 days old suggests at_risk; with orders: quiet >90 days suggests dormant, >45 at_risk, else vip when >=3 revenue orders — and shall never auto-apply.

  • Suggestion is a pure function of revenue orders + dates
  • No code path applies a suggestion automatically
Evidence: src/lib/domain/types.ts:131-148
REQ-09-F-05 Must Functional Log every customer touchpoint

Staff can log 4 types of customer outreach — a suggestion, a reminder, a newsletter, or a gift — with an optional note. Each one notifies the customer and is saved to their history alongside their status changes, newest first.

  • You can check this by confirming all 4 touchpoint types are available no matter what stage the customer is at
  • You can check this by confirming the customer's history shows the note, who logged it, and when
Technical detail

Staff shall send any of 4 touchpoint kinds (suggestion, reminder, newsletter, gift) with an optional note from the inspector or detail page; each is audited (customer.engage.{kind}) and notifies the customer in-app + email; an audit-backed history lists engagements and lifecycle moves newest-first.

  • All 4 kinds available regardless of lifecycle state
  • History renders reason/note, actor and timestamp
Evidence: src/lib/demo/repository.ts:1133-1185
REQ-09-NF-01 Must Non-functional Staff without permission don't see editing options at all

Staff who aren't allowed to make changes to a customer's record don't just see those options grayed out — they don't see them at all. Finance staff can only view.

  • You can check this by confirming a read-only staff member sees no edit buttons whatsoever, not even disabled ones
  • You can check this by confirming any change requires both the correct location and the correct role
Technical detail

Lifecycle and engagement sections shall be hidden entirely (not merely disabled) for actors without write permission on the entity row; finance is read-only.

  • Read-only roles see no write affordances
  • Writes require workspace match + write role
Evidence: src/app/customers/[id]/page.tsx:421-435 · src/lib/seam/rls.ts:77-79
0 / 2000
Thanks — your comment was received.
SCP-10 Inbox & Messaging 4 requirements

One inbox where staff see every customer message and system alert in one place, and can reply to or close out conversations.

What's included

  • Merged notification + thread feed with unread tracking
  • Thread creation with optional customer link and channel selection
  • Reply and resolve workflows with role separation

What's not included

  • Thread assignment UI (store method exists, unwired — FND-13)
  • Snooze workflow (status declared, unreachable — FND-13)
  • Real email delivery (demo console adapter only)
REQ-10-F-01 Must Functional One combined inbox

All customer messages and system alerts show up together in one list, newest first, so staff never have to check multiple places. Unread items are clearly marked, and staff can filter to see everything, just open items, or just resolved items.

  • You can check this by opening the inbox and seeing customer messages and system alerts mixed into a single, newest-first list
  • You can check this by confirming unread items are clearly marked, and that clicking one marks it read and opens the full conversation
  • You can check this by switching between All, Open, and Resolved filters and seeing the list update correctly
Technical detail

The inbox shall merge system notifications (for the actor, entity-scoped) and thread events into one feed sorted most-recent-first, with an unread badge count, per-row mark-read, and All/Open/Resolved filters (thread-status filters pass plain notifications through).

  • Unread = readAt === null across the merged feed
  • Row click marks read and opens the inspector
Evidence: src/lib/growth/messaging.ts:394-445 · src/app/inbox/page.tsx:73-91
REQ-10-F-02 Must Functional Start a new conversation

Staff can start a new message thread, optionally linking it to a specific customer, and choose whether to notify that customer in-app, by email, or both.

  • You can check this by starting a new conversation with a subject and message, optionally attaching it to a customer record
  • You can check this by confirming the new-conversation screen slides up from the bottom on mobile and opens as a pop-up on desktop
  • You can check this by linking a customer, checking a notification method, and confirming that customer receives the message through it
Technical detail

Staff with write roles shall create threads (subject + body required, optional customer link, in_app/email channel checkboxes); a linked customer is notified per selected channel and the new thread auto-selects.

  • Modal is a bottom sheet on mobile, dialog on desktop
  • Customer notification fires per channel chosen
Evidence: src/app/inbox/page.tsx:149-233 · src/components/growth/ThreadView.tsx:183-306
REQ-10-F-03 Must Functional Reply to and close conversations

Staff can reply to any open conversation, and managers can mark a conversation resolved once it's handled. A resolved conversation can't be replied to or resolved again.

  • You can check this by replying to an open conversation and confirming the linked customer is notified
  • You can check this by confirming only managers, not regular support staff, can mark a conversation resolved
  • You can check this by trying to resolve the same conversation twice — the second attempt is blocked
Technical detail

Replies (Cmd/Ctrl+Enter, disabled on resolved threads) shall notify the linked customer per channel; resolve (group_admin/entity_manager only) transitions open -> resolved exactly once and notifies the assignee if set.

  • ops_cs can create/reply but not resolve
  • Resolving twice throws
Evidence: src/lib/growth/messaging.ts:566-704
REQ-10-NF-01 Should Non-functional A tidy, dependable inbox

The inbox list stays a manageable size on screen and shows a friendly message when there's nothing to show. If a conversation can't be found, staff still see something useful instead of an error.

  • You can check this by scrolling a long inbox list and confirming it stays contained rather than making the whole page grow endlessly
  • You can check this by confirming an empty inbox shows a clear 'nothing here' message instead of a blank space
Technical detail

The feed shall render in a bounded-height scroll container with a descriptive empty state; broken thread lookups fall back to a plain notification detail rather than erroring.

  • Page height never grows unbounded
  • getThread failures render null thread, no crash
Evidence: src/app/inbox/page.tsx:81-88 · src/components/growth/InboxPanel.tsx:360-392
0 / 2000
Thanks — your comment was received.
SCP-11 Broadcasts (Mass Messaging) 5 requirements

Send one message to a group of customers at once — for example, everyone who has placed an order — and see how many actually received it.

What's included

  • 3-step compose wizard with segment targeting and live recipient preview
  • Send-time segment resolution with per-recipient fail-soft delivery
  • Draft/sent/cancelled lifecycle, delivery funnel, send log and audit strip

What's not included

  • Tag-based segments (reserved in the type, resolves empty — FND-16)
  • Scheduling broadcasts for future send
  • Real email delivery (demo console adapter only)
REQ-11-F-01 Must Functional 3-step message wizard

Staff write a message, choose who receives it (all customers, only customers who've ordered, or only those who haven't), and pick how to send it (in-app, email, or both), with a preview of who will actually receive it before sending.

  • You can check this by trying to move to the next step without filling in the subject and message — it's blocked
  • You can check this by selecting an audience group and at least one delivery method, then confirming the preview shows a real, live list of recipients
Technical detail

The wizard shall gate Compose (subject + body required) -> Audience (segment: all_customers/has_order/no_order; >=1 channel of in_app/email) -> Preview (summary + live recipient list) with per-step validation.

  • Next is blocked until the current step validates
  • Preview resolves the segment against live data
Evidence: src/components/growth/BroadcastForm.tsx:63-97 · src/lib/growth/broadcasts.ts:203-238
REQ-11-F-02 Must Functional Accurate delivery at send time

Who actually receives the message is worked out at the moment of sending, not when it was written, so the list stays accurate even if customers changed in between. If delivery fails for one customer, it doesn't stop the message going to everyone else.

  • You can check this by confirming the final recipient count matches who qualified right at send time, not at compose time
  • You can check this by confirming a delivery failure for one customer doesn't block delivery to the rest of the batch
Technical detail

Sending shall re-resolve the segment at send time (membership may drift from compose time), notify each recipient per channel fail-soft (one bad recipient never aborts the batch), then atomically set status=sent with sentAt, recipientCount and per-recipient delivery rows.

  • recipientCount reflects send-time membership
  • A throwing notify is logged and skipped, batch completes
Evidence: src/lib/growth/broadcasts.ts:394-493
REQ-11-F-03 Must Functional Send or cancel, no in-between

A draft message can either be sent or cancelled. Once it's been sent, it can't be cancelled or edited.

  • You can check this by confirming a sent broadcast no longer shows a cancel option
  • You can check this by confirming send and cancel buttons only appear for draft messages, and only for staff with permission
Technical detail

Broadcasts shall move draft -> sent (send) or draft -> cancelled (cancel) only; sent and cancelled are both terminal; sending or cancelling from any other status throws.

  • Sent broadcasts cannot be cancelled
  • Actions column renders only for draft rows and write roles
Evidence: src/lib/growth/broadcasts.ts:338-411
REQ-11-F-04 Should Functional Delivery tracking

Staff can see how many customers were targeted versus how many actually received the message, broken down by delivery method, plus a log of sent messages and a recent activity history.

  • You can check this by comparing the 'targeted' and 'delivered' numbers shown for a sent broadcast
  • You can check this by confirming the send log only appears once at least one broadcast has actually been sent
Technical detail

The page shall show a targeted-vs-delivered funnel (split by channel), a send log for sent broadcasts filtered to actual delivery records, and the last 15 broadcast.* audit rows.

  • Funnel sums only sent broadcasts
  • Send log hidden until at least one broadcast is sent
Evidence: src/app/broadcasts/page.tsx:141-176 · src/app/broadcasts/page.tsx:288-351
REQ-11-NF-01 Must Non-functional Who can send broadcasts

Only managers and entity managers can create, send, or cancel broadcast messages. Finance staff and support staff can view them but not send.

  • You can check this by logging in as finance or support staff and confirming you only see a 'view only' notice, with no send or cancel buttons
Technical detail

Create/send/cancel shall require group_admin or entity_manager — both finance AND ops_cs are read-only on this surface (narrower than most surfaces).

  • Read-only banner renders for finance and ops_cs
  • Store re-enforces the role set server-side
Evidence: src/lib/growth/broadcasts.ts:40-43
0 / 2000
Thanks — your comment was received.
SCP-12 Campaigns & Promotions 4 requirements

Create discount promo codes, turn them on or off, and track how often each one gets used.

What's included

  • Campaign creation with per-entity unique codes and 3 discount types
  • Activate/pause lifecycle with audited transitions
  • Discount application validation chain and redemption tracking

What's not included

  • Automatic expiry transitions (expired is a declared but never-set status — FND-17)
  • True buy-X-get-Y computation (stub returns the configured value — FND-18)
  • Order-total mutation (applications are recorded, totals unchanged)
REQ-12-F-01 Must Functional Create a promo campaign

Managers can create a discount campaign with a name, a promo code (automatically capitalized and unique within their business), a discount type (percentage off, flat amount off, or buy-one-get-one style), a value, and a start date, with an optional end date.

  • You can check this by trying to reuse an existing promo code within the same business — it's rejected
  • You can check this by trying to submit the form without a name, code, value, or start date — each one is required
Technical detail

Managers shall create campaigns with name, auto-uppercased promo code (unique per entity, case-insensitive), discount type (pct_off/flat_off/buy_x_get_y), positive integer value, required start date and optional expiry.

  • Duplicate code within the entity is rejected
  • Validation covers name/code/value/start-date
Evidence: src/app/campaigns/page.tsx:136-261 · src/lib/growth/campaigns.ts:294-301
REQ-12-F-02 Must Functional Turn campaigns on and off

A campaign can be switched from draft or paused to active, and from active back to paused. Every switch is recorded, and turning a campaign on notifies the staff member who did it.

  • You can check this by confirming the activate and pause buttons only appear for the right campaign status and staff role
  • You can check this by confirming every activate or pause action leaves a record in the activity history
Technical detail

Campaigns shall activate (draft/paused -> active) and pause (active -> paused) per CAMPAIGN_TRANSITIONS, atomically audited; activation notifies the acting user in-app.

  • Action buttons are role- and state-gated
  • Every transition writes an audit row
Evidence: src/lib/growth/campaigns.ts:50-55 · src/lib/growth/campaigns.ts:343-427
REQ-12-F-03 Must Functional Applying a promo code at checkout

Before a promo code is applied to a sale, the system checks that the code exists, is currently active, is within its valid dates, hasn't hit its usage limit, and that the order meets any minimum spend — then records the discount and counts it against the usage limit.

  • You can check this by trying an expired, inactive, or over-limit code — each is rejected with a clear reason
  • You can check this by applying a valid percentage-off code and confirming it correctly reduces the recorded discount amount
Technical detail

applyDiscount shall validate the code exists in the entity, status is active, the date window holds, the usage limit is not exceeded and the order meets minOrderMinor; on success it records a DiscountApplication, increments usageCount atomically and notifies fire-and-forget; pct_off computes floor(total x bps / 10000).

  • Each validation failure throws a distinct error
  • Application roles include ops_cs (wider than campaign management)
Evidence: src/lib/growth/campaigns.ts:437-533 · src/lib/growth/campaigns.ts:42-46
REQ-12-F-04 Should Functional See how a campaign is performing

Each campaign card shows how many times it's been used, the total discount value given out so far, and a list of the individual orders it was used on. A recent activity list shows the latest campaign and broadcast actions.

  • You can check this by expanding a campaign card and seeing the list of orders the code was used on
  • You can check this by confirming the recent activity list shows at most the 10 most recent actions, newest first
Technical detail

Each campaign card shall show usage count, live revenue impact (sum of discounts issued) and an expandable applications sub-table (order, amount, applied-at, applied-by); the page shows the last 10 campaign/broadcast audit rows.

  • Sub-table has an empty state
  • Audit strip capped at 10, newest first
Evidence: src/app/campaigns/page.tsx:265-299 · src/app/campaigns/page.tsx:501-506
0 / 2000
Thanks — your comment was received.
SCP-13 Marketplace Connections 4 requirements

Automatically bring in sales from TikTok Shop, Shopee, and Lazada so staff don't have to enter them by hand.

What's included

  • 3 channel cards with status, last sync and imported counts
  • Role-gated idempotent sync with deterministic demo feeds
  • Import-as-paid via the standard order funnel with local re-pricing

What's not included

  • Real marketplace OAuth or API calls
  • Two-way sync (inventory/price push back to marketplaces)
  • Connection configuration UI (credentials are synthesized)
REQ-13-F-01 Must Functional Marketplace status at a glance

The page shows a card for each of the three connected marketplaces, with its connection status, when it last synced ('Never synced' if it hasn't yet), and how many orders have been brought in so far.

  • You can check this by confirming all three marketplace cards always appear, even before a first sync
  • You can check this by confirming each card shows a spinner while syncing and a clear success or error message afterward
Technical detail

The page shall list all 3 marketplace channels for the workspace with status, last-sync time ('Never synced' when null) and cumulative imported count, with per-card sync spinner, success ('+N orders imported' / 'No new orders') and inline error states.

  • 3 cards always render
  • Counters are per-channel and per-workspace independent
Evidence: src/lib/connections/connections-store.ts:159-167 · src/app/connections/page.tsx:78-100
REQ-13-F-02 Must Functional Safe to sync again

Running a sync twice in a row never brings in the same order twice — the system remembers which orders it has already seen.

  • You can check this by syncing a marketplace, then syncing it again immediately — the second run brings in 0 new orders
  • You can check this by confirming the 'last synced' time updates every time, even when no new orders came in
Technical detail

Sync shall be idempotent at two layers — a deterministic connector feed (seeded PRNG per provider:account:day; first sync looks back 14 days, then cursor-forward) plus store-level dedup by seen external ids — so re-running a sync never double-imports.

  • Immediate re-sync imports 0 new orders (verified by test)
  • lastSyncAt advances as the next fetch cursor and persists even when 0 imported
Evidence: packages/connectors/src/connectors/sources/marketplace-demo.ts:135-201 · src/lib/connections/connections-store.ts:272 · src/lib/connections/__tests__/connections-store.test.ts:80-87
REQ-13-F-03 Must Functional Orders come in ready to go

Each new marketplace order is matched to an existing customer and current stock, using the business's own prices rather than the marketplace's, and is recorded immediately as a paid sale. Orders that can't be matched to a customer or any stock are skipped rather than guessed at.

  • You can check this by confirming an imported order's price matches the business's own listed price, not whatever the marketplace reported
  • You can check this by confirming imported orders show up already marked as paid
Technical detail

Each unseen record shall deterministically attach to an existing customer and in-stock inventory (qty clamped to stock), be recorded via recordSale as an unattributed (salespersonId null) sale re-priced from the workspace's OWN inventory in local currency, then immediately transition to paid through the standard funnel.

  • Connector's simulated totals/currency never enter the ledger (verified by test)
  • Imported orders carry status paid (verified by test)
  • Records with no matching customer or zero mappable lines are marked seen, not imported
Evidence: src/lib/connections/connections-store.ts:200-296 · src/lib/connections/__tests__/connections-store.test.ts:106-111
REQ-13-NF-01 Must Non-functional Who can trigger a sync, and keeping data separate

Only managers and entity managers can trigger a marketplace sync. Each business location's marketplace data is kept completely separate from every other location.

  • You can check this by logging in as a role without permission and confirming the sync button is disabled with an explanation
Technical detail

Sync shall require group_admin or entity_manager (store throws forbidden_role otherwise); connection state is isolated per workspace and per channel.

  • Unauthorized roles see a disabled button + notice AND the store rejects direct calls
  • Cross-entity/channel isolation verified by tests
Evidence: src/lib/connections/connections-store.ts:62-66 · src/lib/connections/__tests__/connections-store.test.ts:115-137
0 / 2000
Thanks — your comment was received.
SCP-14 Content Pipeline (Create, Publish, Track Performance) 5 requirements

Create social media content, route it through approval, publish it to multiple platforms at once, and see honestly how it performed.

What's included

  • Piece authoring (4 types) and the 6-status content state machine
  • Per-channel variant shaping with constraint warnings
  • Demo publish to 4 channels with audit + deterministic receipts
  • Cross-channel monitoring rollup with honest metrics

What's not included

  • Real platform publishing APIs (demo simulation only)
  • Scheduling UI (approved -> scheduled transition has no trigger — FND-09)
  • China-platform channels (Xiaohongshu/WeChat hard-blocked in the package)
REQ-14-F-01 Must Functional Create and track content pieces

Staff write content pieces (a title and caption are required, plus a type such as reel, post, carousel, or quote card) and see them listed with live counts by status and a step-by-step progress view.

  • You can check this by creating a piece and watching it appear in the list with the correct status
  • You can check this by confirming the status filter counts update immediately as pieces move through the pipeline
Technical detail

Studio shall create pieces (title + caption required; type reel/post/carousel/quote-card; hashtags; duration) and list them with status filters + live counts, an expandable 5-step pipeline stepper, and one contextual action per status.

  • Status filter counts update live
  • Contextual action follows the piece's status (Render / Submit / Approve / Open in Distribution)
Evidence: src/app/studio/page.tsx:157-271 · src/app/studio/page.tsx:450-481
REQ-14-F-02 Must Functional Content approval steps

A content piece moves through fixed stages: draft, rendered, waiting for approval, approved, then either scheduled or published. Support staff can create and prepare content, but only managers can approve and publish it. Every stage change is recorded.

  • You can check this by logging in as support staff and confirming there's no way to approve or publish content
  • You can check this by trying to skip a stage — it's blocked — and confirming every legitimate stage change appears in the activity history
Technical detail

Pieces shall transition only per CONTENT_TRANSITIONS (draft -> rendered -> pending_approval -> approved -> scheduled|published; scheduled -> published; published terminal); create/render/submit allow ops_cs, approve/publish require group_admin/entity_manager; every transition is atomic + audited.

  • ops_cs can author but never approve or publish
  • Illegal transitions throw; audit rows record from/to
Evidence: src/lib/domain/content-types.ts:35-42 · src/lib/content/content-store.ts:495-546
REQ-14-F-03 Must Functional Auto-fit content per platform

Before publishing, the system previews how content will look on each platform and automatically trims captions and hashtags and adjusts video length to fit that platform's limits, flagging anything it had to adjust.

  • You can check this by entering an overly long caption or too many hashtags and confirming a warning appears showing what was trimmed
  • You can check this by confirming a piece that fits all limits shows a 'Clean' badge with no warnings
Technical detail

Distribution shall preview per-channel variants shaped by real channel constraints — caption truncated on word boundary (100 chars YouTube / 2200 others), hashtags deduped + capped (15/30), duration clamped (60/90/90/180s) — with adjustment warnings or a Clean badge.

  • Each constraint violation surfaces a warning
  • Shaping is pure and deterministic
Evidence: packages/distribution/src/channels.ts:65-102 · packages/distribution/src/plan.ts:94-231
REQ-14-F-04 Must Functional Publish to multiple platforms at once

Publishing sends approved content out to four platforms — YouTube Shorts, Facebook Reels, Instagram Reels, and TikTok — at the same time, and keeps a record for each one. Already-published content can't be published again. Two platforms, Xiaohongshu and WeChat, are deliberately blocked.

  • You can check this by publishing a piece and confirming it appears as published on all four platforms
  • You can check this by trying to publish the same piece a second time — it's blocked
Technical detail

Publish shall fan out to all 4 permitted channels (youtube-shorts, facebook-reels, instagram-reels, tiktok) from approved/scheduled only, atomically with rollback, writing deterministic receipts (demo-{assetId}-{channel}) and seeding monitoring snapshots; the package hard-blocks Xiaohongshu/WeChat aliases.

  • A published piece cannot be re-published
  • Publish failure rolls back status and publish records
Evidence: src/lib/content/content-store.ts:426-491 · packages/distribution/src/channels.ts:105-163
REQ-14-F-05 Must Functional Honest performance tracking

The tracking page shows key numbers, a breakdown by platform, and a top-5 list, calculated fairly across platforms with different audience sizes. The system deliberately does not invent a single 'virality score,' since that would be misleading.

  • You can check this by confirming the watch-through numbers are weighted by how many people actually saw the content, not just averaged
  • You can check this by confirming that with no data yet, the page shows a clear empty state instead of an error or blank screen
Technical detail

Monitoring shall aggregate snapshots into 5 KPI cards, a by-channel table and a top-5 list; watch-through is impression-weighted (never raw-ratio-summed); no composite virality score is ever computed (explicit invariant with an honesty footnote).

  • watchThrough = sum(impressions*wt)/sum(impressions)
  • Divide-by-zero guarded; empty state when no snapshots
Evidence: packages/monitoring/src/aggregate.ts:72-146 · src/app/monitoring/page.tsx:63-133
0 / 2000
Thanks — your comment was received.
SCP-15 AI Assistant 6 requirements

A chat assistant that can look up live business information and take action when asked, but only after a staff member approves anything that changes data — and it keeps working even without a paid AI subscription.

What's included

  • 3-tier provider chain with runtime fallback
  • 16 tools: server-defined schemas, 100% client-side execution under the current actor's RLS
  • In-chat approval cards for all write tools
  • Sessions, markdown, attachments and spend guards

What's not included

  • Server-side tool execution or server-trusted role claims
  • Long-term memory across sessions beyond localStorage
  • Autonomous writes without an explicit human approval click
REQ-15-F-01 Must Functional The assistant always responds

The assistant tries its best available option first, falls back automatically if that's unavailable, and finally falls back to a built-in assistant that needs no paid subscription at all — so it never simply fails to respond.

  • You can check this by using the assistant with no paid subscription set up and confirming it still answers helpfully
  • You can check this by confirming that if the paid option fails partway through answering, the assistant recovers and finishes the answer instead of showing an error
Technical detail

The assistant shall resolve its brain as OpenRouter (if keyed) -> Anthropic (if keyed) -> keyless deterministic planner; pre-stream errors return a planner response and mid-stream failures re-plan the same turn into the open stream — the demo never hard-fails.

  • Zero-env boot still answers agentically via the planner
  • Mid-stream provider failure produces a planner answer, not an error bubble
Evidence: src/app/api/assistant/route.ts:14-28 · src/app/api/assistant/route.ts:180-204
REQ-15-F-02 Must Functional Nothing changes without your say-so

The assistant can look things up on its own, but any action that would change data — like issuing a refund — shows a card asking staff to explicitly click 'Run it' or 'Deny' before anything happens. Requesting a refund through the assistant creates an approval request rather than refunding directly, and it won't create a duplicate request if one is already pending.

  • You can check this by asking the assistant to change something and confirming nothing happens until you click 'Run it'
  • You can check this by asking for a refund twice in a row and confirming only one pending approval request is created
Technical detail

All 16 tools shall be schema-only on the server and execute exclusively client-side against the current actor's repo (RLS applies); read tools auto-execute, write tools render an in-chat approval card requiring an explicit Run it / Deny; requestRefund files an approval request (never refunds directly) with a duplicate-pending guard.

  • Server never executes a tool
  • No write tool runs without a click
  • syncMarketplace additionally re-checks the sync role
Evidence: src/lib/assistant/client-tools.ts:1-23 · src/components/assistant/ChatPane.tsx:299-367 · src/lib/assistant/client-tools.ts:229-267
REQ-15-F-03 Must Functional Saved conversations and file uploads

The assistant keeps a history of past conversations, organized by date, which staff can rename, delete, or export. Staff can attach up to 3 files per message (including spreadsheets, PDFs, and Word documents), up to a set size and length limit.

  • You can check this by renaming and deleting a saved conversation
  • You can check this by trying to attach more than 3 files, or a file over the size limit — it's blocked with a clear message
Technical detail

The assistant shall keep localStorage-backed sessions (auto-title, date buckets, rename/delete/export .md) and accept up to 3 attachments per request, extracted server-side (9 formats incl. xlsx/pdf/docx) within an 8MB/24k-char extraction cap and a 40k-char prompt budget.

  • Attachment caps enforced client AND server side
  • Extraction endpoint is separately rate-limited
Evidence: src/lib/assistant/sessions.ts:11-115 · src/app/api/assistant/extract/route.ts:21-54
REQ-15-F-04 Should Functional Works in English and Chinese, even without a paid subscription

The built-in fallback assistant understands common questions in both English and simplified Chinese, and gives a helpful, specific response even to questions it doesn't exactly recognize, rather than a generic 'I don't understand.'

  • You can check this by asking a specific question, like 'show me order 123,' and confirming it doesn't get confused with a broader question like 'show me all orders'
  • You can check this by asking an unrecognized but related question and confirming you get a targeted hint, not a blank shrug
Technical detail

The keyless planner shall match 17 bilingual (EN/简体中文) intents ordered specific-before-general, with a topical near-miss fallback router, using the same tool-call protocol as the live path.

  • getOrder matches before listOrders
  • Unmatched topical queries get a targeted hint, not a generic shrug
Evidence: src/lib/assistant/intents.ts:748-814
REQ-15-NF-01 Must Non-functional Cost controls on the paid assistant

When a paid subscription is connected, the assistant limits how much any one person can use it in a short time, how long its answers can be, how much conversation history it reads, and how many actions it can take in one turn, so costs stay predictable. If someone hits the usage limit, they still get an answer from the built-in fallback instead of being cut off.

  • You can check this by exceeding the usage limit and confirming you still get a useful answer, not an error message
Technical detail

When a paid key is configured, the route shall enforce: per-IP rate limiting that DEGRADES to the free planner when exceeded, max 1024 output tokens, a 24-message history window, and max 5 tool steps per turn; the keyless planner is never throttled.

  • Over-limit requests still get an answer (planner), never a 429 dead-end
  • All four caps verified at the stated values
Evidence: src/app/api/assistant/route.ts:113-122 · src/app/api/assistant/route.ts:254-275
REQ-15-NF-02 Must Non-functional Staff can't trick the assistant into unauthorized actions

The assistant only ever acts using the real permissions of the staff member who is logged in — it can't be talked into pretending someone has a role or access they don't actually have.

  • You can check this by confirming permissions are always checked against the real logged-in user, never anything typed into the chat
Technical detail

Role/entity fields in the request body shall be prompt-context only — never used to authorize tool execution; authorization happens client-side against the real actor, so the model cannot be tricked into privilege escalation via body params.

  • Documented in the route; no server code path scopes data by body role
  • Unknown role strings default to a safe known value
Evidence: src/app/api/assistant/route.ts:39-43 · src/app/api/assistant/route.ts:93
0 / 2000
Thanks — your comment was received.
SCP-16 Approvals, Team & Activity History 6 requirements

A second-person check on sensitive actions like refunds and payroll, a staff directory with roles, and a complete, unchangeable record of everything that happened.

What's included

  • Approval queue: submit / decide / withdraw with four-eyes enforcement
  • Post-approval re-apply under maker authority for refunds and bulk price changes
  • Team roster, invite generation, role-capability table
  • Acting-as role switcher and gated demo-data reset

What's not included

  • Real authentication/invite emails (demo tokens only)
  • Configurable approval policies or additional action types
  • Audit log export
REQ-16-F-01 Must Functional Approval requests

Managers can submit requests for sensitive actions, covering 8 different types including payroll, commission, staff goals, refunds, and bulk price changes. A separate manager or finance staff member can then approve or reject a pending request, and the original requester or a manager can withdraw it before it's decided. Every step is recorded and the right people are notified.

  • You can check this by submitting a request and confirming approvers are notified, then confirming the requester is notified once a decision is made
  • You can check this by trying to decide on a request that's already been decided — it's blocked
Technical detail

The queue shall support submit (group_admin/entity_manager), decide approve/reject (group_admin/finance, pending requests only) and withdraw (original requester or group_admin, pending only), each atomic + audited, notifying approvers on submit and the requester on decision.

  • 8 gated action types cover payroll, commission, goals, refunds and bulk price
  • Deciding a non-pending request throws
Evidence: src/lib/people/approvals-store.ts:221-294 · src/lib/people/approvals-store.ts:306-341 · src/lib/domain/people-approvals-types.ts:21-29
REQ-16-F-02 Must Functional A second person must approve before it takes effect

The person who requested an action can never be the one to approve their own request, even if they try to bypass the normal screen — the system blocks it.

  • You can check this by submitting a request and then trying to approve it as the same user — it's blocked with a clear reason
Technical detail

No approver shall decide their own request: requestedBy === approver throws forbidden_role at the store layer, independent of any UI hint.

  • Same-user approve attempt throws even via direct store call
  • UI shows a hint banner and disables self-approval affordances
Evidence: src/lib/people/approvals-store.ts:336-341
REQ-16-F-03 Must Functional Approved actions go through smoothly

Once approved, refunds and bulk price changes are carried out under the original requester's permissions, so a finance approver without direct edit rights can still approve without the action failing. If carrying out the action fails afterward — for example, a product no longer exists — the approval itself still stands, and the issue is flagged separately rather than undoing the decision.

  • You can check this by having a read-only finance approver approve a refund and confirming it goes through
  • You can check this by having an item deleted before the approved action runs and confirming it's reported as skipped rather than causing an error
Technical detail

On approval, order.refund and inventory.bulk_price intents shall re-apply through the ORIGINAL REQUESTER's authority (so a read-only finance checker can approve without RLS blocking the downstream write) — fail-soft: an apply failure never rolls back the recorded decision; variants deleted in the interim are reported as skipped.

  • Write attribution stays with the maker
  • Apply failure leaves the decision intact and is surfaced as follow-up
Evidence: src/lib/people/approval-appliers.ts:63-115
REQ-16-F-04 Must Functional Staff directory and invitations

The team page lists staff members — group-level roles see everyone across all locations, others see just their own location — lets managers generate an invitation for a new staff member, and shows a reference table of what each role can do. A location manager cannot grant someone full group-admin access.

  • You can check this by generating an invite and confirming it's recorded in the activity history
  • You can check this by trying to invite someone as a group admin while logged in as a location manager — that option isn't available
Technical detail

Team shall list members (group roles see all entities; others their own), generate audited demo invites (group_admin/entity_manager; entity_manager cannot grant group_admin) and render a static role-capability table.

  • Invite option list is role-filtered
  • Invite generation writes a team.invite.generated audit row
Evidence: src/lib/people/team-store.ts:172-262 · src/app/team/page.tsx:674-680
REQ-16-F-05 Must Functional Try any role, and reset the demo

Any user can switch to see the system as any of the 4 demo roles would see it, which makes it easy to demonstrate the request-and-approve process end to end. A full data reset, only available to a top-level admin, requires confirming twice and then reloads with fresh sample data.

  • You can check this by switching between the 4 roles from the Team page and seeing the view change accordingly
  • You can check this by trying the reset button and confirming it asks for confirmation twice before actually resetting
Technical detail

An acting-as switcher shall let any user assume any of the 4 demo roles (making the request-as-one / approve-as-another loop demoable); switching to an entity-locked role snaps the entity home; a group_admin-only two-step-confirmed reset clears the localStorage snapshot and reloads.

  • 4 roles switchable from Team
  • Reset requires two confirmations and reseeds cleanly
Evidence: src/app/team/page.tsx:958-1105 · src/lib/demo/persistence.ts:83-89
REQ-16-NF-01 Must Non-functional A tamper-proof history of everything

Every meaningful action in the system is written to a permanent history log that nothing can edit or delete, visible on the Approvals page, the Team page, and each order's own history. If a history entry can't be written, the action itself is cancelled too, so the two always stay in sync. Notifications, on the other hand, are allowed to fail quietly without blocking anything.

  • You can check this by confirming there is no button or option anywhere to edit or delete a history entry
  • You can check this by confirming every approval, refund, and role change appears in the relevant history log
Technical detail

The audit store shall be append-only (no update/delete API), fail-closed (missing workspace throws), one row per mutation, surfaced on Approvals, Team and per-order trails; mutation + audit are one all-or-nothing unit via the transitionStatus seam, while notifications remain deliberately fail-soft.

  • Audit-write failure rolls the mutation back
  • No API exists to alter or remove an audit row
Evidence: src/lib/seam/audit.ts:41-60 · src/lib/seam/transition.ts:36-55
0 / 2000
Thanks — your comment was received.
SCP-17 Platform Foundation (behind-the-scenes basics) 6 requirements

The underlying setup everything else relies on: keeping each business location's data separate, supporting English and Chinese everywhere, saving work reliably, and an optional password lock for the whole site.

What's included

  • 1 group / 3 entities tenancy with group-read and pinned writes
  • Two-layer RLS (TypeScript runtime mirror + SQL policy recipe)
  • EN/中文 typed-key i18n with compile-time and runtime parity
  • Debounced schema-versioned localStorage persistence
  • Optional site-wide Basic Auth gate covering pages and APIs

What's not included

  • Real authentication/user accounts (demo actors only)
  • Server-side database (in-memory repository + localStorage by design)
  • More than one group (multi-group resolution is stubbed)
REQ-17-NF-01 Must Non-functional Multiple business locations, kept separate

The platform supports one overall business group with 3 locations: Malaysia (in Ringgit), China (in Yuan), and an international location (in US Dollars). The product catalogue is shared across all locations, but stock, prices, and daily operations are kept separate per location. Senior roles like group admin and finance can view across all locations or switch between them, but any changes they make still apply only to their own home location. Location managers and support staff are locked to their own location only.

  • You can check this by confirming a location manager cannot switch to a different location
  • You can check this by confirming that viewing 'all locations' as a senior role only widens what you can see, never what you can change
Technical detail

The platform shall model 1 group with 3 entities (MY=MYR, CN=CNY, INTL=USD); products/variants are group-scoped (shared catalogue), operational rows entity-scoped; group_admin/finance may switch entities or enter All-entities read mode while writes stay pinned to the home entity; entity_manager/ops_cs are locked to their home entity.

  • setEntityId silently no-ops for locked roles
  • All-entities widens reads only, never writes
Evidence: src/lib/demo/repository.ts:106-109 · src/components/RoleProvider.tsx:26-96
REQ-17-NF-02 Must Non-functional Who can see and change what

Access rules are enforced twice over — once directly in the software's own logic, and matched by an equivalent set of database-level rules as a second layer of safety. Finance staff can view everything in their scope but never make changes.

  • You can check this by confirming every action that changes data double-checks the person's permissions before making the change
  • You can check this by confirming finance-role users never see an editable field anywhere in the product
Technical detail

Access rules shall be enforced by a TypeScript runtime mirror (read: own workspace OR group-read role in same group; write: own workspace AND role in {group_admin, entity_manager, ops_cs}) that matches an emitted per-table SQL policy recipe; finance is read-only app-wide.

  • Every store write path asserts the rule server-side
  • rlsPolicySQL emits the equivalent SQL for graduation
Evidence: src/lib/seam/rls.ts:30-109 · src/lib/seam/rls.ts:119-138
REQ-17-NF-03 Must Non-functional Fully bilingual, no missing translations

Every piece of text shown to users exists in both English and Chinese, and the system is built so a missing Chinese translation is caught automatically before it ever reaches a customer.

  • You can check this by switching the language and confirming every screen is fully translated, with nothing left in the wrong language
Technical detail

All user-facing copy shall route through typed keys (EN canonical, 中文 typed against the same shape so missing/extra keys fail compilation), backed by a runtime parity guard in the test suite; dynamic keys must use typed maps, not casts of arbitrary strings.

  • A missing zh key is a tsc error
  • assertLocaleParity covers missing/extra/blank keys
Evidence: src/lib/i18n/index.ts:13-49 · src/lib/i18n/guard.ts:33-69
REQ-17-NF-04 Must Non-functional Your work is saved reliably

Everything staff do is automatically saved to the device's browser storage a fraction of a second after each change, so nothing is lost on reload. If saved data ever becomes unreadable, or is left over from an old, incompatible version, the system safely starts fresh with realistic sample data instead of crashing.

  • You can check this by making a change, reloading the page, and confirming the change is still there
  • You can check this by confirming that corrupted saved data never crashes the app — it just resets to fresh sample data
Technical detail

The whole demo repository (12 sub-stores + connections) shall snapshot to localStorage debounced at 500ms, schema-versioned (v2); any mismatch or corruption silently falls back to a fresh seed; SSR and vitest never touch real persistence (two-layer guard).

  • Corrupt snapshot never crashes the app
  • ~470 tests run without localStorage writes
Evidence: src/lib/demo/persistence.ts:23-89 · src/lib/demo/repository.ts:1767-1890
REQ-17-NF-05 Must Non-functional Works instantly, with an optional password lock

The demo works right out of the box with no setup needed. If the business wants to restrict who can view the whole site, turning on one password protects every page and every behind-the-scenes connection, so nobody can access anything — including any connected paid AI features — without that password.

  • You can check this by confirming the site is open to everyone when no password is set
  • You can check this by turning the password on and confirming every page, not just some, now asks for it
Technical detail

The app shall boot with zero environment variables (all secret-bearing paths fall back to the seeded demo); when SITE_PASSWORD is set, an HTTP Basic Auth gate covers ALL pages AND /api/* (so a configured LLM key cannot be curl'd directly), using a constant-time-style comparison.

  • Unset password = fully public demo; set = everything gated
  • Matcher excludes only static assets
Evidence: src/middleware.ts:21-44 · src/lib/site-gate.ts:24-58
REQ-17-NF-06 Should Non-functional Consistent text styling

All text sizes and weights across the product come from one central style guide, so the look stays consistent everywhere rather than varying from screen to screen.

  • You can check this by confirming headings, labels, and body text look consistent across different pages
Technical detail

Type sizes/weights shall come from one config emitted as --f-text-* CSS variables and .mg-text-* classes; components never use raw font-size literals.

  • One scale of 8 named styles
  • Runtime re-application supported via ThemeProvider
Evidence: src/config/typography.ts:25-67
0 / 2000
Thanks — your comment was received.

Known limitations — 24 entries

Places where the product today deviates from the ideal, or is a deliberate simplification. None of these block sign-off; they are recorded so nothing is a surprise later.

IDSeverityWhat it meansWhich requirements it touches
FND-01 Medium Stock-adjust reason silently discarded. StockAdjustPanel captures an optional 'reason' in local state but never passes it to repo.setInventory(), so the audit row loses the operator's context — unlike the Import page's stock adjust, which threads reason through to the audit payload. Inventory Management
REQ-04-F-04
FND-02 Medium Integer price import misparse risk. parsePriceToMinor treats a value WITHOUT a decimal point as already-minor-units: a CSV price of '50' meaning RM50 imports as 50 minor units (RM0.50). Values with decimals are handled correctly. Data Import & Ingestion
REQ-05-F-02
FND-05 Medium No four-eyes gate on import commits. Bulk-CSV commit, Shopify commit and manual entry write directly once the actor has a write role — unlike inventory bulk price edits, there is no maker-checker queue for imports, though imports can mass-mutate stock and prices. Data Import & Ingestion
REQ-05-NF-01
FND-06 Medium Commission ignores salesperson attribution and revenue status. computePeriod creates entries for EVERY entity member x EVERY order in the period, regardless of the order's salespersonId or whether it is a revenue order (draft/cancelled/refunded included) — commission is member x order, not seller x revenue-order. Sales Commission
REQ-07-F-02
FND-07 Medium No maker-checker on commission entries. Any approval-role holder — including the same actor who ran computePeriod — can approve and pay entries; unlike refunds and payroll there is no requestedBy != approver check. Sales Commission
REQ-07-F-03
FND-09 Medium Dead content transition: approved -> scheduled. CONTENT_TRANSITIONS legalises approved -> scheduled but no UI action or store method (schedule()) ever performs it — 'scheduled' is a declared, unreachable status. Content Pipeline (Studio → Distribution → Monitoring)
REQ-14-F-02
FND-15 Medium Broadcast failures invisible to users. create/send/cancel errors are console.error'd only — there is no user-facing error toast/banner; a failed send shows nothing except the absence of a success toast. Broadcasts
REQ-11-F-02REQ-11-F-03
FND-17 Medium Expired campaign status is dead; no visual expiry. No method ever sets a campaign to 'expired'; date validity is only checked at applyDiscount time, so a date-expired campaign still displays as active on /campaigns with no expiry cue. Campaigns & Promotions
REQ-12-F-02
FND-03 Low Batch history can show stale rows. IngestHistoryTable refreshes only on mount / entity change, so it may not reflect a commit made in the same session until remount. Data Import & Ingestion
REQ-05-F-06
FND-04 Low Shopify connection state not persisted. The connector panel builds its connection object ad hoc per sync and never persists a ChannelConnection, so no durable 'connected' state survives reloads. Data Import & Ingestion
REQ-05-F-04
FND-08 Low Untranslated duplicate-run error. The duplicate payroll-run guard surfaces a raw English error string inline with no i18n key — the one break in the app's strict bilingual discipline on this surface. Payroll
REQ-08-F-01
FND-10 Low Demo publish diverges from package semantics. The local demo simulator always returns status 'published', while the package's real publishAll emits 'queued' for future-scheduled variants — two 'demo mode' implementations with drifting semantics. Content Pipeline (Studio → Distribution → Monitoring)
REQ-14-F-04
FND-11 Low Monitoring insight capabilities unwired. buildInsightCards/explainPerformance (deterministic insight cards, low-volume data-gap guard at <100 impressions, grounded LLM narration) exist in @foundry/monitoring but are not wired into the Monitoring page. Content Pipeline (Studio → Distribution → Monitoring)
REQ-14-F-05
FND-12 Low Silent per-order sync skips. Individual order-import failures during marketplace sync are caught, marked seen and skipped without any UI surfacing — a partial failure looks identical to a clean sync minus the count. Marketplace Connections
REQ-13-F-03
FND-13 Low Unreachable snoozed status; unwired assignment. ThreadStatus declares 'snoozed' (with badge styling) but no code path sets it; assignThread exists in the store with notifications but has no UI entry point. Inbox & Messaging
REQ-10-F-03
FND-14 Low Relative-time strings not localised. relativeTime() returns hardcoded English ('just now', 'Xm ago', ...) — the only non-i18n user-facing copy found on the inbox surface. Inbox & Messaging
REQ-10-F-01
FND-16 Low Tag segment reserved but inert. SegmentRule includes 'tag' which always resolves to an empty recipient list — selectable in the type but not in the UI; a latent no-op if ever exposed. Broadcasts
REQ-11-F-01
FND-18 Low buy_x_get_y discount is a stub. The buy_x_get_y branch of computeDiscount returns the configured value verbatim rather than computing an actual buy-X-get-Y discount. Campaigns & Promotions
REQ-12-F-03
FND-19 Low Inconsistent money formatting paths. Campaigns divide minor units by 100 directly and GoalCard uses a hardcoded en-US USD Intl formatter — both bypass the shared locale/currency-aware formatMoney used elsewhere. Campaigns & Promotions
REQ-12-F-04REQ-03-F-01
FND-20 Low Goal/task update errors console-only. Status-update failures on goals/tasks are console.error'd with a 'future: toast' comment — the user gets no feedback on a failed transition. Goals & Tasks
REQ-03-F-02REQ-03-F-04
FND-21 Low Invite audit bypasses the atomic seam. generateInvite writes its audit row via a direct audit.write() call instead of the transitionStatus mutation+audit transaction used everywhere else — the one documented exception. Governance: Approvals, Team & Audit
REQ-16-F-04
FND-22 Info LOW_STOCK_THRESHOLD duplicated. The threshold value 5 is defined independently in the inventory page and StockMatrix (and again on the home page) rather than shared from one constant — identical today, a drift risk. Inventory Management
REQ-04-F-02
FND-23 Info applyDiscount has no UI entry point. The fully validated POS discount engine is not invoked from /campaigns or any audited surface — it appears built for the order flow but currently has no caller in the UI. Campaigns & Promotions
REQ-12-F-03
FND-24 Info Fuzzy ingest cross-reference fallbacks. Ingest upsert lookups use best-effort string matching (label.includes(sku), var_ingest_ id-prefix scans) — fine for the seeded demo, not a production-faithful dedup guarantee. Data Import & Ingestion
REQ-05-F-03

Review & Sign

Have a general comment on this document, not tied to one section? Leave it here before signing.

0 / 2000
Thanks — your comment was received.

Sign-off

By signing below, you confirm that: (1) the 17 scope areas and 97 requirements in this document accurately describe the product as it exists today, and are accepted as the requirements baseline; (2) the known limitations above are acknowledged; and (3) any future change to a scope boundary will go through change control against this baseline.

 
Draw with mouse, trackpad or finger.

Signed — thank you.

Reference:
Submitted:

Keep this reference for your records.