# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project status

AMANSA-VOYAGE is a Symfony 7.4 (PHP ≥ 8.2) web app that digitizes a visa-application agency's workflow. The functional spec, written in French, is in `projet.md` (cleaned-up version of the original notes in `projet.txt`). Read it before designing entities, voters, or controllers. `note.txt` and `install.txt` are the developer's personal command notes.

Done so far:
- authentication and user management (login, hierarchy, blocking, temporary passwords, activity log);
- the full dossier circuit: dossiers, clients, embassies, deposit lots, payments, notifications, role dashboards, and PDF/Excel exports.

The UI and all user-facing text are in French. Amounts are in FCFA, stored as integers and displayed with the Twig filter `|fcfa`.

## Commands

```bash
composer install                                   # also runs cache:clear, assets:install, importmap:install
symfony serve --port=8088 --no-tls                 # dev server. Ports 8000/8010 are taken on this machine.
                                                   # `php -S ... -t public` returns 404 for /assets/*, use the Symfony CLI
php bin/console tailwind:build --watch             # rebuild CSS (standalone Tailwind v4 binary in var/tailwind, no npm)

php bin/console make:migration
php bin/console doctrine:migrations:migrate --no-interaction
php bin/console doctrine:fixtures:load -n          # AppFixtures (admin devyayaly@gmail.com) + DemoFixtures (team accounts, ~40 dossiers on every status), password Welcome123@
php bin/console doctrine:fixtures:load -n --group=app   # admin only, no demo data
                                                   # DemoFixtures is meant to be deleted before production. It wipes var/media/{dossiers,lots}.
php bin/console mailer:test you@example.com        # check the SMTP config

php bin/console tailwind:build --minify && php bin/console asset-map:compile   # production assets

php bin/phpunit                                    # all tests (functional tests rebuild the schema in SQLite var/data_test.db)
php bin/phpunit tests/Functional/DossierWorkflowTest.php   # circuit rules (kernel test); AuthenticationTest / DossierControllerTest = HTTP
php bin/phpunit --filter testMethodName
```

`phpunit.dist.xml` sets `failOnDeprecation`, `failOnNotice` and `failOnWarning`, so a deprecation fails the test run.

## Environment

- `.env` (committed) has only placeholders. Secrets live in `.env.local` (git-ignored): the MariaDB `DATABASE_URL` and the SMTP values.
- In the test env, `.env.local` is not loaded: tests use SQLite and the `null://null` mailer.
- SMTP is configured through `MAILER_HOST/PORT/USER/PASSWORD/FROM_EMAIL/FROM_NAME`. `config/packages/mailer.yaml` builds the DSN from them, with `urlencode` because the username contains an `@`. It also sets the global From header.
- Emails are sent **synchronously**: `SendEmailMessage` is not routed to Messenger, so no worker is needed.
- Platform media (dossier attachments, receipts, lot photos) must go through `App\Service\PrivateMediaStorage`. It stores files in `var/media` (`%app.private_media_dir%`), never in `public/`. Serve them with `createResponse()` from a controller that checks permissions.

## Architecture (auth & users)

- `App\Enum\UserRole` is the single source of truth for the hierarchy: `rank()`, `isAbove()`, `manageableRoles()`. Each user has exactly one business role stored in `roles` (`User::getMainRole()/setMainRole()`). `security.yaml` mirrors the hierarchy in `role_hierarchy`, so `is_granted('ROLE_AGENT')` is true for every role.
- `App\Security\Voter\UserVoter` (`USER_LIST`, `USER_CREATE`, `USER_VIEW/EDIT/BLOCK/RESET_PASSWORD`): a user may only act on strictly lower ranks, never on their own account. List queries are scoped with `UserRepository::createManageableQueryBuilder()` / `restrictToRoles()`, which match the roles JSON with `LIKE '%"ROLE_X"%'`.
- `App\Service\UserManager` is the only place that mutates accounts: create, update, block/unblock, resetPassword, changePassword. It handles hashing, the email (`UserMailer`), and logging (`ActivityLogger` → `ActivityLog` entity, `ActivityAction` enum). If the SMTP send fails, it returns a `TemporaryPasswordResult` with `emailSent=false`, and the controller shows the password to the superior in a flash.
- `AccountStatusSubscriber` (kernel.request):
  - logs out a user blocked mid-session (`active=false`);
  - redirects every route to `app_password_change` while `mustChangePassword` is true.
  - `UserChecker` refuses blocked accounts at login.
- `LoginActivitySubscriber` records login, failed login and logout.
- Session lifetime is 1 week: session cookie, plus `remember_me` with `always_remember_me` signed on `password` and `active`. Login throttling is 5 attempts per 15 min.
- `User::__serialize()` puts only id, email, roles, the CRC32C of the password and `active` into the session, with no relations.
- Templates: `layout/app.html.twig` is the sidebar layout for authenticated pages (blocks `page_title`, `page_actions`, `content`). `layout/auth.html.twig` is the centered card used by login and the forced password change.
- Form theme: `form/theme.html.twig`.
- Reusable Tailwind classes (`btn`, `btn-primary`, `card`, `badge`, `form-input`…) and the brand colors `brand-*` (blue) and `accent-*` (orange) are defined in `assets/styles/app.css`.
- Forms that change state are POST with a per-user CSRF token (`#[IsCsrfTokenValid]`) and `data-turbo-confirm`.

