# Business rules — stock_system

Evidence date: 2026-10-02. Source: `main` at `02b62e87`.

Rules are copied from behaviour in code or from an explicit sentence in `docs/`. **Inferred** means the code does this and no separate business statement was found. Do not treat an inference as an approved policy.

Secret values are omitted.

## Catalogue and enquiries

| ID | Rule | Label |
|---|---|---|
| BR-001 | The shop stores a costume catalogue and enquiries. It does not store stock on hand, reservations, or supplier links | **Documented** in `docs/db_docs/shop_products.md` |
| BR-002 | Only products with `is_visible = 1` appear in catalogue search, facet lists, and cart add | **Implemented** in `CatalogueQuery` and `CartService`. **Inferred** as the publish rule |
| BR-003 | Hire price is kept on the product and is not shown on the public item page or cart. The page tells the visitor that availability is confirmed by email | **Documented** in the view copy and **Implemented** |
| BR-004 | `size` and `web_notes` are stored and are not shown on the storefront yet | **Documented** in `docs/db_docs/shop_products.md` |
| BR-005 | Catalogue text search looks at code, description, and barcode | **Implemented** |
| BR-006 | Page size is `shop.itemsPerPage`, default 40 | **Implemented**. Task acceptance text says 40 per page (**Documented**) |
| BR-007 | Cart quantity is a requested count. Adding the same visible product increases quantity. It does not check office availability | **Inferred** from `CartService` (no stock read) |
| BR-008 | Enquiry requires a non-empty cart, a name, a valid email, a from-date on or after today, and at least one day | **Implemented** in `EnquiryService::validate` |
| BR-009 | An enquiry does not store the product price on the line | **Documented** in `docs/db_docs/shop_products.md` and **Implemented** (insert columns omit price) |
| BR-010 | Staff enquiry mail uses `shop.enquiryEmail`, then `email.fromEmail`. The customer receives the same subject and HTML | **Implemented** |
| BR-011 | If either email fails, the enquiry remains saved and `email_failed` is 1. The cart is still cleared | **Implemented** |
| BR-012 | Sync may update office-owned columns and must not overwrite `is_visible`, `is_featured`, `featured_sort`, `web_copy`, `web_notes`, or `shop_product_images` | **Documented** in `docs/shop_sync_contract.md`. The PHP app does not perform the upsert, so enforcement is **not in this repo** |
| BR-013 | New office products should stay invisible until staff publish them | **Documented** in the sync contract. **Conflicts** with migration default `is_visible = 1` in `2026-06-08-100000_CreateShopTables.php`. Requires confirmation |
| BR-014 | Sync log `status` is only `success`, `partial`, or `failed` | **Documented** and **Implemented** in `ShopSyncService::recordRun` |
| BR-015 | If `shop.syncApiKey` is empty, sync-log calls are accepted when the app environment is development, and rejected otherwise | **Implemented**. **Inferred** as a local-dev convenience, not a written policy |
| BR-016 | `era` on the shop product is mapped from the office `Location` field | **Documented** in the sync contract |

## Access

| ID | Rule | Label |
|---|---|---|
| BR-017 | Public storefront, login, password reset, email tracking pixels, cron ping, sync log, support webhook, and `GET slack` do not require a session | **Implemented** in `Filters.php` exceptions |
| BR-018 | Other pages require `isLoggedIn`. If the first URL segment is not a menu row for the session role, the user is sent to `/`. If the menu exists and the role has no grant, the user is sent to `blocked` | **Implemented** in `Authorization` |
| BR-019 | An inactive user (`active == 0`) cannot sign in | **Implemented** |
| BR-020 | A matching password starts a session immediately when system setting `2FA` is off | **Implemented** |
| BR-021 | When `2FA` is on, an email code is required unless `check2faEmailConfirmationDate` still accepts the previous confirmation | **Implemented**. The window length is in `UserModel` and was not re-stated here |
| BR-022 | Viewer users land on `documents` after login. Other roles land on `home` | **Implemented** |
| BR-023 | Viewer document search and lists return only assigned documents | **Documented** and **Implemented** (model RLS described in `docs/new/documents.md`) |
| BR-024 | Viewer cannot upload. The upload action returns 403 | **Documented** |
| BR-025 | Some document buttons treat session role ids `3` and `5` as read-only, while Viewer upload checks the Viewer role id from the database | **Documented** in `docs/new/documents.md` as a mismatch to keep aligned with seeds. **Requires confirmation** which id is Viewer in the `annies` database |
| BR-026 | If a user has time restriction enabled and the current time is outside the window, documents and mailbox redirect to `blocked` | **Implemented** in `TimeAccessFilter` |
| BR-027 | A blocked client IP is handled in the `Welcome` constructor before login | **Implemented**. Exact block duration is in the blocked-users model |
| BR-028 | Shop Sync and Scheduler menus are seeded for Developer, System Admin, and Admin | **Implemented** in seeders |
| BR-029 | Support Desk menu is seeded for Developer, System Admin, Admin, and System User. Channel add/delete is documented for Developer, System Admin, and Admin | **Documented** in `docs/support_env.md` and seeder |
| BR-030 | Mailbox menu is seeded for Developer, System Admin, Admin, System User, and Company Admin | **Implemented** in `MailboxMenuSeeder` |
| BR-031 | Documents menu is seeded for System Admin, Admin, System User, Company Admin, Company User, and Viewer | **Implemented** in `DocumentsMenuSeeder` |
| BR-032 | `UserRole` inserts System Admin, Admin, Company Admin, Company User, System User, and Viewer when missing. The branch that looks for `Developer` inserts another `System Admin` row instead | **Implemented** seeder behaviour. **Requires confirmation** before treating Developer as a real role |
| BR-033 | Admin document category screens allow only session role `1` (described as System Admin) | **Documented** in `docs/new/documents.md` |

## Mail, support, and jobs

| ID | Rule | Label |
|---|---|---|
| BR-034 | Scheduler task types are internal handlers, HTTP webhooks, and whitelisted Spark commands | **Documented** in `docs/scheduler.md` |
| BR-035 | Whitelisted Spark commands are `mailbox:poll`, `send:bulk-email-campaigns`, and `monitor:system` | **Implemented** in `app/Config/SchedulerHandlers.php` |
| BR-036 | Internal handlers are send scheduled emails, send immediate emails, and check server logs | **Implemented** in the same config |
| BR-037 | `/scheduler/run` and `/cronJobs` are outside the login filter. Docs say a key is required in production | **Documented**. Confirm the production check in `Scheduler::run` before relying on it |
| BR-038 | Support inbound WhatsApp is accepted on the webhook route, which skips CSRF and login and uses `supportWebhookAuth` | **Implemented** |
| BR-039 | Bulk-email tracking pixels and click/unsubscribe links are public and throttled by `emailTrackingThrottle` | **Implemented** |
| BR-040 | Enquiry, 2FA, and queue mail use the configured SMTP helper. The from-name in the tracked env file is an Overdrive AI label, not "Annie's" | **Implemented** configuration. **Requires confirmation** of the production from-name |

## Conflicts

- BR-013 versus the database default for `is_visible`.
- BR-025 and BR-032: role identity is sometimes a name, sometimes a hard-coded id, and the Developer name is granted by menus but not inserted by the role seeder.
- Repository and deploy naming (stock system, SAWIS Label, Admin Overdrive v2) versus Annie's Angels catalogue rules. The catalogue rules are the ones the shop code enforces. The other names are labels in config and scripts. Which name is the business name of this deployment **requires confirmation**.
