<laravel-boost-guidelines>
=== foundation rules ===

# Laravel Boost Guidelines

The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.

## Foundational Context

This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.

- php - 8.5
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- laravel/wayfinder (WAYFINDER) - v0
- tightenco/ziggy (ZIGGY) - v2
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pail (PAIL) - v1
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- phpunit/phpunit (PHPUNIT) - v12
- @inertiajs/vue3 (INERTIA_VUE) - v3
- tailwindcss (TAILWINDCSS) - v4
- vue (VUE) - v3
- @laravel/vite-plugin-wayfinder (WAYFINDER_VITE) - v0
- eslint (ESLINT) - v9
- prettier (PRETTIER) - v3

## Skills Activation

This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.

## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.

## Verification Scripts

- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.

## Application Structure & Architecture

- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.

## Frontend Bundling

- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.

## Documentation Files

- You must only create documentation files if explicitly requested by the user.

## Replies

- Be concise in your explanations - focus on what's important rather than explaining obvious details.

<!-- house-rules:start -->
<!--
    HOUSE RULES — portable. Copy this block into any Laravel project unchanged.

    Everything below is true of any project built this way. Nothing in it names a table, a screen,
    a database or a domain: those live in the PROJECT block that follows. If you find yourself
    wanting to add a project fact here, it belongs there instead.
-->

# House rules

Hand-written, and not generated by anything. They are here because each one was learned by getting
it wrong, and the reason is kept with the rule — a rule with no reason behind it is the first thing
anyone talks themselves out of.

## How these two files work

**`CLAUDE.md` and `AGENTS.md` are the same file, byte for byte.** Different agents read different
names, and an agent reading one must not be working from a shorter set of rules than an agent
reading the other. They have drifted apart before: one stayed at the generated template while the
other grew hundreds of lines of hand-written rules that the first agent never saw.

**Edit `CLAUDE.md`, then `cp CLAUDE.md AGENTS.md`.**

**⚠ `php artisan boost:update` rewrites this file and discards everything hand-written in it.** Not
some of it — all of it. It has happened more than once: it runs as a Composer script, so it fires on
`composer require` and `composer remove` as well as when invoked directly.

After any of those:

```bash
git diff --numstat CLAUDE.md      # a large deletion count means it happened
git checkout HEAD -- CLAUDE.md    # restore, then re-apply anything you actually wanted
```

The same applies to `boost.json`. **This is why the hand-written content sits in two fenced blocks:**
recovering from a rewrite is pasting two blocks back, not reconstructing rules out of a diff.

**Put the application's description in `config/boost.php`, not in prose here.** The generator reads
`config('boost.purpose')` and emits it into Foundational Context itself, so a description held there
is the one piece of project context that survives a regeneration. Publish the config
(`php artisan vendor:publish --tag=boost-config`) and add a `purpose` key; it ships without one.

## Reach for the shared component before writing one

The project block lists what exists. The rule is the same everywhere:

- **A list of records goes through the shared data-table component.** Those wrappers own searching,
  sorting, paging, selection, export and the phone card view, and a hand-rolled table gets none of
  it. If one genuinely does not fit — a two-column summary inside a dialog, a layout the wrapper
  cannot express — a plain `<table>` is fine, **but give it its own horizontal scroll container**,
  and wrap it in an element rather than setting `display: block` on the table, which strips its
  semantics from a screen reader.
- **A checkbox is a native `<input type="checkbox">`** carrying the project's own checkbox class
  from `app.css`, not a component wrapper. It stays a real form control, and the styling stays in
  one place.
- **Any list a user picks from goes through the searchable select.** Do not introduce a second
  select library or a bare `<select>`. A plain library `<Select>` is acceptable only for a short,
  fixed, self-evident set — a status, a yes/no — where there is nothing to search. It takes options
  as `{ id, label, sublabel? }` and emits the `id`, never the option object; `sublabel` is how you
  show a second line (an email under a name), so a custom option slot is never needed.
