# Business rules — intersect_cloud (Simpleflow Hub)

Evidence date: 2026-10-12. Source: `main` at `6efd925`.

The repository has no business-rules document. Every rule below is **Inferred** from implementation unless a row says otherwise. A rule that the code enforces is not, by itself, an approved business rule. Rows marked **Requires Confirmation** need a person to say whether the behaviour should stay.

## Identity and access

### BR-001 — Two role fields

**Inferred.** A user has a system role and a tenant role.

- System role is `user.user_role`, copied to `FFAppState.systemRole` at login.
- Tenant role is `tenant_user_list.role` on the first row returned for that user, copied to `FFAppState.tenantRole`.

The add-user screen stores system role `Admin` or `User`. The dropdown labels are "System Admin" and "System User". Tenant role stored values are `Admin` and `User`, labelled "Tenant Admin" and "Tenant User".

Evidence: `login_widget.dart`, `add_user_widget.dart`.

**Requires Confirmation:** what happens when `tenant_user_list` returns more than one row. The code uses `firstOrNull`.

### BR-002 — System admin gate

**Inferred.** If `systemRole` is not `Admin`, these pages navigate to Profile on first frame: Management, add user, edit user, edit user search, add tenant, edit tenant, edit tenant search, add client admin, edit client.

Side nav shows the Management entry when `systemRole` or `tenantRole` is `Admin`. A system admin is sent to route `Management`. A tenant admin is sent to route `ManagementUser`. Anyone else opening that control goes to Profile.

Evidence: `lib/pages/management/**`, `side_nav_widget.dart`.

### BR-003 — Tenant admin gate

**Inferred.** If `tenantRole` is not `Admin`, these pages navigate to Profile: ManagementUser, user list tenant admin, add user tenant admin, edit user tenant admin, tenants list user, edit user tenant, add user tenant, clients list user, edit client user.

Evidence: the `initState` checks in those widgets.

### BR-004 — Router does not require auth except for support tickets

**Inferred, and likely unintentional.** `FFRoute.requireAuth` defaults to false. Only `SupportTicketList` sets it true. Logged-out `/` still shows Login, because the `/` builder checks `appStateNotifier.loggedIn`. Other named routes do not.

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

**Requires Confirmation:** whether every signed-in area should require a session.

### BR-005 — OTP does not create a user

**Inferred.** `getOtp` calls `signInWithOtp` with `shouldCreateUser: false`.

Evidence: `lib/custom_code/actions/get_otp.dart`.

### BR-006 — Sign-out clears the system role

**Inferred.** The More menu and side nav set `FFAppState.systemRole` to an empty string on sign-out. Whether the rest of the secure-storage keys are cleared in that same handler was not fully read.

Evidence: `more_nav_bar_widget.dart`, `side_nav_widget.dart`.

**Requires Confirmation:** the full sign-out cleanup.

## Tenant and branding

### BR-007 — Session tenant is the first linked tenant

**Inferred.** Login sets `currentTenantId` and `selectedTenantName` from the first `tenant_user_list` row and writes that id to `user.current_tenant_id`.

Evidence: `login_widget.dart`.

### BR-008 — Colours come from the tenant

**Inferred.** Login reads `primary_color`, `accent_color`, `background_color`, `header_color`, `tile_color`, and `menu_color`. Defaults when missing: primary and header and menu `#ff9900`, accent and tile `#495564`, background `#ffffff`.

`updateAppColorsPostLogin` uses different defaults (`#ff9d00`, `#125cba`, `#f62626`) and reads `user_profiles`, which has no generated table class. The Login button does not call that function.

Evidence: `login_widget.dart`, `update_app_colors_post_login.dart`.

**Requires Confirmation:** which colour path is live in the FlutterFlow action graph.

### BR-009 — Process chips are tenant data

**Inferred.** Chips are `process.filter_name` for processes linked in `tenant_process` to the current tenant, ordered by `process.order`.

Evidence: `process_hub_widget.dart`.

## Process Hub

### BR-010 — Changing a filter deletes job cards with a null status

**Inferred from code. Requires Confirmation as a rule.**

On every Process Hub chip change the app runs `JobCardTable().delete` where `status` is null. The filter in that call does not add `tenant_id` or `user_id`.

Evidence: `process_hub_widget.dart` around the choice-chip `onChanged`.

