Documentation

How to test the application

Three layers, in order: get a tenant with data, prove the build is green, then walk the acceptance scenarios by hand. Each scenario states the expected result so a tester can pass or fail it without asking a developer.

1. Bring the environment up

The whole stack runs under Docker Compose: Nginx, PHP-FPM, Postgres, Redis, a queue worker, a scheduler, Mailpit for mail and MinIO for object storage.

cp .env.example .env
docker compose up -d --build
docker compose exec php php artisan key:generate
docker compose exec php php artisan migrate
npm install && npm run build

Or the shorthands in the Makefile: make up, make migrate, make ps, make logs, make build-assets.

Service URL Use it to
Application http://facilities-manager.localhost Everything below
Health check /health Confirm the app booted before testing anything else
Mailpit http://localhost:8025 Read notification and invitation emails
MinIO console http://localhost:9003 Verify uploaded photos, documents and exports landed
Check /health returns {"status":"ok"} first. If it does not, nothing else in this page will behave, and the answer is in make logs.

2. Seed a demo tenant

No admin user or sample data ships by default — credentials are never committed. Run this once against the running stack. It builds one full location branch from portfolio down to a room, an asset in that room, and your login.

docker compose exec php php artisan tinker --execute="
\$org = App\Models\Organization::factory()->create(['name' => 'Demo Org']);
\$admin = App\Models\User::factory()->create([
    'name' => 'Demo Admin',
    'email' => 'demo@facilities-manager.localhost',
    'password' => 'demo-password-123',
    'is_admin' => true,
]);
App\Models\Membership::factory()->for(\$admin)->for(\$org)->create();

\$portfolio = App\Models\Portfolio::factory()->for(\$org)->create(['name' => 'EMEA Portfolio']);
\$continent = App\Models\Continent::factory()->create(['portfolio_id' => \$portfolio->id]);
\$country = App\Models\Country::factory()->create(['continent_id' => \$continent->id]);
\$city = App\Models\City::factory()->create(['country_id' => \$country->id]);
\$facility = App\Models\Facility::factory()->create(['city_id' => \$city->id]);
\$building = App\Models\Building::factory()->create(['facility_id' => \$facility->id]);
\$floor = App\Models\Floor::factory()->create(['building_id' => \$building->id]);
\$room = App\Models\Room::factory()->create(['floor_id' => \$floor->id, 'name' => 'Room 301', 'kind' => 'room']);

\$category = App\Models\AssetCategory::factory()->create(['organization_id' => \$org->id]);
\$asset = App\Models\Asset::factory()->create([
    'organization_id' => \$org->id,
    'location_node_id' => \$room->locationNode()->firstOrFail()->id,
    'asset_category_id' => \$category->id,
    'name' => 'Rooftop AHU Unit 1',
]);

echo 'Login: '.\$admin->email.' / demo-password-123'.PHP_EOL;
"

Sign in at /operations/login with the printed credentials. Requesters, technicians and inspectors all use this same login; what differs is the permissions on their role and which surface they are sent to.

For a real UAT environment, do not use these credentials. Create named accounts per participant with the role each of them is meant to exercise, so the audit trail shows who did what during the session.

3. Run the automated suite

Before any manual session, prove the build is green. The suite covers the domain actions, the policies, the API contract and cross-tenant isolation.

make test                                   # full PHPUnit suite
docker compose exec php php artisan test --filter=WorkOrder
docker compose exec php php artisan test tests/Feature/ConvertServiceRequestToWorkOrderTest.php

make pint-check                             # PHP code style
make phpstan                                # static analysis
npm run lint                                # JavaScript and Vue
make lint-all                               # style + static analysis + tests

During development, run the checks against only the files you changed. Before a UAT deployment, run the whole thing.

4. Deployment smoke checks

Five minutes of checks that catch most bad deploys before a tester finds them:

  1. curl http://facilities-manager.localhost/health returns {"status":"ok"}.
  2. The marketing home page renders with styling and its navigation links resolve.
  3. /operations/login renders and a seeded user can sign in.
  4. The dashboard, an asset list and a work-order list all load without a 500.
  5. make artisan cmd="migrate:status" shows no pending migrations.
  6. make artisan cmd="queue:monitor" or the queue container's logs show a worker consuming jobs — uploads, exports and reminders depend on it.
  7. Upload one photo and confirm it appears in the MinIO console.
  8. Trigger one notification and confirm it lands in Mailpit.

5. UAT scenarios

Walk these in order — each one leaves the data the next one needs. Read the data-flow page alongside them if you want to know why each step lands where it does.

Scenario A