- **Phone, telephone, mobile and contact numbers go through the phone input.** It carries the
  country picker and dial code and stores one normalised value, so numbers are consistent wherever
  they are entered. A bare text input with a `+256 …` placeholder is not a substitute — the
  placeholder is a hint, the component is a guarantee.
- **Charts and download controls each have one component too.** Two implementations of the same
  control will not stay in step, and the second is always the one that misses the accessibility
  work.

## One design system, one file-handling library

**Every interface aligns to the project's design system reference**, so the product reads as one
thing rather than as the sequence of sprints that built it. The design tokens live in the
stylesheet; take colours, spacing and type from there rather than writing a hex value into a
component. The project block names the reference.

**Documents and images go through one media library** — upload, storage, conversion and download —
rather than each feature inventing its own path handling. Its schema can be extended where a
feature needs extra columns. Two file-handling paths in one application means two sets of rules
about what is public, and one of them will be wrong.

## Controllers stay lean

**Validation belongs in a Form Request. Business logic belongs in a service.** A controller
resolves what it was given, delegates, and responds.

The reason is not tidiness: validation inline in a controller cannot be reused by a console command,
an import or a queued job, so the second caller re-implements it slightly differently and the two
drift. A service is also the only place a multi-step write can be wrapped in a transaction and
tested without a HTTP request.

**⚠ Check the project block for how far this has actually got.** On an inherited codebase this is
usually a rule for new work rather than a description of what is there, and it is worth saying which
so nobody assumes the pattern is already established.

## Every control that causes a request must show that it is working

**No exceptions, in existing code or new.** A button that looks idle while a request is in flight
gets clicked again, and a double-submitted approval or a duplicated payment is a real cost.

- **The button component takes a `loading` prop.** It disables the button, so the action cannot
  fire twice, and swaps the icon for a spinner while keeping the label so the layout does not jump.
  **`loading` implies `disabled`** — never write both for the same condition.

  ```vue
  <!-- right -->
  <Button :loading="form.processing" @click="save">Save</Button>

  <!-- right — a separate validity condition stays on disabled -->
  <Button :loading="form.processing" :disabled="!form.body.trim()" @click="post">Post</Button>

  <!-- wrong — disabled alone gives no feedback, it just goes dead -->
  <Button :disabled="form.processing" @click="save">Save</Button>
  ```

- **A form helper** exposes its own in-flight flag; bind that form's flag, and do not share one
  form across unrelated buttons to avoid having to.
- **Router calls without a form** need a keyed busy-action helper, so only the button that was
  pressed spins and a second click while one is in flight is ignored.
- **Non-Inertia promises** — fetch, export, file read — get tracked the same way.
- **⚠ File uploads are the easiest to forget and the slowest to finish.** The trigger must spin from
  the moment the file is chosen until the response lands.
- **A plain `<button>` gets no spinner.** Use the button component, or add a spinner and `disabled`
  by hand.
- **Confirmed actions spin after confirmation**, not while the dialog is open.
- Navigation-only calls are already covered by the global progress bar; a spinner there is optional.

## Confirming an action

**Anything with a consequence asks first** — deleting, submitting something that cannot be
withdrawn, sending something to somebody, changing who can see what.

- **Destructive actions require the word `DELETE` typed**, with the submit button disabled until it
  matches. A confirm dialog you can dismiss with a stray tap is not a confirmation for something
  that cannot be undone. Use one word across the whole project so it is a habit rather than a puzzle.
- **From a page** — a header button, a table row action, a toolbar — a SweetAlert is right, and is
  the majority of uses.
- **⚠ Never open a SweetAlert while a `Dialog`, `AlertDialog` or `Sheet` is open. It freezes the
  whole screen.** The dialog holds a focus trap and marks everything outside itself inert;
  SweetAlert renders at `document.body`, which is outside it. Focus can never reach the confirm
  button and neither layer will close.
