---
apply: always
---

### Creating and securing routes — Rules (Project‑specific)

These rules define how to add HTTP routes to the Copenhagen Truck Wash API and how to secure them consistently. They reflect current patterns in `services/nginx/app/routes` and the helper APIs in `traits\route_t`.

1. Location, naming, and structure
   - Place route classes under `services/nginx/app/routes`.
   - File names should describe the domain and end with `Route.php`, e.g., `BookingsRoute.php`, `ModuleSelfServeRoute.php`.
   - Namespace must be `routes` and each file must define one class that uses `traits\route_t` and implements `run(): void`.
     ```php
     <?php
     namespace routes;
     use traits\route_t;

     class ExampleRoute
     {
         use route_t;

         public function run(): void
         {
             // endpoints go here
         }
     }
     ```

2. Registering endpoints (HTTP verbs)
   - Define endpoints inside `run()` using the helpers provided by `route_t`:
     - `get($path, $handler, $permissions = [])`
     - `post($path, $handler, $permissions = [])`
     - `put($path, $handler, $permissions = [])`
     - `delete($path, $handler, $permissions = [])`
     - `patch($path, $handler, $permissions = [])`
     - `options($path, $handler, $permissions = [])` (useful for CORS preflight when needed)
   - Handlers are closures that perform validation, authorization, side effects, and write responses via the global `$response` (`classes\response`).
   - Example (read‑only endpoint):
     ```php
     $this->get('/example', function () {
         global $response;
         $response->success(['message' => 'Hello World!']);
     });
     ```

3. Route paths and local routing
   - Caddy serves the app directly; Traefik adds an external `/api` prefix for local access.
     - Direct path in the app: `/foo/bar`.
     - Local access paths (both work):
       - `http://localhost/foo/bar` (direct)
       - `http://localhost/api/foo/bar` (Traefik strip‑prefix `/api` → still routes to `/foo/bar` in the app)
   - Keep paths meaningful and resource‑oriented. Prefer plural nouns for collections and subpaths for actions when unavoidable (e.g., `/modules/self-serve/lane/status`).

4. Responses and status codes
   - Use `$response->success($payload, $status = 200)` for successful results and `$response->error($messageOrPayload, $status)` for failures.
   - Typical statuses:
     - 200 OK for successful reads/updates; 201 Created when new resources are created.
     - 400 Bad Request for validation errors.
     - 401 Unauthorized when authentication is missing/invalid.
     - 403 Forbidden when authenticated but lacking permissions.
   - Return concise, non‑sensitive error messages. Internal details go to logs (see `objects\logs_o`).

5. Input handling and validation (route_t helpers)
   - Read parameters via:
     - `getParameter($name)` or `getParametersAsArray()` (preferred; integrates with `classes\response` parsing)
     - `fromRequest($name)` (checks JSON body, then `$_POST`, then query)
     - `fromQuery($name)` and `fromRoute($segmentName)` when relevant
   - Validate early and fail fast using helpers:
     - Presence: `requireParameters(['a','b'])`, `isParametersSet(['a','b'])`
     - Types: `requireType($val, type_string()|type_int()|TYPE_BOOL()|TYPE_ARRAY())`, `requireTypeIn($val, [...])`
     - Ranges/lengths: `requireMinValue($n, $min)`, `requireMaxValue($n, $max)`, `requireMaxLength($name, $len)`, `requireParameterIntPositive($n, $name)`
     - Formats/enums: `requireDateFormat($date, FORMAT_DATE())`, `requireInArray($val, $allowed)`

6. Authentication and authorization (core rules)
   - Default posture: endpoints are protected and require authentication unless explicitly public.
     - To check authentication without a specific permission: `isAuthenticated()`.
   - Enforce permissions inside handlers using:
     - `requirePermission('some_permission')` — throws 401/403 as appropriate.
     - `hasPermission('some_permission', $customerNumber = null)` — boolean check (do not throw).
   - Register permissions for discoverability/administration by supplying the third `$permissions` argument when declaring the route. Keys are permission names; values are human‑readable descriptions. You should still call `requirePermission()` inside the handler to actually enforce.
     ```php
     $this->get('/modules/self-serve/lane/status', function () {
         global $response;
         self::requirePermission('modules_selfserve_lane_status_view');
         // ...
         $response->success(['status' => 'OK']);
     }, [
         'modules_selfserve_lane_status_view' => 'View self-serve lane status',
     ]);
     ```

