# Business rules — mobile (LOGiiT)

Evidence date: 2026-10-09.

Source: `git@bitbucket.org:overdrive/mobile.git`, `main` at `8ef251f`.

Rules are what the client enforces or displays. The API may enforce more. Where the server rule is not visible here, it is **Unknown**. Inferred rules are marked **Inferred**.

Secrets in `credits.md` and the unused key in `lib/config.dart` are not copied.

## Identity and access

### BR-001 — A session is a JWT plus an optional offline user id

**Implemented.** SharedPreferences keys `jwt` and `offlineUserId`. Authenticated means `getUser()` left a user in memory and the method did not throw. If there is no JWT while offline, the offline user id alone can still load the local user. If both are missing, the person is logged out.

Evidence: `lib/services/auth_service.dart`.

### BR-002 — Online login stores a password hash for later offline login

**Implemented.** After a successful `api/login`, the app writes `UtilService.getHashPassword(password)` to secure storage keyed by email. Offline login compares that value. Logout does not delete it.

Evidence: `AuthService.login`, `_offlineLogin`, `logout`.

### BR-003 — Email must already be verified

**Implemented.** If `user.emailVerifiedAt` is null after login, the app logs out and shows "Please verify email before logging in".

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

### BR-004 — A web profile is required before the app will stay logged in

**Implemented.** If user details cannot be loaded, login logs out and shows "Please complete profile on web before app logging in." The dashboard uses a different sentence if details disappear later: "User details not found, contact support."

Evidence: `login_page.dart`, `landing_page.dart`.

### BR-005 — "Verified" for sharing and some notifications is three flags

**Implemented.** `isVerified` is true only when KYC `status` is the string `GREEN`, `paidUp` is true, and `supervisorApproved` is true. Any exception yields false.

This does not block entering the app.

**Unknown / Requires Confirmation:** what KYC process sets `GREEN`, and what `paidUp` means commercially.

Evidence: `AuthService.isVerified`.

### BR-006 — Unverified accounts cannot share a logbook

**Implemented.** Share shows "Account verification failed, please check web for more details" and does not call `LogSheetService`.

Also required: at least one selected dive, and a non-empty email. The email is formatted with `UtilService.formatEmail`. The DTO sets `userIsValid: true` itself. **Inferred:** the client does not check that the recipient is a LOGiiT user before send.

Evidence: `lib/pages/logbook/logbook_page_old.dart`. SHARiiT uses the same verification check in `logbook_sharing_page.dart`.

### BR-007 — Students do not get Share Logbook

**Implemented.** Drawer item is shown only when `isDiver()` and not `isStudentProfile()`. A "Student" chip is shown when `isStudent` is true. COMPiiT stays available because students are still divers (`isContractor()`).

Evidence: `lib/widgets/app_page.dart`, `User.isStudentProfile`.

### BR-008 — Navigation is role-gated; admin and company are not

**Implemented** drawer rules:

| Role | Destinations beyond Dashboard, Manage Profile, Logout |
|---|---|
| diver | Projects, COMPiiT, Share Logbook (unless student) |
| supervisor | Notifications |
| rov_pilot | ROV Tours, COMPiiT, SHARiiT |
| rov_supervisor | Notifications |
| admin, company, empty | No extra items. Manage Profile opens the supervisor profile because they are not `isContractor()` |

`isAdmin()` is unused by pages. **Unknown** whether admin is meant to use this app.

Evidence: `app_page.dart`, `lib/enum/user_type.dart`.

### BR-009 — Approver notifications are loaded by supervisor id

**Implemented.** For `isSupervisor()` or `isRovSupervisor()`, notification fetch uses field name `supervisor_id`. Otherwise it uses `from_id`.

Evidence: `lib/services/notification_service.dart`.

### BR-010 — Password reset from the profile emails the signed-in address only

**Implemented.** Body is the current user's email. Offline is rejected with "Please connect to the internet". Success is only the message `We have emailed your password reset link!`.

The login screen's Forgot Password link is a different path: it opens the website and does not call `api/reset-password`.

Evidence: `AuthService.resetPassword`, `login_page.dart`.