## Architecture (business)

- **Dossier status**
  - The circuit is defined in `App\Enum\DossierStatus`: `EN_ATTENTE_AFFECTATION → AFFECTE → SOUMIS_EN_LIGNE → VALIDATION_RECUE → EN_LOT → DEPOSE → AVIS_FAVORABLE → RETIRE → RECUPERE_AGENT → RESTITUE`.
  - `REJETE` goes back to `SOUMIS_EN_LIGNE`. That resubmission clears the batch and the decision and increments `submissionCount`.
  - `ANNULE` can be reached from any open status.
- **Who changes the status**
  - Only `App\Service\DossierWorkflow` changes it, along with `App\Service\BatchManager` for lot operations (add to lot, deposit, pickup with a decision per dossier).
  - Every change writes a `DossierEvent` (the dossier history), creates `Notification`s through `Notifier`, and flushes.
  - Never call `Dossier::setStatus()` from a controller. Errors are thrown as `WorkflowException`, whose message is shown in a flash.
- **Role rules** live in `DossierWorkflow::can()`, which is also used by `DossierVoter` and to build the actions shown on the dossier page:
  - "agent affecté or supervisor (dispatcher+)" for most steps;
  - payment: the assigned agent, or `isFinance` (comptable/admin);
  - restitution is refused while there is a balance, except a forced close by comptable/admin with a reason;
  - a dossier created by an agent stays unassigned: only a supervisor assigns.
- **Visibility**
  - Agents only see dossiers assigned to them: `DossierRepository::createVisibleQueryBuilder()` and `DossierVoter::VIEW`.
  - `BusinessVoter` controls access to the modules: `BATCH_MANAGE` (dispatcher+) for lots and clients, `FINANCE` (comptable/admin) for payments, `EMBASSY_MANAGE` (admin).
- **Clients** are identified by their normalized passport number (`Client::normalizePassport`).
  - On dossier creation, `DossierType` (PRE_SUBMIT) reattaches the dossier to the existing client.
  - On the client side, the Stimulus controller `passport_lookup_controller.js` calls `/clients/recherche`.
- **Files**
  - Dossier attachments go through `DossierDocumentManager`, lot attachments through `BatchManager`.
  - Both are stored with `PrivateMediaStorage` under `var/media/{dossiers,lots}/{id}` (`var/media_test` in the test env) and are only served by `MediaController` after a voter check.
- **References** (`AV-2026-00001`, `LOT-2026-001`, `REC-2026-00001`) come from `ReferenceGenerator`. It holds a lock per prefix and releases it on Doctrine `postFlush` and in `reset()`, so always flush after generating one.
- **Exports**
  - `PdfGenerator` (dompdf, templates in `templates/pdf/`, logo embedded in base64) produces the deposit slip and the payment receipt.
  - `DossierExporter` (PhpSpreadsheet) produces the Excel export, using the same `DossierFilter` as the list.

## Frontend

The frontend is AssetMapper + importmap (`importmap.php`, `assets/`), with Stimulus (`assets/controllers/`) and Turbo, and Tailwind through `symfonycasts/tailwind-bundle`. There is no Webpack Encore or npm build. Add JS packages with `php bin/console importmap:require <pkg>`. Do not bring in Encore: per `note.txt`, Stimulus controllers loaded through AssetMapper cannot import modules bundled by Encore (e.g. Flowbite).

Installed libraries for the planned features: dompdf (PDF), PhpSpreadsheet (Excel export), KnpPaginator (Tailwind template), rate-limiter, lock, uid, and donatj/phpuseragentparser (used by `ActivityLogger`).

## Domain (from projet.md)

**Roles and hierarchy:** `ROLE_ADMIN` > `ROLE_COMPTABLE` (accountant) > `ROLE_DISPATCHER` > `ROLE_AGENT`. Each role can create, block and supervise accounts and activity only for the roles below it. Agents see only the dossiers assigned to them.

**Auth:** email + password, with sessions that last 1 week. There is no self-service "forgot password". A higher-role user generates a temporary password, the app emails it, and the user must change it at first login.

**Dossier (client file):** client first name, last name, phone, and passport number. The passport number identifies the client, but one client can have several dossiers for separate requests, so it is not unique per dossier. Attached documents have labels that the user defines; other fields are optional.

**Workflow steps:**
1. Dossier creation (Dispatcher or Agent)
2. Assignment of dossiers to agents (Dispatcher)
3. Online submission on the embassy platform (Agent)
4. Tracking on the embassy platform (Agent)
5. Retrieval of the validation paper (Agent)
6. Physical deposit at the embassy (Dispatcher). The Dispatcher groups dossiers into **lots** and records the pickup date, receipts, images, and the deposit fee paid per lot.
7. Decision, favorable or unfavorable. A rejection sends the dossier back to online submission (Agent).
8. Document pickup at the embassy (Dispatcher marks it in the app). Agents then collect their dossiers and update the status.
9. Fee payment (Agent responsible for the dossier, or Accountant)
10. Return of the dossier to the client; processing ends (Agent)