- **When a dialog's own action needs confirming, confirm inside that dialog.** Either a two-step
  footer, where the button becomes `Confirm …` and Cancel becomes Back, or an inline typed
  confirmation. Do not stack a second dialog either — keeping it in the open one reads better
  anyway, because the figures being confirmed stay on screen while the user decides.

## Notifications and the queue

Decide by **channel**, not by habit:

- **Sends mail** → `implements ShouldQueue`, plus `use Queueable, SerializesModels;`. An SMTP round
  trip is what makes a user wait on submit. `SerializesModels` keeps the job payload to an
  identifier and re-fetches in the worker rather than freezing a whole model graph into the queue row.
- **In-app only** (`via()` is just `['database']`) → leave it synchronous. A single insert costs
  nothing inline, and queueing it only adds a way for the notification bell to stay empty when the
  worker is down.
- **Mixed** → queue it, and pin the database channel back to `sync` so the bell still updates inside
  the request:

  ```php
  public function viaConnections(): array
  {
      return ['database' => 'sync'];
  }
  ```

**⚠ `afterCommit` is required, not optional.** Senders routinely raise notifications inside
`DB::transaction`; without it the worker can pick the job up before the commit lands and find no
row. `Queueable` already declares `$afterCommit`, so redeclaring the property is a fatal error —
call the trait's setter in the constructor instead:

```php
public function __construct(private Thing $thing)
{
    $this->afterCommit();
}
```

**⚠ `use Queueable;` on its own does nothing.** It ships in the `make:notification` stub and only
takes effect alongside `implements ShouldQueue`. Do not read its presence as a sign a notification
is queued.

**Not everything belongs on the queue.** Where a recipient row must be stamped `sent` when the mail
actually left — rather than when a job was dispatched — send it synchronously and queue the *batch*
instead, re-queueing while recipients remain. An immediate send then starts at once and a scheduled
fallback means a stopped worker delays a send rather than losing it.

Run tests with the queue driver set to `sync` so queued work executes inline. In development,
`queue:listen` picks up code changes; `queue:work` caches code and must be restarted after editing
a notification.

## Public and legal pages

- **A legal document has exactly one source.** The terms, privacy policy, cookie policy and DPA each
  live in one component, and both the on-screen and printable pages render it. Held as separate
  copies, the print versions silently fall behind — printing the terms then produces a contract with
  a different effective date, and a clause from another product. Keep a test on it.
- **New legal pages reuse the shared page chrome** rather than copying it; that copying is how the
  drift starts.
- **Every link in the public footer must resolve.** A legal link that 404s is worse than no link —
  it reads as a document withdrawn.
- **Nothing internal is linked from the unauthenticated header or footer.** A design-system
  reference is a working document, not a public page.
- These pages are read by people evaluating the product, so **wording copied in from a sibling
  project is a real defect, not a typo**.

## Accessibility: build it in, do not audit it later

**Every new page and component carries its own accessibility, at the time it is written.** It is a
requirement, not a polish pass, and retrofitting it across a finished application is several times
the work of writing it in.

**Assume anything you did not write yourself is missing it.** The state this was written against was
about thirty screen-reader-only labels in an entire front end — thirty in total, not per page — with
no skip link and no focus-trap utility.

**The rules, in the order they are most often broken:**

- **An icon-only control needs a name.** A button whose only content is an icon is an unlabelled
  button to a screen reader. Add `<span class="sr-only">…</span>` or `aria-label`. A tooltip does
  not count — `title` is not reliably announced, and on a touch device it never fires at all.
- **A figure that means something by its colour must also say it in words.** Roughly one man in
  twelve has a colour vision deficiency. If the project has a component that renders such a figure
  with its label, everything showing that figure uses it.
- **State changes that happen without a page load need announcing.** A toast, a filter that empties
  a table, a figure that arrives on a partial reload: wrap the region in `aria-live="polite"`, or
  the change is silent.