## Logbook content

### BR-011 — Industry logbook id follows the role family

**Implemented.** `User.industryLogbookId` is `2` for `rov_pilot` and `rov_supervisor`, and `1` for every other role. Project save copies that value onto the project.

**Inferred:** `1` is the dive logbook and `2` is the ROV logbook. The names live in industry-logbook rows from the API, which were not hard-coded.

Evidence: `lib/models/user/user.dart`, `project_widgets.dart`.

### BR-012 — Dashboard hours ignore unfinished projects and unverified ROV entries

**Implemented.**

- Dive supervisor: dives with `approvedBy` equal to their user-details id, `enabled`, on a completed project.
- Diver: dives on completed projects whose `userDetailsId` is theirs. The dive query used for hours does not also filter `approvedBy`.
- ROV supervisor: enabled contractor entries with `logbookEntryStatusId == 3` and `approverId == user.id`.
- ROV pilot: enabled entries with their `userId`, status id `3`, on a completed project.

**Inferred:** "completed" on the project is the flag that hours may count. The UI label for supervisors is "Approved Dives"; for others it is "Previous Projects".

Evidence: `lib/pages/landing/landing_page.dart`.

### BR-013 — Status ids 2–5 are the only named outcomes

**Implemented** display map: 2 APPROVED, 3 VERIFIED, 4 DECLINED BY APPROVER, 5 DECLINED BY VERIFIER. Null and every other id, including 1, display as NOT APPROVED.

Hour totals treat only 3 as verified. **Inferred:** approved (2) is not enough for the hour total. **Unknown** what the server calls id 1.

Evidence: `lib/utils/logbook_status_utils.dart`, `landing_page.dart`.

### BR-014 — A supervisor email on a save must match an admin-approved local user

**Implemented.**

- Dive save: `adminApproved` and role `supervisor` (`isValidDiveSupervisorByEmail`).
- Project save and ROV project save: `adminApproved` and role `supervisor` or `rov_supervisor`.

Failure message: "One or more of the selected supervisors are not valid".

Lookup is local-database-only (`UserService.fetchByEmail` after `find()`). **Inferred:** a valid supervisor who has never been synced onto the device fails this rule.

Evidence: `lib/services/user_service.dart`, `dive_widgets.dart`, `project_widgets.dart`, `rov_projects_crud.dart`, `logbook_entry_state.dart`.

### BR-015 — Changelog vs supervisor check

**Conflict — Requires Confirmation.** `CHANGELOG.md` v1.3.0 says "project supervisor no longer being verified before saving". `project_widgets.dart` still calls `verifySupervisorEmails()` immediately before `ProjectService().save`. Dive and ROV saves do the same.

Do not drop the check from the documented rule until a person confirms the changelog is the intended rule and the code is stale, or the reverse.

### BR-016 — Resend of notifications is silent when unverified

**Implemented.** `resendNotifications` returns immediately when the record id is null or `isVerified()` is false. No error snackbar on that path.

Evidence: `dive_widgets.dart`, `lib/widgets/logbook_entry_state.dart`.

### BR-017 — Project name is required

**Implemented.** `validateInput` returns `*required` for null or empty. Other project fields are written from controllers (start date, contractor name, client, country, vessel) without a second unique business constraint visible at the save call that was read.

Evidence: `project_widgets.dart`.

### BR-018 — COMPiiT rows belong to the signed-in user

**Implemented.** List filter is `competency.diver == user.id`, sorted by date descending. Approver submit on the view page stays disabled until a role (ODM, LSS, or Supervisor) is selected and the comment is non-empty. The selected role is stored on `approverPosition`.

Evidence: `lib/pages/compiit/compiit.dart`, `view_compiit_page.dart`.

### BR-019 — Missing COMPiiT profile redirects to profile

**Implemented.** If `findByUserId` returns no user details, a diver is sent to `DiverProfilePage` and anyone else to `SupervisorProfilePage`.

Evidence: `compiit.dart` `fetchUser`.

## Sync and connectivity

### BR-020 — "Online" means the flavor API host answered

