# Business rules — coreUnit

Evidence date: 2026-10-18.

Source: `git@bitbucket.org:overdrive/coreunit.git`, `master` at `e424b25`.

Rules below are taken from validators, schema, middleware, and client checks. Where the code implies a rule that no comment or README states, the rule is marked **Inferred**. No rule was copied from a policy document. There is no policy document in the repository.

## Identity and access

### BR-001 — A user is an email and a password

**Implemented.** Table `users` (`api/database/migrations/2014_10_12_000000_create_users_table.php`):

- `id` big increment
- `name` string, required by both register validators
- `email` string, unique index, required, must pass Laravel's `email` rule
- `email_verified_at` nullable timestamp
- `password` string
- `remember_token`, `created_at`, `updated_at`

Mass-assignable fields on `App\User` are `name`, `email`, and `password` only.

### BR-002 — Password storage is bcrypt

**Implemented.** Both register actions set `password` to `bcrypt($input['password'])` before `User::create`. `api/config/hashing.php` uses bcrypt with `BCRYPT_ROUNDS` or 10. Login checks the password through `Auth::attempt`.

The API does not set a minimum length. The small app's Yup schema does (BR-012). Those rules apply to different programs.

### BR-003 — Confirm password must match, and is not stored

**Implemented.** Both register validators require `c_password` with `same:password`. `c_password` is not in `$fillable`, so it is dropped on create.

### BR-004 — Email uniqueness is a database constraint

**Implemented** as a unique index.

**Inferred.** Neither register validator uses `unique:users`. A second insert with the same email fails in the database and is not returned as the validation `error` object.

### BR-005 — Email verification is declared and not enforced

**Implemented** declaration: `User` implements `MustVerifyEmail`. The users table has `email_verified_at`. `EventServiceProvider` maps `Registered` to `SendEmailVerificationNotification`.

**Inferred.** `AuthController::register` and `RegisterController::register` never fire `Registered`. No route uses middleware `verified`. `Auth::attempt` does not consult `email_verified_at`. A user can log in with a null `email_verified_at`.

### BR-006 — API authentication is a Passport bearer token

**Documented** in the README: "Auth uses Passport".

**Implemented.** Guard `api` uses driver `passport` and provider `users`. `User` uses `HasApiTokens`. `AuthServiceProvider::boot` calls `Passport::routes()`. Login and both registers call `createToken`. Protected routes use `auth:api`.

Token names differ: `AppName` on the v1 actions, `MyApp` on `POST /api/register`. **Inferred** that the name is a label stored with the token, not a second user type. Passport's own tables are not in the committed migrations, so a fresh `php artisan migrate` does not create them.

### BR-007 — There is one user kind

**Inferred.** No role column, no gate, and no policy class exist. Any valid token can list, create, update, and delete every product. `GET /api/user` returns whichever user the token belongs to.

The Fuse roles `admin`, `staff`, `user`, and guest (`frontend/src/app/auth/authRoles.js`) are theme constants. The home login builds a local object with role `admin` and does not dispatch it. They do not gate the Laravel API.

### BR-008 — Session login controllers are not in the route map

**Implemented.** The files under `api/app/Http/Controllers/Auth/` exist. `routes/web.php` does not call `Auth::routes()`. A browser session login, a password-reset mail, and a verification link are not reachable through the committed routes.

`password_resets` is created by `2014_10_12_100000_create_password_resets_table.php` (`email` index, `token`, nullable `created_at`). The broker expiry is 60 minutes. Nothing writes the table.

### BR-009 — Remember Me is not a server rule

**Implemented** on the Fuse forms as a checkbox defaulting to checked. Neither login request sends it. `Auth::attempt` is called with email and password only.

## Products

### BR-010 — A product is a name and a detail

**Implemented.** Migration `2019_07_13_213211_create_products_table.php`:

- `id` increment (integer, not big integer)
- `name` string, required on create and update
- `detail` text, required on create and update
- timestamps

No price, SKU, owner, or status column exists. `$fillable` is `name` and `detail`.

### BR-011 — Products are global

**Inferred.** `index` returns `Product::all()` with no `user_id` and no query scope. Update replaces `name` and `detail` only. Delete removes the row. Any authenticated caller can act on any id.

### BR-012 — Missing product

**Implemented.** `show` loads by id and returns `Product not found.` with HTTP 404 when `find` returns null.

**Inferred.** `update` and `destroy` type-hint `Product $product`. Laravel resolves that before the method. A missing id does not reach `sendError`. `create` and `edit` routes are registered and the controller does not define those methods.

## Client-side rules that the API does not share

### BR-013 — Small-app password shape

**Implemented** in `public/src/components/LoginForm.js` only:

- Email required and must be an email address.
- Password required, minimum 8 characters, must match `(?=.*[0-9])`.
- Submit does not call the API. A commented fixed-username check is not executed.

### BR-014 — Fuse login only checks non-empty fields

**Implemented** in `Login.js` and `Login2Page.js`. `isFormValid` is true when `email.length > 0` and `password.length > 0`. There is no format check. The Login button stays disabled until both are non-empty.

### BR-015 — Dashboard rows are the initial array

**Implemented.** `Dashboard` seeds four clients and renders `this.state.clients`. The Axios callback writes `contacts`. The hardcoded rows stay on screen after the request.

## Cross-cutting rules

### BR-016 — Throttle

**Implemented.** The `api` middleware group allows 60 requests per minute per client (`throttle:60,1`), including login, both registers, and products.

### BR-017 — Validation errors use two status codes

**Implemented.**

- v1 register and v1 login failures: HTTP 401.
- Unversioned register and product validation failures: HTTP 404 (`sendError` default).
- Product not found from `show`: HTTP 404.
- Login failure body is exactly `error: Unauthorised`. There is no 204 response in the controller. Both Fuse pages contain a 204 branch.

### BR-018 — CORS allows any origin

**Implemented** in `api/config/cors.php`: `allow_origins` is `*`, `allow_headers` is `*`, `allow_credentials` is true. The route middleware sets `Access-Control-Allow-Origin` to `*` again for v1 login and register.

**Inferred.** Browsers reject `Access-Control-Allow-Origin: *` together with credentialed responses. Whether a browser call from port 3000 succeeds was not executed here.

### BR-019 — No owner, tenant, or audit trail

**Inferred.** Users and products have `created_at` and `updated_at` only. There is no `created_by`, soft delete, or history table. Product delete is a hard delete and the response includes the attributes just deleted.

### BR-020 — Duplicate register contracts

**Implemented.** `POST /api/register` and `POST /api/v1/register` both create a user. They differ in HTTP status on failure, JSON shape, and token name (see the PRD). No README sentence chooses one.

## Rules that are not in force

- Google and Facebook login buttons render and have no handler.
- Forgot-password and register-2 links open Fuse pages. Those pages do not post to Laravel.
- `MustVerifyEmail` does not block `auth:api`.
- The navigation role examples do not change `ProductController`.
- Queue, mail, and broadcast settings in `.env.example` do not encode a business process. No job class or mailable exists.