Request to closed work order

  1. As a requester, open /portal and submit a request against Room 301 and the rooftop AHU.
    Expect: the request appears in your list as submitted, with a reference you can reopen later.
  2. As a supervisor, find it in the operations panel and ask a clarification question.
    Expect: the requester sees the question in the portal and can answer it; both messages persist on the request.
  3. Approve the request.
    Expect: status approved, and a triage event recording who approved it and when.
  4. Convert it to a work order.
    Expect: a new work order in draft with a generated number, the request's priority, location and asset copied across, and the request now converted. Converting twice must not create a second work order.
  5. On /dispatch, assign it to a technician with a planned window.
    Expect: status assigned; the order appears in that technician's workbench and nowhere else.
  6. As that technician on /technician, start the work, complete a task, add a note and a meter reading, pause, then resume.
    Expect: status in_progress, paused time accumulating as hold minutes, and each action written to the history timeline.
  7. Complete the work with an action, root cause and notes.
    Expect: it refuses to complete if a required field or required evidence is missing; otherwise status completed with an actual duration that excludes the paused minutes.
  8. As the supervisor, approve, then close.
    Expect: approved then closed, each as its own history event.
  9. As the requester, confirm the resolution, then leave feedback.
    Expect: the request moves to resolved and then confirmed; reopening instead sets reopened.
Scenario B

Assets, labels and the hierarchy

  1. Browse the hierarchy tree from portfolio down to Room 301.
    Expect: the full path renders and the asset is listed under the room.
  2. Open the asset and add a photo and a document.
    Expect: both upload, appear on the asset, and are visible in MinIO.
  3. Print the asset label and scan its QR code.
    Expect: /asset-scan/{token} resolves to that asset with its location path.
  4. Move the asset to another room and record the reason.
    Expect: a movement record awaiting decision, and once completed, a new history event and an updated location path.
  5. Import a small CSV of assets.
    Expect: a preview showing valid and invalid rows before anything is written, and a commit step that creates only the valid rows.
Scenario C

Inspection to corrective work

  1. Create a checklist template with a few items and publish it.
    Expect: an immutable version; editing the template afterwards must not change that version.
  2. Schedule an inspection against Room 301 for an inspector.
    Expect: it appears in the inspector workspace as scheduled.
  3. Run the guided inspection, failing at least one item, and attach a photo and a signature.
    Expect: a live score that updates as you answer.
  4. Complete the inspection.
    Expect: one corrective work order per failed item, each named after the failed item's prompt, and a completed inspection you can open as a report.
  5. Close a corrective work order, then verify it in the compliance workspace.
    Expect: the verification records the decision and notes, and the evidence is attachable against an obligation.
Scenario D

Preventive maintenance, fleet and inventory

  1. Create a monthly PM schedule for the AHU with a checklist and generate the due occurrence.
    Expect: one work order with source=pm; generating the same occurrence again returns the same work order, never a duplicate.
  2. Record a fleet odometer reading past a maintenance interval.
    Expect: a fleet maintenance work order at the right priority, and no second order for the same milestone.
  3. Reserve stock for a work order, then issue it.
    Expect: a reservation, then an immutable ledger entry naming that work order, and a store balance that moves by exactly the issued quantity.
  4. Add and approve a labor entry.
    Expect: the work-order cost rolls up parts plus approved labor.
Scenario E

Mobile and offline behaviour

  1. Log in to the Flutter app with the same credentials and pull the work-order list.
    Expect: only work assigned within your organization and location scope.
  2. Put the device in airplane mode, start a work order and complete a task, then reconnect.
    Expect: the queued actions sync once and only once; retrying does not double-apply.
  3. Edit a work order on the web while the device is offline, then let the device sync a conflicting change.
    Expect: the stale mutation is rejected with a clear message rather than silently overwriting.
  4. Scan an asset QR code from the app.
    Expect: the asset and its location path resolve.

6. Negative and security checks

A UAT that only tests the happy path proves very little. Run at least these:

  • Cross-tenant isolation — seed a second organization, then try to open its asset, work order or request by ID as a user of the first. Expect a 404 or 403, never data.
  • Location scope — give a user a grant on one facility only and confirm work at another facility is invisible and un-actionable.
  • Permissions — a technician must not be able to approve their own completion; a requester must not reach the operations panel.
  • Lifecycle guards — converting a rejected request, completing an unassigned work order or approving one that is not completed must all be refused.
  • Rate limits — repeated failed logins, searches, uploads and exports are throttled.
  • Multi-factor — enrol an authenticator app from the profile page and confirm the code is required at the next sign-in, and that a recovery code works once.
  • Audit trail — after the session, export the audit trail and confirm every participant's actions are present and unedited.

7. Reporting a defect

A useful UAT defect report has six lines. Anything less and it will come back as a question.

  1. Surface and URL — for example the dispatch board at /dispatch.
  2. Who you were — the account and its role and location scope.
  3. The record — the work-order number, asset code or request reference.
  4. Steps — numbered, from a known state.
  5. Expected versus actual — quote the expectation from the scenario above.
  6. Evidence — a screenshot, and the timestamp so the log and audit entry can be found.
Going deeper. The full feature test script walks every shipped feature individually and marks which ones are clickable in the browser today versus backend-verified with no UI yet — worth reading before a demo so nobody hunts for a button that does not exist. The delivery plan shows what is still to come.