**Implemented.** `InternetConnection` is created with a single custom check, `Uri.parse(AppEnvironment.baseApiUrl)`. A device with general internet but no route to that host is offline. Dev flavor checks `http://10.0.2.2/`.

Evidence: `lib/controller/network_controller.dart`, `lib/env.dart`.

### BR-021 — Offline edits queue; online edits hit the API

**Documented** in `README.md` and **Implemented** through action logs plus `SyncService`. The README says an online save writes both cloud and local database, and an offline save writes only the action log until sync.

Logout while status is `in_progress` forces failure reason `User logged out` and cancels background tasks.

Evidence: `README.md`, `sync_service.dart`, `AuthService.logout`.

### BR-022 — Periodic sync is opt-in and about every 15 minutes

**Implemented.** Preference `periodic_sync_enabled` defaults to false. The task requires `NetworkType.connected`. Unique name `com.logiit.backgroundSync`. Manual sync name `com.logiit.manualSync`.

Workmanager is not initialised when `kIsWeb` is true.

Evidence: `background_sync_service.dart`, `periodic_sync_service.dart`, `main.dart`.

### BR-023 — Coming back online can send an unauthenticated person to login

**Implemented.** On transition to connected, if `isAuthenticated()` is false and `logout()` returns true, the app opens `LoginPage`.

Evidence: `NetworkController._updateConnectionStatus`.

### BR-024 — The offline-API dialog does not switch the app's API

**Implemented.** The dialog polls a DNS name the person can edit. Default is the flavor `offlineApiUrl` (`logiit.local` for dev and production; a Balena device host for staging and demo). It does not assign `AppEnvironment.baseApiUrl`.

Evidence: `network_status_dialog.dart`, `env.dart`. Changelog v1.0.28 describes the dialog.

### BR-025 — Content hash ignores identity and timestamps

**Implemented.** `HashService.calculateHash` skips fields named `id`, `createdAt`, `updatedAt`, and `hash`, then SHA-256s the rest. Project save assigns `project.hash` before save. Changelog says user and user-details models also carry hashes. Those fields exist on the models.

**Inferred:** the hash is a change detector for sync, not a password and not a signature a person verifies.

Evidence: `lib/services/hash_service.dart`, `project_widgets.dart`, `CHANGELOG.md`.

## Files and device permissions

### BR-026 — Private files use a secure URL while online

**Implemented.** Paths that do not already contain `private` are prefixed with `private/`. Online: POST `api/getSecureUrl`. Offline: `{baseApiUrl}secure-file-download/{type}/{file name}`.

Evidence: `lib/services/file_service.dart`.

### BR-027 — Gallery permission follows OS version

**Implemented.** iOS requests photos. Android API 32 and below requests storage. Android API 33 and above requests photos. Camera is a separate request. Battery-optimisation exemption and location can be requested by `PermissionService`; the product reason for location was not found in a screen requirement. **Unknown** which user action asks for location.

Evidence: `lib/services/permissions.service.dart`.

## Display and build rules

### BR-028 — One Android id except staging

**Implemented.** `co.logiit.app` for dev, demo, and production. `co.logiit.app.staging` for staging. Display names: "LOGiiT - DEV", "LOGiiT - Staging", "LOGiiT - Demo", "LOGiiT".

Evidence: `android/app/build.gradle`.

### BR-029 — Release builds without a keystore are debug-signed

**Implemented** and **Documented** in README and `TEST_RESULTS.md`. `android/key.properties` is absent from git.

### BR-030 — Login fields have no format rule beyond non-empty

**Implemented.** `isValid` is `email != '' && password != ''`. Empty submit shows "Please enter email and password". There is no minimum password length in the client.

Evidence: `login_page.dart`.

## Rules that must not be invented from leftovers

- ODM and LSS are labels on a COMPiiT dropdown. No rule in the client defines those jobs.
- `Config.apiKey` is not an authentication rule of the running app. Nothing reads it.
- Accounts written in `credits.md` are not an in-app backdoor. The login screen does not pre-fill them.
- The REST conversion document's proposed URLs are not current business behaviour.
