# Business rules — intersect_cloud_prod (Simpleflow Hub)

Evidence date: 2026-10-12. Revision: `main` @ `f4affe1`.

Rules are taken from the client. The database may enforce more, or less, through RLS and triggers. Those definitions are **not in this repository**. Where a rule is only visible in Dart, it is marked **Inferred** or **Implemented**. Nothing here is a signed-off business policy unless the README said it, and the README does not state business rules.

## Identity and access

### BR-001 — Two role axes

**Implemented.** A person has a system role on `user.user_role` and a tenant role on the tenant link (`tenant_user_list.role`).

The UI offers "System Admin", "System User", "Tenant Admin", and "Tenant User". The values written to the database are `Admin` and `User` on both axes.

Evidence: add-user widgets under `lib/pages/user_management/users/` and the matching screens under `lib/pages/management/users/`.

### BR-002 — Login binds the first tenant link

**Implemented.** After a successful password sign-in, the app loads `tenant_user_list` for `currentUserUid` and uses `firstOrNull` as the current tenant, tenant name, and tenant role. It does not ask the user to pick a tenant at login.

**Inferred.** If the user has several tenant links, which row is "first" depends on the API's default order. No `order()` is applied on that query.

**Implemented failure.** If there is no row, `firstOrNull!` throws. There is no product message for "user has no tenant".

Evidence: `lib/pages/auth/login/login_widget.dart`.

### BR-003 — System admin may switch tenant

**Implemented.** The tenant switcher is shown only when the side nav is expanded and `systemRole == 'Admin'`. Choosing a row replaces the current tenant id, display name, and tenant role.

Evidence: `lib/pages/include/side_nav/side_nav_widget.dart`.

### BR-004 — Management is admin-only

**Implemented.**

- The Management nav item is visible when `systemRole == 'Admin'` or `tenantRole == 'Admin'`.
- System admin is sent to route `Management`.
- Tenant admin is sent to route `ManagementUser`.
- Individual edit pages repeat the check. Examples: system-admin edit user and edit tenant require `systemRole == 'Admin'`; tenant-admin user and tenant-user pages require `tenantRole == 'Admin'`.

Evidence: side nav; `initState` guards in `lib/pages/management/**`.

### BR-005 — A user is not linked twice to the same tenant

**Implemented.** Before inserting `tenant_user_list`, the add-user flow queries that table by `tenant_id`. If any row comes back, it shows "Already linked!" and does not insert.

The check that was read filters on tenant id only, not on user id. **Requires Confirmation** whether that means "this tenant already has any user" rather than "this user is already on this tenant". If the query is unscoped, the dialog would fire for every tenant that already has members.

Evidence: `lib/pages/user_management/users/add_user/add_user_widget.dart` around the `tenantCheck` query.

### BR-006 — Router login wall covers one route

**Implemented.** `requireAuth` is true only for `SupportTicketList`. Every other named route uses the default, false.

**Inferred.** Hiding a menu item is not the same as blocking the URL. A logged-out client can still be routed to `dashboard`, `tasks`, or `management` by path. Whether data comes back is a database concern.

Evidence: `lib/flutter_flow/nav/nav.dart`.

### BR-007 — Role, permission, and menu tables are not the shell's gate

**Implemented** (client) / **Unknown** (database). Tables `role`, `permission`, `menu`, `role_menu_link`, and `user_role_link` exist as generated classes. The side nav does not query them. It compares the two strings in app state.

Do not describe a permission matrix. One was not found in the UI.

### BR-008 — Sign-out clears local role state

**Implemented.** Sign-out sets `selectedTenantName`, `systemRole`, and `tenantRole` to empty strings. Secure-storage keys for those fields are updated by the setters.

Evidence: side nav sign-out action; `lib/app_state.dart`.

## Tenant

### BR-010 — Branding comes from the tenant row

**Implemented.** Missing colours fall back to `#ff9900` (primary, header, menu), `#495564` (accent, tile), and `#ffffff` (background).

### BR-011 — Active flags exist

**Implemented** as columns `user.active` and `tenant.active`. The login query that was read does not filter `active`. **Unknown / Requires Confirmation** whether an inactive user or tenant can still sign in.

## Tasks

### BR-020 — New tasks start Open

**Implemented.** Create task writes `status: 'Open'`.

### BR-021 — Completion is the status Done

**Implemented.** Complete actions write `status: 'Done'`. The details UI branches on `status == 'Done'`.

No `In Progress`, `Cancelled`, or `Closed` status was found in the task widgets that were searched.

### BR-022 — Task tags are a fixed set

**Implemented.** Choice chips: `Urgent`, `Project`, `Activity`, `Assistance`. Stored on `task.tags` (a list).

### BR-023 — Tasks can point at a process and a form

**Implemented** as nullable columns `process_id`, `process_step_id`, `form`, `form_id`. The rule that a task must belong to a process is **not** in the create payload that was read (status is set; process is optional in the row class).

## Job cards

### BR-030 — A submitted basic job card has status Submitted

**Implemented.** `pages/job_card/job_card_basic` writes `status: 'Submitted'`.

### BR-031 — Null-status job cards are deleted when a Process Hub filter is selected

**Implemented.** The dynamic filter chip handler calls `JobCardTable().delete` where `status` is null. It does not show a confirmation dialog in that handler.

**Inferred.** This is cleanup of unfinished cards. It is not scoped, in the Dart that was read, to the current tenant or the current user. Scope then depends entirely on RLS.