- **Anything that opens over the page traps focus and returns it.** Use the library's `Dialog`,
  `AlertDialog` or `Sheet` rather than building a panel by hand.
- **Every form control has a `<Label for=…>`**, not a paragraph that looks like one. Use the
  project's error component rather than colouring the border red.
- **Tables get real `<th>` with `scope`**, and a `<caption>` or `aria-label` saying what the table
  is. A grid of `<div>`s is not a table to anybody using one.
- **Never remove a focus ring.** If it looks wrong, restyle it — `outline: none` with nothing in its
  place makes the keyboard path invisible.
- **Interactive means `<button>` or `<a>`.** A `<div @click>` is not reachable by keyboard and is
  announced as nothing.

**If the project ships a user manual, editing a screen means editing its chapter.** Keep a test that
holds the manual against the application — the routes it prints must answer, the menu entries it
names must exist — because nothing else ties a chapter to the screen it describes, and documentation
drifts silently by nature.

## Every screen is a phone screen too

**Every rule here fails silently on a desktop and breaks the screen on a phone.** That asymmetry is
why they are written down, and why they are worth a test that reads source.

**Target 360px and up.** Check 360, 390 and 768 in device mode — 768 because that is where a
`md:`-style breakpoint and any JavaScript media query meet, and they must not both be true at once.

- **A dialog sets its width from `sm:` upward, never unprefixed and never with `!`.** The base
  carries a mobile cap (`w-full max-w-[calc(100%-2rem)] … sm:max-w-lg`), and because the class merge
  is `tailwind-merge`, a call site's own `max-w-*` **replaces** it. Worse, an unprefixed width and
  the base `sm:` width are different merge keys, so the override never removes the `sm:` one and
  loses on source order — the width appears to do nothing on desktop, someone reaches for `!`, and
  now it is broken at both ends. `sm:max-w-4xl` gives the same desktop result and keeps the guard.
  Anything wider than `lg` needs an `md:`/`lg:` step too, or a 640px viewport gets a 1000px layout.
- **Heights use `dvh`, never `vh` and never `max-h-screen`.** `vh` on mobile is the URL-bar-hidden
  height. A full-height box centred with `translate-y-[-50%]` puts its own title above the screen,
  unreachable, because the scrolling happens inside the box.
- **A form control is `text-base md:text-sm`, never `text-sm` alone.** Safari zooms the whole page
  when a focused control is under 16px and does not zoom back. The base components do this already;
  the failure is a call site overriding it.
- **Touch sizing goes through `pointer-coarse:`, not a breakpoint.** A finger needs ~44px and a
  mouse does not, and `pointer-coarse` keys off the input device rather than the width — so touch
  gets a real target and no desktop screen moves a pixel. Set it once in the button variants.
  ⚠ It cannot be checked in device emulation, which emulates width and not pointer type.
- **A `flex-1` item needs `min-w-0`.** A flex item's default `min-width: auto` refuses to shrink
  below its content's intrinsic width, so one wide table widens the row past the viewport and the
  whole document scrolls sideways. ⚠ Before deleting a width that looks like debris on a page
  container, check it is not the only thing clamping that minimum.
- **A horizontally scrolling row is `justify-start`, never centred.** A centred flex container whose
  content overflows puts its first item at a negative offset, and `scrollLeft` cannot go negative —
  so the start becomes unreachable. Applies to tab strips, filter chips, any pill row. Give the row
  `max-w-full overflow-x-auto` and the items `shrink-0`.
- **An overlay needs a way out that works without a keyboard.** A phone has no Escape key, so a
  sheet that hides its close button *and* disables outside dismissal cannot be closed at all — and
  it looks fine on a desktop, where Escape still works. That is exactly how it ships.
- **Grids start at one column.** `sm:grid-cols-2` is right; a bare `grid-cols-3` is three columns on
  a phone. A tab strip given a column count starts at two.
