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.
On this page
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 |
/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.
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:
curl http://facilities-manager.localhost/healthreturns{"status":"ok"}.- The marketing home page renders with styling and its navigation links resolve.
/operations/loginrenders and a seeded user can sign in.- The dashboard, an asset list and a work-order list all load without a 500.
make artisan cmd="migrate:status"shows no pending migrations.make artisan cmd="queue:monitor"or the queue container's logs show a worker consuming jobs — uploads, exports and reminders depend on it.- Upload one photo and confirm it appears in the MinIO console.
- 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.
Request to closed work order
- As a requester, open
/portaland submit a request against Room 301 and the rooftop AHU.
Expect: the request appears in your list assubmitted, with a reference you can reopen later. - 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. - Approve the request.
Expect: statusapproved, and a triage event recording who approved it and when. - Convert it to a work order.
Expect: a new work order indraftwith a generated number, the request's priority, location and asset copied across, and the request nowconverted. Converting twice must not create a second work order. - On
/dispatch, assign it to a technician with a planned window.
Expect: statusassigned; the order appears in that technician's workbench and nowhere else. - As that technician on
/technician, start the work, complete a task, add a note and a meter reading, pause, then resume.
Expect: statusin_progress, paused time accumulating as hold minutes, and each action written to the history timeline. - 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 statuscompletedwith an actual duration that excludes the paused minutes. - As the supervisor, approve, then close.
Expect:approvedthenclosed, each as its own history event. - As the requester, confirm the resolution, then leave feedback.
Expect: the request moves toresolvedand thenconfirmed; reopening instead setsreopened.
Assets, labels and the hierarchy
- Browse the hierarchy tree from portfolio down to Room 301.
Expect: the full path renders and the asset is listed under the room. - Open the asset and add a photo and a document.
Expect: both upload, appear on the asset, and are visible in MinIO. - Print the asset label and scan its QR code.
Expect:/asset-scan/{token}resolves to that asset with its location path. - 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. - 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.
Inspection to corrective work
- Create a checklist template with a few items and publish it.
Expect: an immutable version; editing the template afterwards must not change that version. - Schedule an inspection against Room 301 for an inspector.
Expect: it appears in the inspector workspace asscheduled. - 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. - 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. - 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.
Preventive maintenance, fleet and inventory
- Create a monthly PM schedule for the AHU with a checklist and generate the
due occurrence.
Expect: one work order withsource=pm; generating the same occurrence again returns the same work order, never a duplicate. - 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. - 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. - Add and approve a labor entry.
Expect: the work-order cost rolls up parts plus approved labor.
Mobile and offline behaviour
- 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. - 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. - 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. - 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.
- Surface and URL — for example the dispatch board at
/dispatch. - Who you were — the account and its role and location scope.
- The record — the work-order number, asset code or request reference.
- Steps — numbered, from a known state.
- Expected versus actual — quote the expectation from the scenario above.
- Evidence — a screenshot, and the timestamp so the log and audit entry can be found.