# Seed Data Reference

Every id, UUID, and email used as an example value throughout `docs/api/*.md` and `docs/postman-collection.json` was queried directly from the live seeded database (`php artisan tinker`, MySQL `maintenance` DB) on **2026-08-21**, not invented. This file is the single source of truth for those values, and the place to look when a request 404s/422s because the underlying seed data has since changed (a reseed, a test run that consumed a row, etc.) — re-run the queries in [How to Refresh This File](#how-to-refresh-this-file) and update accordingly.

> **Refreshed 2026-08-22** after `ContractVisitLedger` was removed from the codebase (contracts now materialize their visit quota as real `Order` rows — see [Company](#company) below) and a `migrate:fresh --seed` was re-run to verify it. That reseed also surfaced and fixed a real bug: `ContractSeeder`/`ContractService::generateVisitOrders()` called `$branchService->category?->name` without eager-loading `category.translations`, which threw `LazyLoadingViolationException` and made the seeder — and therefore `admin.contracts.approve` in production — fail outright; both now eager-load `branchServices.category.translations`. ⚠️ **`OrderSeeder`'s `Order::factory()` calls are not seeded/deterministic** (status and, on non-contract orders, `technician_id` come from unseeded Faker), so the ids/emails below are stable across reseeds but **exact order statuses, counts-by-status, and which specific order demonstrates which status are not** — re-run the queries below after any reseed rather than trusting a stale count.

Organized to match the API docs: **Client → Technician → Company → Admin → Shared/Related Entities** (the reference/catalog data more than one role touches).

## Client

| id | status | email | notes |
|---|---|---|---|
| 3 | active | individual@info.com | **primary seeded client login used throughout `02-client.md`** (and `01-guest.md` §01 for the login call itself); owns address 1 |
| 7 | **suspended** | suspended.individual@info.com | demonstrates the 403 `account_suspended` login path |
| 20 | active | qaclient_...@example.com | "QA Test Client" — owns the default address (id 11); has **zero orders** as of the 2026-08-22 refresh (`OrderSeeder` non-determinism — see note above) |

Password for all: **`password`**. (2 more individual rows exist beyond these 3 — see `database/seeders/UserSeeder.php`.)

**Client's own address:**

| id | user_id | title | country | governorate | region | default |
|---|---|---|---|---|---|---|
| 1 | 3 (individual@info.com) | Other | Egypt (1) | Alexandria (2) | — | no |
| 11 | 20 (QA client) | Home | Saudi Arabia (2) | — | — | **yes** |

**Client's own orders** (customer_id = 3, individual@info.com) — **only 2 rows, `OrderSeeder` creates exactly 2 per individual/company user**, status/uuid reshuffle on every reseed (see the non-determinism note above); as of the 2026-08-22 refresh:

| status | example uuid |
|---|---|
| pending | `ae2bd438-4f51-48b7-b197-2f6525dc1f7c` |
| accepted | `bab9f22d-53d9-4076-b968-6aad178581a0` |

⚠️ **No `finish`-status order currently exists for this client** (or for QA client 20 below) — `client.orders.confirm-completion` has no valid live target this run. Re-run `php artisan tinker --execute="\App\Models\Order::where('customer_id', 3)->get(['uuid','status'])"` after any reseed to find one, or drive an order through the full lifecycle first (Workflow 1 in [WORKFLOWS.md](WORKFLOWS.md)).

⚠️ **Client cannot use the wallet-withdraw endpoint** — `client.wallet.withdraw` exists on the shared `WalletController` but `WalletValidationService::validateWithdrawal()` rejects any non-technician wallet outright (422 `withdrawal_not_allowed`). It's really a technician-only action wearing a client-prefixed URL — see **Technician** below.

Full catalog (categories), geo hierarchy, and the complete orders table (68 rows as of the 2026-08-22 refresh, all statuses) are in **[Shared / Related Entities](#shared--related-entities)** below, since Client, Company, and Admin all read the same rows.

## Technician

| technicians.id | users.id | email | employment_type | availability_status | status |
|---|---|---|---|---|---|
| 1 | 5 | technician@info.com | in_house | available | active — **the seeded technician login used throughout `03-technician.md`, but cannot withdraw (see below) and has zero orders of their own** |
| 2 | 15 | fahad.technician@info.com | in_house | available | active |
| 3 | 16 | mohammed.technician@info.com | **freelance** | unavailable | active — **the only freelance+active technician; use for wallet-withdrawal demos** |
| 5 | 18 | khalid.technician@info.com | freelance | unavailable | **pending** — use for onboarding-approval demos |

(technicians.id 4 and 6, users.id 17 and 19, both `in_house`, omitted — not referenced anywhere in the docs.)

`admin.orders.assign-technician`'s `technician_id` field means **`technicians.id`**, not `users.id` — mixing them up is the single most common mistake when exercising this API by hand.

**Technician-assigned orders** — as of the 2026-08-22 refresh, **zero rows anywhere have `technician_id` set** (non-contract orders get their `technician_id` from `Order::factory()`'s unseeded Faker — see the non-determinism note at the top of this file; it happened to assign none this run). The default seeded technician login (technician@info.com, technicians.id=1) has zero orders of any status regardless.

⚠️ **No order is currently `pending` with a technician attached** — nothing is in the "awaiting technician response" state that `technician.orders.accept-assignment`/`.reject-assignment` act on. To exercise Accept/Reject → Status Updates → Completion end-to-end, `admin.orders.assign-technician` must be called first in the same run — the Postman collection does this automatically via the chained `{{order_id}}` variable (see [README.md](README.md#importing-the-postman-collection)). Re-check with `php artisan tinker --execute="\App\Models\Order::whereNotNull('technician_id')->get(['uuid','status','technician_id'])"` after any reseed — a run that does assign one changes this section.

⚠️ **Wallet/withdrawal eligibility is code-enforced by `employment_type`, not just documentation**: only `technicians.id=3` (mohammed.technician@info.com) is both `freelance` and `active` — the only account that can actually call `technician.wallet.withdraw` successfully. `technicians.id=1` (the default login) is `in_house` → 422 `wallet_locked_for_account_type`. `technicians.id=5` (khalid) is freelance but `pending` → can't even log in.

⚠️ **Wallets and Withdraw Requests: zero rows exist for anyone** — see [Shared / Related Entities](#shared--related-entities) for the full detail; this blocks the withdrawal flow from being demonstrated end-to-end no matter which technician you use, until a wallet is seeded directly in the database (there is no create-wallet endpoint in the API).

## Company

| id | status | email | notes |
|---|---|---|---|
| 4 | active | company@info.com | simplest company profile — **no branches, no contracts** |
| 6 | **pending** | pending.company@info.com | use for onboarding-approval (`activate`/`reject`) demos |
| 9 | active | alamal.facilities@info.com | owns branch 3 and contract 2 (`pending_signature`, not yet approved — no visit orders generated for it) |
| 10 | active | gulf.maintenance@info.com | **the seeded company login used throughout `04-company.md`**; owns branches 1–2 and contract 1 (`active`) |
| 11–14 | pending/active/suspended/rejected | najd.property, riyadh.technical, blocked.ventures, declined.contractors (@info.com) | demonstrate the remaining account-status paths; none own branches |

Password for all: **`password`**.

**Branches — only 3 rows exist across all 8 companies:**

| id | company_id | name |
|---|---|---|
| 1 | 10 (gulf.maintenance) | Gulf Maintenance - Riyadh Main |
| 2 | 10 (gulf.maintenance) | Gulf Maintenance - Jeddah Branch |
| 3 | 9 (alamal.facilities) | Al Amal Facilities - Head Office |

Companies 4, 6, 11, 12, 13, 14 have **no branches at all** — job-order checkout and contract creation for those would need a branch created first.

**Contracts — only 2 rows exist:**

| id | company_id | status | visits_allowed | branch_services rows | visit orders generated |
|---|---|---|---|---|---|
| 1 | 10 (gulf.maintenance) | **active** | 24 | 4 (2 branches × 2 categories) | **48** — see note below |
| 2 | 9 (alamal.facilities) | **pending_signature** | 12 | 1 (1 branch × 1 category) | 0 (only `approve` materializes orders) |

⚠️ Contract 1's `branch_services` reference `category_id=1`, a **parent** category with `price: null` (contributes 0 to `total_amount` by design), alongside `category_id=2` (correctly priced) — real seed data, not a documentation error.

⚠️ **`ContractVisitLedger` was removed** — a contract's remaining visits (`GET /company/contracts/{id}` → `remaining_visits`, also `Contract::remainingVisits()`) is now a live count of that contract's still-active `Order` rows, not a ledger balance. `admin.contracts.approve` materializes the full visit quota as real orders via `ContractService::generateVisitOrders()`: **one order per visit slot per `branch_services` row**, not one per `visits_allowed`. For contract 1 that's `visits_allowed_per_row (12) × 4 rows = 48` orders — more than the contract-level `visits_allowed` (24) suggests, since `visits_allowed` isn't divided across the branch/category rows. This is real current seeded behavior (confirmed via `Contract::find(1)->remainingVisits()` → 48), not a doc error; flagging it here in case it's actually a bug in `generateVisitOrders()` worth raising separately from documentation.

Each generated order carries `order_source: contract_visit`, `customer_id` = the contract's `company_id`, `contract_id` set, `booking_type: scheduled`, `scheduled_date: null`, and zero pricing (contract-covered visits aren't charged per-order — the contract itself bills via Invoice). They then flow through the normal Order Status Machine like any other order — some of contract 1's 48 have already been advanced to `accepted`/`start`/`on_the_way`/`finish` by the rest of the order-related seeders (counts below).

⚠️ Neither contract is `pending_renewal` — `admin.contracts.renewal` has no valid live target; call `company.contracts.renew` first to create one.

⚠️ Contract 2 (the only `pending_signature` one) belongs to **alamal.facilities (company 9)**, not gulf.maintenance (company 10) — the company used throughout the rest of these docs. To demo `company.contracts.sign`, either log in as alamal.facilities or create a fresh contract needing a signature first.

**Company-owned orders** — two separate populations now: `order_source: company` (regular job-order checkout, 2 per company from `OrderSeeder`, random status) and `order_source: contract_visit` (only for companies with an **active** contract — currently just gulf.maintenance/10, 48 rows, see above). As of the 2026-08-22 refresh:

| Company | order_source: company | order_source: contract_visit |
|---|---|---|
| gulf.maintenance (10) | `pending` (`071fe6b4-50bc-481a-bf7f-b4423f791c23`), `on_the_way` (`f449327a-0718-4d12-bf07-de3cada8bca1`) | 48 rows spanning `pending`/`accepted`/`start`/`on_the_way`/`finish` — e.g. `finish`: `1e166935-b19c-4062-b009-e3ed96612d01` |
| alamal.facilities (9) | `pending` (`aa9e9fa6-1b90-4089-a9d6-fe6eb68f0b2a`), `on_the_way` (`8eced27f-09b9-4c73-9e74-8f06a47ca81d`) | *(none — contract 2 isn't approved yet)* |

⚠️ To demo `company.orders.confirm-completion`, use one of gulf.maintenance's `finish`-status **contract_visit** orders (e.g. the uuid above) — alamal has no `finish`-status order of either kind this run, and gulf's own `order_source: company` orders aren't at `finish` either. Re-run the query in the note at the top of this file after any reseed, since `OrderSeeder`/status-history seeding are non-deterministic.

⚠️ **Invoices: zero rows exist for any company** — `company.invoices.index` returns an empty list; no generation job has run against this seed. See [Shared / Related Entities](#shared--related-entities).

⚠️ **No Employees/Users, Technicians, or Wallet endpoints exist for the Company role** — flagged explicitly in `04-company.md` at the relevant section headers. Employee/contact management is admin-only (`admin.branches.contacts`); technician assignment is admin-only (`admin.orders.assign-technician`); billing runs through Contracts + Invoices instead of a wallet.

## Admin

| id | status | email | notes |
|---|---|---|---|
| 1 | active | admin@info.com | super_admin — **the seeded admin login used throughout `05-admin.md` and `01-guest.md` §04** (`database/seeders/UserSeeder.php`) |
| 2 | active | admin2@info.com | plain `admin` (not `super_admin`) — same password, not used elsewhere in these docs |
| 8 | active | admin@maintenance.sa | a separate super_admin from `database/seeders/AdminSeeder.php` (printed to the console on every seed run) — works identically to `admin@info.com` but isn't the one this documentation standardizes on |

Password for all: **`password`**.

Admin can see/manage everything above plus these admin-only entities:

**RBAC:**

| Roles (`roles.id`) | 1=super_admin, 2=admin, 3=support, 4=finance, 5=dispatcher |
|---|---|
| Permissions | 96 rows exist (ids 1–96); `permission_id_sample` used throughout = 4 (`admin.countries.store` → resolves to `countries.create`) |

Per [AUTH.md](AUTH.md), `RolePermissionSeeder`/`UserRoleSeeder` are empty stubs — none of these 96 permissions are actually attached to any role, and RBAC is bypassed entirely in `local`/`testing` anyway.

**Admin-reviewed content:**

Contact messages: 3 rows (ids 1–3, e.g. "Ahmed Hassan" / ahmed@example.com — real, both admin and the person that submitted it are trackable). Conversations: **only 1 row** (id 1, "Question about my order"), and it belongs to **user 20 (QA client)**, not individual@info.com. FAQs: 5 rows (ids 1–5). Static pages: 5 rows (ids 1–5). Sliders: 3 rows (ids 1–3). Feature flags: 5 rows — `enable_notifications`, `enable_wallet`, `enable_cod` enabled; `enable_reviews`, `maintenance_mode` disabled.

⚠️ **Audit Logs: zero rows** — `admin.audit-logs.index` returns an empty list.

⚠️ **Wallets and Withdraw Requests: zero rows anywhere** — every `admin.wallets.*` action (show/credit/lock/unlock/zero-out/block-refund/transactions) and `admin.withdraw-requests.{approve,reject}` has no valid `:id` to target. See [Shared / Related Entities](#shared--related-entities) for the full explanation (there is no create-wallet endpoint in the API at all).

⚠️ **Invoices: zero rows** — `admin.invoices.index` returns an empty list; `.show`/`.cancel` have no valid `:id`.

## Shared / Related Entities

Reference/catalog data that Client, Technician, Company, and Admin all read the same rows of.

### Full Users Table

20 rows total: 2 super_admin, 1 admin, 3 individual, 8 company, 6 technician. Every row relevant to any doc example is listed in its own role section above; see `database/seeders/UserSeeder.php` for the remaining 3 (a second super_admin, a second admin, one more individual).

### Catalog (Categories)

5 top-level (parent, unbookable, `price: null`) categories, ids **1, 6, 11, 16, 20** (Plumbing, Electrical, AC & Cooling, Appliance Repair, Painting & Carpentry). 18 bookable child categories, ids 2–5, 7–10, 12–15, 17–19, 21–23. All `contract_price` values are `null` (falls back to `price`). Two used throughout the docs:

| id | name | parent | price |
|---|---|---|---|
| 2 | Pipe Leak Repair | 1 | 150.00 |
| 7 | Outlet & Switch Repair | 6 | 120.00 |

Max category id is **23** — the base Postman-generator's own heuristic guess for some nested fields (`branches.*.service_ids.*`, plain `category_id` on technician registration) produced **out-of-range ids (32, 41)** that don't exist; this documentation pass corrected those. See [Corrections Made](#corrections-made-to-pre-existing-generated-examples).

### Geo Hierarchy

| Table | Real ids | Notes |
|---|---|---|
| Countries | 1 = Egypt, 2 = Saudi Arabia | only 2 rows |
| Governorates | 1 = Cairo, 2 = Alexandria, 3 = Giza, 4 = Ismailia, 5 = Assiut | all `country_id=1` (Egypt) — **no Saudi governorate is seeded**, despite most companies having Saudi phone numbers |
| Regions | 1 = Nasr City (gov 1), 2 = Maadi (gov 1), 3 = Heliopolis (gov 1), 4 = Dokki (gov 3), 5 = 6th of October (gov 3) | 5 rows total |
| Shipping Zones | 1 = Cairo Zone, 2 = Delta Zone, 3 = Upper Egypt Zone, 4 = Remote Zone | all `country_id=1` |

### Media — only 4 rows

All 4 are `CompanyProfile` commercial-register PDFs, for companies 9–12 (id 2 = gulf.maintenance's, used as `signed_media_id` in contract examples). None are suitable as a stand-in for an avatar or a technician registration document (`personal_photo`, `national_id_front/back`, `cv_file`) — those still need a real upload performed first; no seeded id is semantically appropriate to substitute.

### Full Orders Table — 68 rows, by status (as of the 2026-08-22 refresh)

`contract_visit` orders — 48 of the 68 — didn't exist before `ContractVisitLedger` was removed; see [Company](#company) for how they're generated. Counts below will differ after any reseed (`OrderSeeder` and order-status-history seeding are non-deterministic — see the note at the top of this file); re-run the query in [How to Refresh This File](#how-to-refresh-this-file) rather than trusting these numbers long-term.

| status | count (contract_visit / company / individual) | total | example uuid |
|---|---|---|---|
| pending | 25 / 7 / 2 | 34 | `0dccf2e6-28ae-474e-905e-2416df25ed18` (contract_visit) |
| accepted | 1 / 1 / 2 | 4 | `bab9f22d-53d9-4076-b968-6aad178581a0` (individual, customer 3) |
| on_the_way | 7 / 4 / 0 | 11 | `f449327a-0718-4d12-bf07-de3cada8bca1` (company, gulf.maintenance) |
| start | 8 / 2 / 0 | 10 | — |
| finish | 7 / 2 / 0 | 9 | `1e166935-b19c-4062-b009-e3ed96612d01` (contract_visit, gulf.maintenance) |
| **confirmed** | **0** | **0** | — no order has been through `confirm-completion` this run |
| **waiting_price** | **0** | **0** | — |
| **priced** | **0** | **0** | — |
| **cancelled** | **0** | **0** | — |

`order_source` in the live data now takes the literal values `individual`, `company`, and `contract_visit` (not the `job_order` string the job-order checkout code path itself writes — see the note in [WORKFLOWS.md](WORKFLOWS.md) and the `OrderSource` enum row in [README.md](README.md)); this is pre-existing/generated seed data, not a contradiction to resolve.

### Wallets, Withdraw Requests, Invoices, Audit Logs — the cross-role gaps

These four tables are **confirmed completely empty** and referenced from every role section above, because the same root cause blocks all of Client/Technician/Company/Admin equally:

| Table | Consequence |
|---|---|
| **Wallets (0 rows)** | No user has a `Wallet` row, and **there is no create-wallet endpoint anywhere in the API** — checked `WalletQueryService`, admin `WalletService`, the `User` model, and every seeder/observer. `client.wallet.balance`/`technician.wallet.balance` correctly return `{balance:0, currency:'SAR', is_locked:false}` with **no `can_withdraw` key at all** for a walletless user (real, current behavior — not a bug). Wallet-funded checkout will 422 (`insufficient_balance`) for every single seeded user. |
| **Withdraw Requests (0 rows)** | Follows directly from the empty wallets table. **The wallet-withdrawal workflow cannot be demonstrated end-to-end through the API at all** — not even by combining requests — until a wallet is seeded directly in the database. |
| **Invoices (0 rows)** | System-generated monthly installments with no store endpoint; no generation job has run against this seed. |
| **Audit Logs (0 rows)** | No write path has gone through whatever triggers an audit-log entry yet in this seed. |
| **Ratings (0 rows)** | Only written by `client.orders.confirm-completion` when `stars` is sent — and no order in this seed has been confirmed yet (see the Full Orders Table above: `confirmed` is 0). `ratings.technician_id` used to be constrained to `users.id` while every reader/writer treated it as `technicians.id`, so the first real call would have thrown a `500` FK-violation — fixed 2026-08-20 by re-pointing the constraint at `technicians.id` (see the callout in `02-client.md` §04 Confirm Completion Order); unrelated to why the table is currently empty. |

### Corrections Made to Pre-Existing Generated Examples

The base collection (`php artisan postman:generate`) guesses request-body values via reflection + simple heuristics, and got a few genuinely wrong before this pass — these were real bugs in the example data, not stylistic preferences, since they'd fail real `exists:` validation:

- `admin.contracts.store` example body had `company_id: 21` (only 20 users exist), `branch_ids: [5]` (only 3 branches exist) — fixed to `company_id: 10`, `branch_ids: [1]` (a real branch actually owned by that company).
- `company.contracts.store` example body had `branches[].service_ids: [32]` (max category id is 23) — fixed to `[2]`.
- `technician.auth.register` example body had `category_id: 41` (max is 23) — fixed to `2`.

Every other numeric foreign-key-shaped field across all 325 request bodies was scanned against the real table bounds above; no further out-of-range values were found.

## How to Refresh This File

```bash
php artisan tinker --execute="
foreach (\App\Models\User::query()->select('id','name','email','user_type','status')->get() as \$u) {
    echo \"{\$u->id}\t{\$u->user_type}\t{\$u->status}\t{\$u->email}\" . PHP_EOL;
}
"
# Repeat for Technician, Address, Category, Branch, Contract, Order (group by status),
# Country/Governorate/Region/ShippingZone, Role/Permission, FAQ/StaticPage/Slider/FeatureFlag,
# ContactMessage, Conversation, Wallet, WithdrawRequest, Invoice, AuditLog — see the exact
# queries used to build this file in the session that produced it, or re-derive with `::count()`
# / `::max('id')` / `::first()` per model.
```
Then re-run the reorganization pass so `docs/postman-collection.json` and the 4 role files pick up the refreshed ids automatically instead of hand-editing 325 endpoints.