**Requires Confirmation** before treating this as an approved business rule. It may be a defect.

### BR-032 — Money columns

**Implemented** as fields, not as a visible formula in Dart: `vat`, `total_no_vat`, `total_with_vat`. Totals can be requested from RPCs `calculate_total_job_card_new` and `calculate_sum_by_jobcard`.

The VAT rate and rounding rule are **Unknown / Requires Confirmation** (function bodies are not in git).

### BR-033 — Customer confirmation of labour and parts

**Implemented** as boolean `job_card.confirmed_labour_parts`. When the UI requires it to be true before submit was not fully traced.

## Process Hub

### BR-040 — A tenant sees only linked processes

**Implemented.** Chips are built from `process` rows whose ids appear in `tenant_process` for `currentTenantId`, ordered by `process.order`.

### BR-041 — The panel is chosen by an exact filter name

**Implemented.** The selected `filter_name` must equal one of:

`Job Cards`, `Scheduled Inspection`, `Building Inspection`, `Tasks`, `Equipment Inspection`, `Items`, `Bookings`, `Action Reports`, `Non-Conformance Reports`, `Corrective Action Reports`, `Tickets`, `Recommendation Reports`, `Service Reports`, `Site Forms`, `Documents`.

**Inferred.** A process whose `filter_name` is anything else produces no panel. The chip can still be selected, and BR-031 still runs.

### BR-042 — Process records

**Implemented** columns: name, created by, active (int), version (int), email template id, icon, `is_base_process`, `filter_name`, `order`.

What `active` and `version` must be for a process to show is **Unknown**. The load query does not filter `active` in the `initState` that was read. It filters by id list and orders by `order`.

## Support tickets

### BR-050 — New tickets start as New

**Implemented.** Create ticket writes `status: 'New'`.

### BR-051 — Allowed status chips on edit

**Implemented.** `New`, `Pending`, `Complete`.

Evidence: `lib/pages/pest_elite/support_ticket/edit_ticket/edit_ticket_widget.dart`.

## Pest-control forms

### BR-060 — Recommendation severity

**Implemented.** Chips include `Low`, `Moderate`, `High` on the recommendation form.

### BR-061 — Cockroach level includes None

**Implemented.** Cockroach checklist item chips: `None`, `Low`, `Moderate`, `High`.

### BR-062 — Action report yes/no

**Implemented.** At least one chip set is `Yes` / `No`.

These are field constraints in the widgets. They are **Inferred** as the allowed answers for those forms. They are not described in a written standard.

## Plateau forms

### BR-070 — Non-conformance closed-out

**Implemented.** The list prints `problemClosedOut == true` as `Yes`, otherwise `No`. A target completion date is shown (`responsibleTargetCompletionDate`).

### BR-071 — Corrective action has a target date

**Implemented** column `targetCompletionDate` on the corrective-action list.

No rule was found that forces the date to be in the future, or that blocks close-out without a date.

## Documents

### BR-080 — Document lists are tenant-scoped in the search calls

**Implemented.** `ListDocumentSearchCall` adds `tenant_id=eq.$searchTenantId`. A second call, `ListDocumentSearchRLSCall`, uses the same filter and is named as if RLS also applies.

**Unknown** whether RLS is enabled. The name of the Dart class is not evidence that Postgres RLS exists.

### BR-081 — Document access rows

**Implemented.** `document_access` is keyed by `user_id` in `DocumentAccessCall`. RPC `insert_document_access` creates access.

**Unknown / Requires Confirmation:** the full rule (owner always sees the file, or only listed users, and whether storage objects are public).

## Users and files

### BR-090 — Default profile image

**Implemented.** If add-user uploads no photo, `profile_picture` is set to a public URL on host `lwbyelfbryqvcvebcsin.supabase.co`, bucket `uploads`.

That host is not `prod-alb.simpleflowhub.app`. **Requires Confirmation** whether new users should still receive that image.

### BR-091 — Upload buckets are fixed names

**Implemented.** See the bucket table in `prd.md`. The app does not ask the user to choose a bucket.

## Email

### BR-100 — Outbound mail is a queue plus an RPC

**Implemented** structures: `send_que` (recipient, subject, body, attachment, status, result), `email_template`, `mail`, RPC `send_custom_email`, and `process.email_template_id`.

**Unknown / Requires Confirmation:** when a row is queued, which statuses exist, retry rules, and who is allowed to send.

## Rules that must not be invented from nearby repos

- Nothing in `intersect_lambda` (mail, SES, Supabase) is a rule of this app unless this repo calls it. This repo calls `send_custom_email` on its own API host. It does not reference `intersect_lambda`.
- `intersect_cloud_v2` has no rules. It has no application.

## Conflicts that need a human decision

| Topic | What the client does | What is not decided in git |
|---|---|---|
| Role names | Labels say "System Admin"; stored value is `Admin` | Which string reports and RLS should use |
| Already-linked check | Query filtered by tenant id only, in the screen that was read | Whether the intended rule is per user or per tenant |
| Null job cards | Deleted on every Process Hub filter change | Intentional cleanup or defect |
| Route protection | One route requires auth | Whether every page should require a session |
| Inactive users | Column exists; login does not read it | Whether inactive means "cannot sign in" |
| Anon key in the repo | `environment.json` is tracked | Whether that key should remain in git |