### BR-011 — Job card list is user and tenant scoped

**Inferred.** When the chip is `Job Cards`, `JobCardCall` sends `searchUserId` (`currentUserUid`), `searchTenantId`, and `order=created_at.desc`.

Evidence: the same handler, and `JobCardCall` in `api_calls.dart`.

### BR-012 — Known filter labels

**Inferred.** The hub has branches for these exact strings: `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`.

A database `filter_name` that does not match one of those strings does not open one of those branches.

Evidence: string comparisons in `process_hub_widget.dart`.

## Users

### BR-013 — Active flag on create

**Inferred.** Add user sets `active` true when the choice chip value is `Active`, otherwise false.

Evidence: `add_user_widget.dart`.

### BR-014 — Login requires profile rows

**Inferred.** After auth succeeds, the code force-unwraps the first `tenant_user_list` row and the first `user` row. A missing row is a throw, not a message that was read in this pass.

Evidence: `login_widget.dart`.

**Requires Confirmation:** the intended message when the auth user has no tenant link.

## Mail and files

### BR-015 — Custom email payload

**Inferred.** `send_custom_email` is called with template name, recipient, placeholder JSON, recipient user id, attachment, and current tenant id. The bearer token sent is the `authToken` argument because that `Authorization` entry is the later key in the header map. `apikey` remains the anon key.

Evidence: `SendCustomEmailCall`.

The RPC body is not in this repo. Delivery rules are **Unknown**.

### BR-016 — `send_que` is a queue table on the client only

**Inferred.** The generated row has recipient, subject, body, attachment, status, and result. No worker in this repo reads the table.

Evidence: `lib/backend/supabase/database/tables/send_que.dart`.

**Requires Confirmation:** which system sends those rows.

### BR-017 — Uploads use named buckets

**Inferred.** JPG and signature uploads use storage bucket `uploads`. `upload_file.dart` uses bucket `pdfUploads`. Public object URLs in the UI also point at `logos` and `files` on `https://alb.simpleflowhub.app`.

Evidence: `upload_file.dart`, `upload_file_jpg.dart`, signature widgets, hardcoded URLs in dashboard, side nav, and profile.

## Product strings that look like environment rules

### BR-018 — Backend host on `main`

**Implemented configuration, not a written rule.** `assets/environment_values/environment.json` sets `SupabaseAPIURL` to `https://alb.simpleflowhub.app`. The Dart constant `currentEnvironment` is `demoQA`.

**Requires Confirmation:** whether this host is demo, QA, staging, or production. `intersect_cloud_prod` uses `https://prod-alb.simpleflowhub.app`.

### BR-019 — Demo site links are hardcoded

**Inferred.** Create-task screens contain `https://www.demo.simpleflowhub.app/tasks`. `user_landing_widget.dart` assigns `https://demo.simpleflowhub.app/` to `siteLink`. The landing page is not exported by `lib/index.dart`.

**Requires Confirmation:** whether those links are still correct.

## Release rules visible in config

### BR-020 — Web deploy does not compile

**Inferred from the pipeline.** `main` deploys the committed `build/web` to the Bitbucket deployment named staging. `prod` deploys the committed `build/web` on that branch to the deployment named production. The YAML build step is commented out on both.

Evidence: `bitbucket-pipelines.yml`.

### BR-021 — Android release uses the debug signing config

**Implemented.** `android/app/build.gradle` sets `signingConfig signingConfigs.debug` on the release build type. A comment above it says signing uses debug keys so `flutter run --release` works. A `signingConfigs.release` block reads `key.properties` if that file exists, and the release build type does not use it.

### BR-022 — Web should not be indexed

**Implemented.** `web/index.html` contains `<meta name="robots" content="noindex" />`.

**Requires Confirmation:** whether production web is supposed to stay `noindex`.

## What was not established

- Row Level Security, and whether the client filters are the real access control.
- Password length, email format, and other field validators inside each form. FlutterFlow validators are attached in widgets; they were not extracted form by form.
- Who may edit a job card, inspection, or Pest Elite report after create.
- Retention, audit, or approval chains. Process tables (`process_step`, `process_instance`, `process_action`) exist as generated clients. Their rules are **Unknown**.
- Whether the unwired `user_management` tree and the extra Process Hub copies were ever a supported path.