- **`title=""` is invisible on touch.** If it carries information that appears nowhere else, it is
  not documented.

**Judgement matters more than the grep.** Distinguish unusable from merely tight: a two-column grid
of label/value pairs is snug and fine, a two-column grid of input fields is not. An audit that
reports everything is as useless as one that reports nothing.

## Guard tests that read source

When breaking a rule produces no symptom on the machine the developer is using and a broken screen
on the machine the user is using, **assert it by scanning source**. Most of the mobile and dialog
rules above are enforced this way.

Three things make the difference between a guard and a decoration:

- **Prove it fails against the unfixed code before believing it.** Stash the fix, run the test,
  confirm it fails, restore. A guard that passes for the wrong reason is worse than none.
- **Mutation-test the important ones.** Re-break one instance deliberately and confirm the guard
  fires and names the right file.
- **⚠ Parse elements, not lines.** A formatter wraps long attribute lists, so a tag and its `class`
  routinely sit on different lines. A line-based scan silently skips exactly the worst offenders and
  then reports a clean run.

**Say what they are.** They catch regressions; they did not catch the original bug and could not.
Nothing here drives a browser, so layout is verified by looking at it.

## Migrations are consolidated, not accumulated

**One migration per module, holding that module's tables in their settled form.** Every `ALTER`-style
migration is folded back into the `Schema::create` it belongs to, and every table sits in the module
that owns it.

**So a new column goes into the create migration, not into a new file.** The schema is read as a
definition rather than as a history; the history is in git, where it belongs.

**Cross-cutting migrations stay separate and must run last**, because they run against every table:
the CHECK constraints in one registry, any triggers, row-level security policies, and every foreign
key after every table exists. Name them so they sort last.

**⚠ ORDER MATTERS AND IS EASY TO GET WRONG.** A create migration must sort *before* the constraint
registry, or its CHECK constraints are silently skipped — the registry walks its list and passes over
any table that does not exist yet, so the constraint never appears and nothing fails. A consolidation
once dropped seven CHECK constraints exactly that way, and the only thing that caught it was the
schema fingerprint.

**A schema fingerprint is how you prove a schema change was the one you meant.** Dump every column,
comment, constraint and index in a stable order; run it before a structural refactor, run
`migrate:fresh` on a scratch database afterwards, and diff. An empty diff is the only evidence worth
having — folding migrations by hand is exactly where a column quietly becomes nullable, a default
moves, or an index is not recreated, and none of those break a test. They break a deployment months
later on a table nobody was looking at.

**Comments are part of the schema, not documentation about it.** If generated documents read table
and column comments, a fold that drops one silently empties a row of a deliverable. Note that
`->change()` DROPS a column comment on PostgreSQL.

## Database conventions

Write database-agnostic queries and portable schemas. Treat the primary database as the source of
truth and make the other paths explicit.

- Default to the query builder and Eloquent so the driver handles dialect differences.
- Prefer portable column types (`string`, `text`, `json`, `timestamp`); avoid database-specific
  types unless guarded by a driver check.
- Where a query genuinely differs — JSON operators, full-text search, `ILIKE` vs `LIKE`, date
  functions, `RETURNING` — branch on `DB::connection()->getDriverName()`.
- Performance-critical paths may use database-specific features where the portable builder is too
  slow, but both drivers must be handled explicitly.

**⚠ MariaDB reports itself as `'mysql'`, and it is not MySQL.** So `getDriverName() === 'mysql'` is
necessary and *not sufficient* wherever the two diverge. Tell them apart by the server banner:

```php
str_contains(strtolower(DB::selectOne('SELECT VERSION() AS v')->v), 'mariadb')
```

The known divergences, all found the hard way:

- **Dropping a CHECK:** MySQL wants `DROP CHECK <name>` (8.0.16+) and has **no `IF EXISTS`** for it
  at any version. MariaDB wants `DROP CONSTRAINT IF EXISTS <name>` (10.2+) and does not accept
  `DROP CHECK`. `DROP CHECK IF EXISTS` is valid on neither — and a project emitted exactly that for
  months, because it only ever ran on PostgreSQL so nothing caught it.