7. Subusers, permission nodes, and customer context
   - When a permission corresponds to a subuser permission node, define it with `definePermission($perm, subusers_permission_node_key::CASE)` to keep mapping explicit.
   - Subuser contexts may target a specific customer via the `X-Customer-Number` header or a `customer_number` parameter; `route_t` exposes this as response meta (`target_customer_number`) when applicable.
   - Use `allowOwnOrDepartmentAccess($ownPerm, $deptPerm, $targetCustomerNumber, $departmentId, $ownGuard = null, $denyMessage = null)` for the common pattern: allow the principal to act on their own customer scope or fall back to a department/admin permission.
   - Use `isOwnCustomerContext($targetCustomerNumber)` and `resolveEffectiveCustomerNumber()` when building conditional logic.

8. Department, order, and special access helpers
   - Department access: `requireDepartmentAccess($departmentId, $permissionSuffix = null)`, `hasDepartmentAccess(...)`.
   - Order access: `requireOrderAccess($orderId, $permissionSuffix = null)` (note: indirect access rules may evolve; keep logic minimal in routes).
   - Plate scanners (API key based): protect endpoints with `requirePlateScannerAuth()`.
   - Bot protection (public forms): use `requireRecaptcha()` and expect `g_recaptcha_response` in the request.

9. Public endpoints
   - Keep public endpoints extremely limited. If an endpoint must be public, you must:
     - Validate all inputs thoroughly using the helpers above.
     - Add rate‑limit or abuse‑mitigation where relevant (Redis is available as `classes\redis` for counters/locks; coordinate design with maintainers before introducing new limits).
     - Prefer `requireRecaptcha()` for anonymous form submissions.

10. Side effects and idempotency
   - For POST/PUT/PATCH with side effects, make operations idempotent when feasible (e.g., by honoring a client idempotency key header). If you add such behavior, document it in the route’s docblock and in `openapi.yaml`.
   - Wrap multi‑step operations with proper validation and permission checks before any external calls (Stripe, e‑conomic, etc.). Do not leak third‑party error payloads directly; map to concise API errors and log details.

11. Logging and observability
   - On auth/permission denials, the helpers already log to `logs_o` with structured messages. Avoid duplicating logs for the same event.
   - For unexpected states you handle gracefully, add targeted logs via `objects\logs_o` with a clear category/key.

12. OpenAPI contract synchronization
   - Every public surface change must be reflected in `openapi.yaml` at repo root. Keep paths, methods, parameters, request/response schemas, and error statuses up to date.
   - Use the same path strings as in the route declaration (remember Traefik’s `/api` is stripped before hitting the app).
   - If you add or change permissions that users must hold, document them in the endpoint description.

13. Example: secure command endpoint
   ```php
   <?php
   namespace routes;
   use traits\route_t;
   use classes\authentication;

   class LaneCommandRoute
   {
       use route_t;

       public function run(): void
       {
           $this->post('/modules/self-serve/lane/command', function () {
               global $response;

               // AuthZ
               self::requirePermission('modules_selfserve_lane_command_execute');

               // Validate input
               self::requireParameters(['lane_id', 'command']);
               $laneId = (int) self::getParameter('lane_id');
               self::requireType($laneId, self::type_int());
               self::requireMinValue($laneId, 1);

               $cmd = (string) self::getParameter('command');
               self::requireType($cmd, self::type_string());
               self::requireInArray($cmd, ['START','STOP','RESERVE','RELEASE','RESET']);

               // Business logic (call into modules/classes)
               // ...

               $response->success(['ok' => true]);
           }, [
               'modules_selfserve_lane_command_execute' => 'Execute self‑serve lane commands',
           ]);
       }
   }
   ```

14. Quick reference (local)
   - Bring up minimal stack: `docker compose up -d traefik redis php1 caddy`
   - Access an app route locally:
     - `http://localhost/<path>` or `http://localhost/api/<path>`
   - Tests are CLI scripts under `services/nginx/app/tests`. Prefer unit‑style tests for core logic; see “Creating and maintaining tests — Rules”.

15. Code style and hygiene
   - Follow `.editorconfig` (UTF‑8, CRLF, 4‑space indents).
   - Keep route files cohesive; avoid adding unrelated endpoints to the same class. If a class exceeds ~200–300 lines or mixes domains, split it.
   - Do not perform heavy bootstrap in route files. Delegate to classes/modules and keep handlers thin: validate → authorize → invoke → respond.