- **Dropping a foreign key:** MySQL wants `DROP FOREIGN KEY <name>`, again with no `IF EXISTS`.
  MariaDB accepts `DROP FOREIGN KEY IF EXISTS` (10.1+).
- Where the engine will not accept `IF EXISTS`, ask `information_schema.TABLE_CONSTRAINTS` first
  rather than dropping blind.

**Run a fresh `migrate` against every engine you claim to support before believing a migration is
portable.** A suite that runs on SQLite and the primary database touches neither MySQL nor MariaDB,
so a migration can be green in CI and fail on the first deploy. Keep a way to run the whole suite
against each real server, and put anything driver-sensitive in its own test directory — those tests
must **not** branch on the driver, because the point is that the engines agree.

**⚠ The divergences that bite, all found the hard way:**

- **Searching:** use `whereLike()` / `orWhereLike()`, never `where(..., 'like', ...)`. MySQL's
  `utf8mb4_unicode_ci` makes `LIKE` case-insensitive and PostgreSQL's is case-sensitive, so a raw
  `like` silently behaves differently. Laravel compiles `whereLike()` to `ILIKE` on PostgreSQL.
- **NULL ordering:** `ORDER BY col ASC` puts NULLs first on MySQL and last on PostgreSQL. Order
  explicitly — `(col IS NULL) DESC, col ASC`. `NULLS FIRST` is PostgreSQL-only and forces a fork.
- **`CASE` ladders always need an `ELSE`.** Without one an unmatched row sorts as NULL, and the two
  drivers disagree about where that goes.
- **Never aggregate a boolean.** `SUM(bool)` works on MySQL and raises on PostgreSQL. Use
  `SUM(CASE WHEN cond THEN 1 ELSE 0 END)`.
- **Division:** `COUNT(*) / 7` is integer division on PostgreSQL and decimal on MySQL. Divide in
  PHP, or divide by a float.
- **Elapsed time:** measure seconds and divide. `TIMESTAMPDIFF(HOUR, …)` truncates where
  `EXTRACT(EPOCH …)/3600` does not, and MySQL's `DATEDIFF` counts date boundaries where
  PostgreSQL's `DATE_PART` counts elapsed interval.
- **Week numbers:** PostgreSQL's `EXTRACT(WEEK)` is ISO-8601; MySQL's `WEEK()` needs mode 3 to match.
- **`TO_CHAR` pads:** use `FM` (`FMMon`) when the output is compared or grouped.
- **Aliases in `HAVING`:** PostgreSQL allows a select alias in `GROUP BY` and `ORDER BY` but not
  `HAVING`. Repeat the aggregate there.
- **`->first()` needs an `orderBy`** when the query can match more than one row; the engines do not
  agree on which row comes back otherwise.
- **New columns:** no new `->enum(` (use `->string()` plus `Rule::in()`), no new `->json(` (use
  `->jsonb()`), and no `->after(`, which PostgreSQL ignores. Worth a test that fails the build if
  those counts rise.
- **Identifier length:** truncated at 63 characters by PostgreSQL and 64 by MySQL. Name long
  constraints explicitly rather than letting a generated name grow past the limit.

## Running the test suite

- **A green run on SQLite has not tested what the database enforces.** SQLite installs no CHECK
  constraints, no triggers and no row-level security, so the tests that prove those guarantees skip
  — and, worse, tests that *do not* skip can pass there for the wrong reason. Two live 500s once sat
  green on SQLite for months, both writing a string a CHECK constraint did not permit. **Any change
  that writes a constrained string column — a status, a channel, a type, a scope — must be exercised
  on the real database.**
- **Point the suite at a separate database.** `RefreshDatabase` runs `migrate:fresh` and will
  otherwise destroy development data. Override the connection on the command line rather than
  editing `phpunit.xml`.
- **Never run two suites against the same test database at once**, and never edit files while a run
  is in progress — the result describes a tree that no longer exists.
- **Prove a new test fails against the old code before believing it.**
- **`php artisan test` can die partway through, print no summary, and still exit 0** — so a CI step
  checking the exit code sees a pass for a run that never finished. It is a memory ceiling, not a
  broken test, and it returns as the suite grows. Use `php -d memory_limit=2G vendor/bin/phpunit`
  for the whole suite and keep `php artisan test --compact --filter=…` for single files.
- **A front end with a build step needs building before the suite runs on a fresh clone**, or every
  test that renders a full page fails with a manifest error that reads like an application bug.

## Docker: running and deploying

The patterns worth keeping whatever the project:

- **Development and production are separate down to the compose project name**, so nothing in one
  can reach a container, volume or database belonging to the other.
- **The production image is the artefact.** Dependencies, built assets and source are layers, not
  mounts, so what runs is what was built and a deployment is a new container rather than a file
  copy. Opcache with `validate_timestamps = 0` depends on that being true.
- **There is no dev-server service in production**, so the browser can never be told to fetch assets
  from a machine that is not there. Removing the service is the only way to make that impossible.
- **Guard the production build on the settings that fail silently at run time** — an empty app key,
  an empty database password, a non-production environment (which may seed demo data with published
  logins), and debug left on. Refuse to build rather than discover it later.
- **`optimize` runs at container start, not at build**, because config caching freezes what `env()`
  returns and those values arrive at run time. One consequence: `env()` outside `config/` returns
  null once cached, so anything reading `env()` directly breaks under a cached config.
- **A backup takes both halves** — the database and the file storage. The database knows which file
  belongs to which record; the volume holds the file. Either alone restores to rows pointing at
  documents that do not exist.
- **Environment files are read twice**, by the framework and by compose, so the database name, user
  and password provision the container *and* configure the connection to it. They cannot drift, and
  changing them only takes effect on a volume that does not exist yet.

<!-- house-rules:end -->

<!-- project:start -->
<!--
    THIS PROJECT — the only block that changes between projects.

    Sections marked TODO were not detectable from the codebase and need a human.
    Everything else here was read from the repository and was true when written.
-->

# This project

## What it is

Tradixor is a cloud-based, multi-tenant SaaS platform built for importers, distributors, and retailers who manage goods across international and local supply chains. The platform unifies the complete product lifecycle — from sourcing trips abroad, through international shipping and customs clearance, local purchase and production,  into warehoused inventory, and finally to point-of-sale transactions with integrated finance and accounting. The platform is domain-agnostic by design. TradiXor provides automation tools and real-time insights that empower organizations to make smarter, data-driven decisions.

## Module folders

Detected under `app/Models`. The same names should run through `app/Http/Controllers`,
`app/Services`, `app/Policies`, `resources/js/pages` and `tests/Feature`.

| Folder | Holds |
| --- | --- |
| `Api` | <!-- TODO --> |
| `Concerns` | <!-- TODO --> |
| `Finance` | <!-- TODO --> |
| `Hr` | <!-- TODO --> |
| `Inventory` | <!-- TODO --> |
| `LocalProcurement` | <!-- TODO --> |
| `Logistics` | <!-- TODO --> |
| `Procurement` | <!-- TODO --> |
| `Production` | <!-- TODO --> |
| `Sales` | <!-- TODO --> |
| `Security` | <!-- TODO --> |
| `System` | <!-- TODO --> |
| `Traits` | <!-- TODO --> |

## Shared components to reach for

| Need | Component |
| --- | --- |
| A table of records | `components/datatables/DataTable.vue`, `ServerPaginatedDataTable.vue` |
| A searchable dropdown | `components/ui/SearchableSelect.vue` |
| A phone or telephone field | `components/ui/phone-input/index.vue` |
| A chart | `components/charts/` — `BarChart`, `LineChart`, `PieChart`, `DoughnutChart` |
| A checkbox | native `<input type="checkbox">` with the checkbox class in `app.css` |
| A busy state for a button that sends a request without a form | `composables/useBusyAction.ts` — `busy.run(key, (visit) => router.put(…, visit))` with `:loading="busy.is(key)"`; `busy.track(key, …)` for axios/fetch |
| A table row action that sends a request | take `(row, visit)` and pass `visit` to the router call (`mergeVisit` when it has options of its own); the table spins that row's actions button |

## How far the lean-controller rule has actually got

**70 Form Request class(es) against 120 controllers, and 36 service class(es).** 
Treat the rule as guidance for new work until those numbers say otherwise.

## Running it locally and running the suite

Database `tradixorclean` on port 5432. **⚠ Point the suite at a
SEPARATE database** — `RefreshDatabase` runs `migrate:fresh` and will destroy this one.

55 test files. <!-- TODO: the measured test and assertion count, and the exact
     command for the real database. -->

## Tools installed but switched off

<!-- TODO: anything in package.json or composer.json that is NOT wired up. -->

---

<!-- Sections carried over from this project's previous CLAUDE.md, unchanged. -->

<!-- project:end -->

=== boost rules ===

# Laravel Boost

## Artisan

- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
- To check environment variables, read the `.env` file directly.

## Tinker

- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
    - Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`

=== php rules ===

# PHP

- Always use curly braces for control structures, even for single-line bodies.
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`.
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
- Use array shape type definitions in PHPDoc blocks.

=== deployments rules ===

# Deployment

- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.

=== tests rules ===

# Test Enforcement

- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.

=== inertia-laravel/core rules ===

# Inertia

- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
- Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.

# Inertia v3

- Use all Inertia features from v1, v2, and v3. Check the documentation before making changes to ensure the correct approach.
- New v3 features: standalone HTTP requests (`useHttp` hook), optimistic updates with automatic rollback, layout props (`useLayoutProps` hook), instant visits, simplified SSR via `@inertiajs/vite` plugin, custom exception handling for error pages.
- Carried over from v2: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data.
- When using deferred props, add an empty state with a pulsing or animated skeleton.
- Axios has been removed. Use the built-in XHR client with interceptors, or install Axios separately if needed.
- `Inertia::lazy()` / `LazyProp` has been removed. Use `Inertia::optional()` instead.
- Prop types (`Inertia::optional()`, `Inertia::defer()`, `Inertia::merge()`) work inside nested arrays with dot-notation paths.
- SSR works automatically in Vite dev mode with `@inertiajs/vite` - no separate Node.js server needed during development.
- Event renames: `invalid` is now `httpException`, `exception` is now `networkError`.
- `router.cancel()` replaced by `router.cancelAll()`.
- The `future` configuration namespace has been removed - all v2 future options are now always enabled.

=== laravel/core rules ===

# Do Things the Laravel Way

- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.

### Model Creation

- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.

## APIs & Eloquent Resources

- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.

## URL Generation

- When generating links to other pages, prefer named routes and the `route()` function.

## Testing

- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.

## Vite Error

- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.

=== wayfinder/core rules ===

# Laravel Wayfinder

Use Wayfinder to generate TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes).

=== pint/core rules ===

# Laravel Pint Code Formatter

- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.

=== phpunit/core rules ===

# PHPUnit

- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test.
- If you see a test using "Pest", convert it to PHPUnit.
- Every time a test has been updated, run that singular test.
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
- Tests should cover all happy paths, failure paths, and edge cases.
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application.

## Running Tests

- Run the minimal number of tests, using an appropriate filter, before finalizing.
- To run all tests: `php artisan test --compact`.
- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`.
- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file).

=== inertia-vue/core rules ===

# Inertia + Vue

Vue components must have a single root element.

- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.

</laravel-boost-guidelines>
