From 51014bf7748bb535cd0b6288d18157485feeec86 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 18 Feb 2026 14:13:05 +0100 Subject: [PATCH] Add development guidelines, testing rules, and secure routes documentation - Add `.junie/guidelines.md` with comprehensive development instructions. - Include `.aiassistant/rules/Creating and maintaining tests.md` and `.aiassistant/rules/Creating and securing routes.md`. - Introduce `CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES` for lane service validation. - Update `selfserve_lane_relay_controller_t` to enforce service-specific permissions for machine relay. --- .../rules/Creating and maintaining tests.md | 262 ++++++++++++++++++ .../rules/Creating and securing routes.md | 173 ++++++++++++ .junie/guidelines.md | 148 ++++++++++ openapi.yaml | 118 ++++++++ services/nginx/app/cli.php | 4 + .../traits/selfserve_lane_cache_t.php | 4 + .../selfserve_lane_relay_controller_t.php | 9 + .../nginx/app/routes/moduleSelfServeRoute.php | 82 ++++++ 8 files changed, 800 insertions(+) create mode 100644 .aiassistant/rules/Creating and maintaining tests.md create mode 100644 .aiassistant/rules/Creating and securing routes.md create mode 100644 .junie/guidelines.md diff --git a/.aiassistant/rules/Creating and maintaining tests.md b/.aiassistant/rules/Creating and maintaining tests.md new file mode 100644 index 00000000..d8a49b30 --- /dev/null +++ b/.aiassistant/rules/Creating and maintaining tests.md @@ -0,0 +1,262 @@ +--- +apply: always +--- + +### Creating and maintaining tests — Rules (Project‑specific) + +These rules apply to all tests under `services/nginx/app/tests` for the Copenhagen Truck Wash API. They codify how to create, run, and maintain the lightweight CLI PHP tests used in this repository. + +1. Scope and philosophy + - Prefer fast, deterministic CLI scripts over framework‑based tests. + - Keep unit‑style tests isolated from I/O (no DB/Redis/files/network). Use test doubles and explicit `require_once` for only the code exercised. + - Use integration tests sparingly and only when configuration/globals are required. Run them in Docker with `USE_ENV=true`. + +2. Location and naming + - Place tests in `services/nginx/app/tests//YourTest.php` where `` reflects the feature area (e.g., `subusers`, `orders`, `redis`). + - File names must end with `Test.php` (e.g., `SelfservePermissionInitTest.php`). + - Keep one coherent scenario per file. If a file grows beyond ~150 lines or mixes unrelated scenarios, split it. + +3. Execution modes + - Unit‑style (preferred): run on host with PHP CLI. + - Command: `php services/nginx/app/tests//Test.php` + - Integration‑style (needs env/globals): run inside Docker (php1) where `USE_ENV=true` and env vars are provided by `docker-compose.yml`. + - Start minimal stack: `docker compose up -d traefik redis php1 caddy` + - Command: `docker compose exec -T php1 php /var/www/html/tests//Test.php` + +4. Bootstrapping and includes + - At the top of every test, define `WD` if not already defined: + ```php + if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); } + ``` + - For unit‑style tests: include only the files you exercise via explicit `require_once WD . '/path/to/file.php';`. + - Do NOT include `config.php` for unit‑style tests; it throws if `USE_ENV` is not set. + - For integration‑style tests that need globals (`$CONFIG_DB`, `$REDIS_CONFIG`, etc.): `require_once WD . '/config.php';` and run the test inside `php1`. + +5. Output and exit codes + - Use simple helpers for human‑readable output: + ```php + function ok($message){ echo "\n\033[32m✔ $message\033[0m\n"; } + function fail($message){ echo "\n\033[31m✖ $message\033[0m\n"; } + ``` + - Prefer collecting failures and exiting non‑zero when any assertion fails: + ```php + $failed = 0; + // ... on failure set $failed = 1; + exit($failed); + ``` + - End with a concise completion line, e.g., `echo "\nTest completed.\n";`. + +6. Determinism, time, and randomness + - Avoid `sleep()` and real time dependencies. If time is relevant, pass it as a parameter or stub the time source. + - Do not use random values unless strictly necessary; if used, seed explicitly (`mt_srand(1234)`) and document it. + +7. External I/O and side effects + - Never call external services (Stripe, e‑conomic, MinIO, Slack, WordPress) from tests. Stub or override code paths as shown in `tests/subusers/SelfservePermissionInitTest.php`. + - Do not modify files under the repository during tests. If temporary files are required, use `sys_get_temp_dir()` and delete them before exit. + +8. Redis and database (integration‑only) + - Run inside Docker (`php1`) so `config.php` can populate `$REDIS_CONFIG` and `$CONFIG_DB`. + - Use unique, namespaced Redis keys for test data (e.g., `test::`). Clean them up at the end. Do NOT call `FLUSHALL`. + - Prefer fakes/stubs over real DB writes. If DB writes are unavoidable, scope them to clearly identifiable rows and delete them before exit. + +9. Performance budgets + - Each unit‑style test script should complete in < 100 ms on a typical dev machine. + - Each integration‑style test should complete in < 2 s and must avoid N+1 loops or heavy queries. + +10. When to add or update tests + - New route/feature: add at least one unit‑style test for core logic. If behavior depends on configuration/globals, add a minimal integration test. + - Bug fix: first reproduce with a failing test; then fix and ensure the test passes. + - Refactor: keep behavior‑preserving tests green. If public behavior changes intentionally, update both tests and `openapi.yaml` accordingly. + +11. Maintenance expectations + - Do not weaken or remove assertions to “make tests pass”. Investigate and fix root causes. + - Keep tests small and readable. Extract tiny helpers within the test file rather than introducing new shared libraries. + - Match the project code style and line endings (see `.editorconfig`: UTF‑8, CRLF, 4‑space indent, class brace `next_line`). + +12. Templates + - Unit‑style template: + ```php + set($key, 'pong'); + $val = $r->get($key); + if ($val === 'pong') { ok('Redis set/get works for namespaced key'); } else { fail('Unexpected value from Redis'); $failed = 1; } + } finally { + // Best‑effort cleanup + try { $r->del($key); } catch (\Throwable $e) { /* ignore */ } + } + + echo "\nExampleRedisIntegrationTest completed.\n"; + exit($failed); + ``` + +13. Quick reference + - Run unit‑style test on host: `php services/nginx/app/tests//Test.php` + - Run integration‑style test in Docker: `docker compose exec -T php1 php /var/www/html/tests//Test.php` + - Minimal stack for integration: `docker compose up -d traefik redis php1 caddy` + +For broader build, configuration, and existing examples, see `.junie/guidelines.md` (Testing section). + +--- +apply: always +--- + +### Creating and maintaining tests — Rules (Project‑specific) + +These rules apply to all tests under `services/nginx/app/tests` for the Copenhagen Truck Wash API. They codify how to create, run, and maintain the lightweight CLI PHP tests used in this repository. + +1. Scope and philosophy + - Prefer fast, deterministic CLI scripts over framework‑based tests. + - Keep unit‑style tests isolated from I/O (no DB/Redis/files/network). Use test doubles and explicit `require_once` for only the code exercised. + - Use integration tests sparingly and only when configuration/globals are required. Run them in Docker with `USE_ENV=true`. + +2. Location and naming + - Place tests in `services/nginx/app/tests//YourTest.php` where `` reflects the feature area (e.g., `subusers`, `orders`, `redis`). + - File names must end with `Test.php` (e.g., `SelfservePermissionInitTest.php`). + - Keep one coherent scenario per file. If a file grows beyond ~150 lines or mixes unrelated scenarios, split it. + +3. Execution modes + - Unit‑style (preferred): run on host with PHP CLI. + - Command: `php services/nginx/app/tests//Test.php` + - Integration‑style (needs env/globals): run inside Docker (php1) where `USE_ENV=true` and env vars are provided by `docker-compose.yml`. + - Start minimal stack: `docker compose up -d traefik redis php1 caddy` + - Command: `docker compose exec -T php1 php /var/www/html/tests//Test.php` + +4. Bootstrapping and includes + - At the top of every test, define `WD` if not already defined: + ```php + if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); } + ``` + - For unit‑style tests: include only the files you exercise via explicit `require_once WD . '/path/to/file.php';`. + - Do NOT include `config.php` for unit‑style tests; it throws if `USE_ENV` is not set. + - For integration‑style tests that need globals (`$CONFIG_DB`, `$REDIS_CONFIG`, etc.): `require_once WD . '/config.php';` and run the test inside `php1`. + +5. Output and exit codes + - Use simple helpers for human‑readable output: + ```php + function ok($message){ echo "\n\033[32m✔ $message\033[0m\n"; } + function fail($message){ echo "\n\033[31m✖ $message\033[0m\n"; } + ``` + - Prefer collecting failures and exiting non‑zero when any assertion fails: + ```php + $failed = 0; + // ... on failure set $failed = 1; + exit($failed); + ``` + - End with a concise completion line, e.g., `echo "\nTest completed.\n";`. + +6. Determinism, time, and randomness + - Avoid `sleep()` and real time dependencies. If time is relevant, pass it as a parameter or stub the time source. + - Do not use random values unless strictly necessary; if used, seed explicitly (`mt_srand(1234)`) and document it. + +7. External I/O and side effects + - Never call external services (Stripe, e‑conomic, MinIO, Slack, WordPress) from tests. Stub or override code paths as shown in `tests/subusers/SelfservePermissionInitTest.php`. + - Do not modify files under the repository during tests. If temporary files are required, use `sys_get_temp_dir()` and delete them before exit. + +8. Redis and database (integration‑only) + - Run inside Docker (`php1`) so `config.php` can populate `$REDIS_CONFIG` and `$CONFIG_DB`. + - Use unique, namespaced Redis keys for test data (e.g., `test::`). Clean them up at the end. Do NOT call `FLUSHALL`. + - Prefer fakes/stubs over real DB writes. If DB writes are unavoidable, scope them to clearly identifiable rows and delete them before exit. + +9. Performance budgets + - Each unit‑style test script should complete in < 100 ms on a typical dev machine. + - Each integration‑style test should complete in < 2 s and must avoid N+1 loops or heavy queries. + +10. When to add or update tests +- New route/feature: add at least one unit‑style test for core logic. If behavior depends on configuration/globals, add a minimal integration test. +- Bug fix: first reproduce with a failing test; then fix and ensure the test passes. +- Refactor: keep behavior‑preserving tests green. If public behavior changes intentionally, update both tests and `openapi.yaml` accordingly. + +11. Maintenance expectations +- Do not weaken or remove assertions to “make tests pass”. Investigate and fix root causes. +- Keep tests small and readable. Extract tiny helpers within the test file rather than introducing new shared libraries. +- Match the project code style and line endings (see `.editorconfig`: UTF‑8, CRLF, 4‑space indent, class brace `next_line`). + +12. Templates +- Unit‑style template: + ```php + set($key, 'pong'); + $val = $r->get($key); + if ($val === 'pong') { ok('Redis set/get works for namespaced key'); } else { fail('Unexpected value from Redis'); $failed = 1; } + } finally { + // Best‑effort cleanup + try { $r->del($key); } catch (\Throwable $e) { /* ignore */ } + } + + echo "\nExampleRedisIntegrationTest completed.\n"; + exit($failed); + ``` + +13. Quick reference +- Run unit‑style test on host: `php services/nginx/app/tests//Test.php` +- Run integration‑style test in Docker: `docker compose exec -T php1 php /var/www/html/tests//Test.php` +- Minimal stack for integration: `docker compose up -d traefik redis php1 caddy` + +For broader build, configuration, and existing examples, see `.junie/guidelines.md` (Testing section). + diff --git a/.aiassistant/rules/Creating and securing routes.md b/.aiassistant/rules/Creating and securing routes.md new file mode 100644 index 00000000..75c22fcb --- /dev/null +++ b/.aiassistant/rules/Creating and securing routes.md @@ -0,0 +1,173 @@ +--- +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 + 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 + 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/` or `http://localhost/api/` + - 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. + diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 00000000..38ca5fe5 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,148 @@ +### Copenhagen Truck Wash API — Development Guidelines (Project‑specific) + +#### Scope +This document captures project‑specific knowledge for building, configuring, testing, and extending the API. It assumes an advanced developer familiar with Docker, PHP 8.2, and HTTP APIs. + +--- + +### Build and Configuration + +- Stack overview (local): + - Reverse proxy/router: Traefik 2.x (`docker-compose.yml` service `traefik`). + - Web server: Caddy (`caddy`) serving the PHP app from `services/nginx/app` and proxied by Traefik. + - PHP runtime: Multiple PHP‑FPM containers (`php1`..`php5`), sharing the bind‑mounted app directory. + - Redis: `redis` for caching/queues/locks. + +- App location: `services/nginx/app` is the effective application root (many scripts/tests derive `WD` to point here). + +- Composer and dependencies: + - `services/php/Dockerfile` installs Composer and PHP extensions. + - `services/php/docker-entrypoint.sh` performs a guarded Composer install on `php1` at container start when `AUTO_COMPOSER_INSTALL=true` and `composer.json` is present. + - App dependencies live under `services/nginx/app/composer.json` (note: very light, primarily runtime libs; dev tool `rector/rector`). + +- Configuration source of truth during containerized runs is environment variables consumed by `services/nginx/app/config.php`. + - `config.php` requires `USE_ENV=true`; otherwise it throws an exception. Many CLI scripts/tests bypass `config.php` entirely to remain env‑agnostic. + +- Local run targets and routing: + - Traefik exposes: + - `http://localhost` → routes to Caddy → app (HTTP only for dev). + - `https://localhost` → also mapped, using Traefik’s default/self‑signed dev cert. + - `http(s)://localhost/api/*` → Traefik strip‑prefix middleware forwards to Caddy; the app sees paths without the `/api` prefix. + - For production‑like HTTPS with real certs, see `README.md` for `LETSENCRYPT_PATH` mounting strategy (only needed if you want the exact `api.truckwash.dk` TLS behavior locally). + +- Minimal bring‑up for local development: + - Prerequisites: Docker Desktop 4.x+. + - First run will build PHP images and start dependent services. Composer install runs automatically on `php1`. + - Recommended minimal set: + - `docker compose up -d traefik redis php1 caddy` + - Full set (scale out PHP or add observability as needed): + - `docker compose up -d` (starts `traefik`, `redis`, `caddy`, `php1`..`php5`, and other declared services). + - Logs: + - `docker compose logs -f caddy` + - `docker compose logs -f php1` + - `docker compose logs -f traefik` + +- Security note: `docker-compose.yml` currently embeds sensitive env values (DB, API tokens). Treat the file as secret in private repos; never re‑publish as is. Prefer `.env` overrides and secrets providers for wider teams. + +--- + +### Testing + +The repository does not use PHPUnit for the app. Instead, tests are lightweight CLI scripts under `services/nginx/app/tests`. Conventions: + +- Test style + - Self‑contained procedural PHP scripts intended to be executed with `php`. No framework required. + - Many tests define the `WD` constant to the app root and then `require_once` specific class/trait/interface files they exercise. + - Integration‑style scripts that need configuration will rely on `services/nginx/app/config.php` and so must run within a properly provisioned environment (Docker containers with `USE_ENV=true`). + - Fast unit‑style scripts should avoid `config.php` and any I/O; they manually include only what’s needed and/or use test doubles. + +- Running tests from host (fastest path) + - Prereq: PHP CLI available on host. Verified with: + - `php -v` → observed on our env: `PHP 8.2.30 (cli)` + - Example: an existing, fully self‑contained test validating subuser permission wiring: + - Command: + - `php services/nginx/app/tests/subusers/SelfservePermissionInitTest.php` + - Verified output (captured): + - + ``` + ✔ SELFSERVE_ADD is granted as expected + ✔ SELFSERVE_LIST is not granted as expected + ✔ SELFSERVE_EDIT is not granted as expected + ✔ SELFSERVEDELETE is not granted as expected + SelfservePermissionInitTest completed. + ``` + +- Running tests inside Docker (when host PHP is unavailable or when env is required) + - Ensure containers are up: `docker compose up -d traefik redis php1 caddy` + - Execute a test within `php1`: + - `docker compose exec -T php1 php /var/www/html/tests/subusers/SelfservePermissionInitTest.php` + - For integration tests that depend on `config.php` (e.g., Redis/DB), environment variables are preconfigured in `docker-compose.yml` for the PHP services. Running inside `php1` will satisfy `USE_ENV=true`. + +- Adding a new test + - Place the file under `services/nginx/app/tests//YourTest.php`. + - At top of the file, define `WD` if not already defined: + ```php + if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); } + ``` + - Prefer self‑contained tests that avoid I/O. If you need to touch internal classes without Composer autoloading, include files directly, mirroring existing tests. + - If the test must hit Redis/DB or rely on globals from `config.php`, run it inside a PHP container (`php1`) with `USE_ENV=true`. + +- Demonstration: creating and running a minimal test + - We created a temporary, env‑agnostic test at `services/nginx/app/tests/examples/HelloWorldTest.php` with these semantics: + - Define `WD`, perform trivial assertions, and print success markers. + - Command executed and verified output: + - Command: + - `php services/nginx/app/tests/examples/HelloWorldTest.php` + - Output (captured): + - + ``` + ✔ Basic arithmetic works (2 + 2 = 4) + ✔ WD points to the application root: C:\\Users\\2jepp\\PhpstormProjects\\api\\services\\nginx\\app + HelloWorldTest completed. + ``` + - The example file was removed afterwards to keep the repository unchanged. You can replicate by creating a similar file and removing it after execution. + +--- + +### Additional Development Information + +- Routing and HTTP surface + - Routes live under `services/nginx/app/routes`. Example route `exampleRoute.php` exposes `GET /example` returning `{"message":"Hello World!"}`. Local access paths (with Traefik): + - `http://localhost/example` (direct) + - `http://localhost/api/example` (Traefik strip‑prefix `/api` → still routes to `/example` in the app). + - The OpenAPI contract is at the repo root `openapi.yaml` (large, authoritative). Keep it synchronized with implemented routes and payloads. + +- Module layout and traits + - Domain modules live in `services/nginx/app/modules/*` and are heavily trait‑based. Tests often pull in precise files from here to avoid full app bootstrap. + - Example: subusers module (`modules/subusers/...`) provides permission node containers and helpers. The test `tests/subusers/SelfservePermissionInitTest.php` demonstrates overriding DB‑backed methods to inject permissions for fast, deterministic checks. + +- Config and globals + - `services/nginx/app/config.php` populates globals like `$CONFIG_DB`, `$REDIS_CONFIG`, etc., but only when `USE_ENV=true`. If you see tests throwing “Environment variables are not set”, run them inside Docker or set required envs for host PHP. + +- Code style + - `.editorconfig` at repo root configures formatting. Key PHP rules: + - Encoding: UTF‑8 with CRLF line endings. + - Indent: 4 spaces; continuation indent 4. + - Class brace style: next_line; function/method blank lines: 1. + - Import sorting: alphabetic; various alignment toggles are disabled. + - Match existing patterns (procedural scripts for tests; namespacing in app code; traits for cross‑cutting concerns). Avoid adding new frameworks for tests unless explicitly requested. + +- Composer / Autoload + - There is no Composer autoload configured for the app code in `composer.json`; tests include files directly. If you introduce autoloading, coordinate with Docker entrypoint behaviors and ensure zero‑downtime for existing scripts. + +- Performance & reliability hints for tests + - Prefer small, deterministic CLI scripts; inject dependencies and override I/O methods (as shown in `SelfservePermissionInitTest.php`). + - Avoid hitting external services (Stripe, e‑conomic, MinIO) from tests; instead, stub/override or provide fakes. + +--- + +### Quick Reference + +- Bring up minimal dev stack: + - `docker compose up -d traefik redis php1 caddy` + +- Run a fast, self‑contained test (host PHP): + - `php services/nginx/app/tests/subusers/SelfservePermissionInitTest.php` + +- Run a test inside Docker (env‑backed): + - `docker compose exec -T php1 php /var/www/html/tests/subusers/SelfservePermissionInitTest.php` diff --git a/openapi.yaml b/openapi.yaml index 1b1f13fb..04e7ae15 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2756,6 +2756,11 @@ paths: order_priority: type: integer default: 0 + services: + type: array + description: Optional services enabled by this task. Items must be valid service enum names. + items: + $ref: '#/components/schemas/SelfserveLaneService' responses: '200': description: Successfully added task @@ -2802,6 +2807,12 @@ paths: type: string order_priority: type: integer + services: + type: array + nullable: true + description: Services enabled by this task. Set to null to clear all services. + items: + $ref: '#/components/schemas/SelfserveLaneService' responses: '200': description: Successfully updated task @@ -5060,6 +5071,101 @@ paths: schema: $ref: '#/components/schemas/SelfServeLaneStatus' + /modules/self-serve/lane/services/allowed: + post: + tags: + - Modules + summary: Set allowed services for a lane based on shown tasks + description: | + Updates the set of services that are allowed to be manually activated for a given self-serve lane, + derived from the tasks currently shown to the user after answering the self-serve questions. + This endpoint does not activate anything by itself; it only sets what is allowed to be activated. + operationId: setSelfServeLaneAllowedServices + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lane_id + properties: + lane_id: + type: integer + task_ids: + type: array + description: List of task IDs that are currently shown to the user + items: + type: integer + responses: + '200': + description: Allowed services updated + content: + application/json: + schema: + type: object + properties: + lane_id: + type: integer + allowed_services: + type: array + items: + type: string + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /modules/self-serve/lane/relay/machine/enable: + post: + tags: + - Modules + summary: Manually enable MACHINE relay for a lane + description: | + Manually turns on the MACHINE relay for a self-serve lane if and only if the current allowed services + include `MACHINE` (set via `/modules/self-serve/lane/services/allowed`). The relay is never automatically + enabled; an explicit call to this endpoint is required. + operationId: enableSelfServeLaneMachineRelay + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lane_id + properties: + lane_id: + type: integer + duration: + type: integer + description: Optional number of seconds after which the relay should automatically turn off + responses: + '200': + description: MACHINE relay enabled + content: + application/json: + schema: + type: object + properties: + lane_id: + type: integer + relay: + type: string + enabled: + type: boolean + duration: + type: integer + nullable: true + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Not allowed to enable MACHINE relay (no matching task currently shown) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + # Module - Other Integration Endpoints /modules/motorapi/lookup: get: @@ -6609,6 +6715,12 @@ components: type: integer description: The product ID used for minute-based billing + SelfserveLaneService: + type: string + description: Allowed self-serve lane service name + enum: + - MACHINE + DepartmentSelfserveQuestion: type: object properties: @@ -6672,6 +6784,12 @@ components: order_priority: type: integer description: Display order priority (lower numbers shown first) + services: + type: array + description: Services enabled by this task. Each item must be a valid service enum name. + items: + $ref: '#/components/schemas/SelfserveLaneService' + default: [] created_at: type: string format: date-time diff --git a/services/nginx/app/cli.php b/services/nginx/app/cli.php index 62b9d3c3..80c1d942 100644 --- a/services/nginx/app/cli.php +++ b/services/nginx/app/cli.php @@ -78,6 +78,10 @@ if ($args[1] === 'run') { echo "Running the Permission node mapping test script"; require_once 'tests/permissions/PermissionNodeTest.php'; break; + case 'selfserve-relay-gating-test': + echo "Running the Self-Serve relay gating test script"; + require_once 'tests/selfserve/SelfServeRelayGatingTest.php'; + break; case 'logSync': require_once 'cron/SyncLogs.php'; break; diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_cache_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_cache_t.php index 04aa098c..1adf8a29 100644 --- a/services/nginx/app/modules/selfserve/traits/selfserve_lane_cache_t.php +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_cache_t.php @@ -17,6 +17,10 @@ trait selfserve_lane_cache_t const CACHE_SELFSERVE_LANE_KEY_CUSTOMER_NUMBER = self::CACHE_SELFSERVE_PREFIX . 'customer_number'; const CACHE_SELFSERVE_LANE_KEY_LICENSE_PLATE = self::CACHE_SELFSERVE_PREFIX . 'license_plate'; + // Allowed services for this lane, derived from currently shown tasks after Q&A. + // Stored as an array of service names (e.g., ['MACHINE']). + const CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES = self::CACHE_SELFSERVE_PREFIX . 'allowed_services'; + /** * Get the cache key for the lane diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php index 7d7eb5ac..bcc8ef1f 100644 --- a/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php @@ -9,6 +9,7 @@ use modules\selfserve\helpers\selfserve_lane_log_action; use modules\selfserve\helpers\selfserve_lane_relay; use modules\selfserve\helpers\selfserve_lane_state; use modules\selfserve\helpers\selfserve_lane_status; +use modules\selfserve\helpers\selfserve_lane_services; use modules\shelly\helpers\shelly_device_switch; use modules\shelly\helpers\shelly_request_body_get_states; @@ -29,6 +30,14 @@ trait selfserve_lane_relay_controller_t if ($this->status->equals(selfserve_lane_status::CLOSED)) throw new \Exception("Cannot turn on relay on CLOSED lane"); if ($this->status->equals(selfserve_lane_status::MAINTENANCE)) throw new \Exception("Cannot turn on relay on MAINTENANCE lane"); if ($this->status->equals(selfserve_lane_status::FAULT)) throw new \Exception("Cannot turn on relay on FAULT lane"); + // Enforce that MACHINE relay can only be enabled when allowed by current self-serve tasks (self-serve, manual trigger required) + if ($relay === selfserve_lane_relay::MACHINE) { + // Allowed services are stored as an array of names in lane cache + $allowed = $this->getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES); + if (!is_array($allowed) || !in_array(selfserve_lane_services::MACHINE->name, $allowed, true)) { + throw new \Exception("MACHINE relay is not allowed to be enabled at this time"); + } + } // Get the relay ID based on the relay type $relay_id = match ($relay) { selfserve_lane_relay::MACHINE => $this->department_lane->relay_machine_id->value(), diff --git a/services/nginx/app/routes/moduleSelfServeRoute.php b/services/nginx/app/routes/moduleSelfServeRoute.php index 6174b79a..9a68d6cf 100644 --- a/services/nginx/app/routes/moduleSelfServeRoute.php +++ b/services/nginx/app/routes/moduleSelfServeRoute.php @@ -9,6 +9,7 @@ use classes\router; use classes\selfserve; use classes\stripe; use modules\selfserve\helpers\selfserve_lane_command; +use modules\selfserve\helpers\selfserve_lane_relay; use objects\departments_o; use objects\logs_o; use objects\orders_o; @@ -125,5 +126,86 @@ class moduleSelfServeRoute 'modules_selfserve_lane_command_bypass_customer_number_validation' => 'Bypass customer number validation when executing commands', ] ); + + /** Modules > Self Serve > Lane > Allowed services (derived from shown tasks) */ + $this->post('/modules/self-serve/lane/services/allowed', function () { + global $response; + self::requirePermission('modules_selfserve_lane_services_set_allowed'); + $selfserve = new selfserve(); + // Validate parameters + self::requireParameters(['lane_id']); + $lane_id = (int)$this->getParameter('lane_id'); + self::requireType($lane_id, self::type_int()); + self::requireMinValue($lane_id, 1); + // Parse task_ids from request (optional, may be empty to clear) + $task_ids_param = $this->isParametersSet(['task_ids']) ? $this->getParameter('task_ids') : []; + if (is_string($task_ids_param)) { + $decoded = json_decode($task_ids_param, true); + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $task_ids_param = $decoded; + } else { + $task_ids_param = array_filter(array_map(fn($s) => (int)trim($s), explode(',', $task_ids_param)), fn($v) => $v > 0); + } + } + if (!is_array($task_ids_param)) { + $response->error('Invalid format for task_ids. Expected array, JSON array, or comma-separated string of IDs.', 400); + } + $task_ids = array_values(array_unique(array_map(fn($v) => (int)$v, $task_ids_param))); + // Build allowed services from provided tasks + $lane = $selfserve->lane($lane_id); + $allowed_services = []; + foreach ($task_ids as $tid) { + if ($tid <= 0) continue; + $t = new \objects\department_selfserve_tasks_o(); + $t->select($tid); + if (!$t->exists()) continue; // ignore unknown ids + // Validate the task belongs to the same lane + if ((int)$t->lane->value() !== $lane_id) continue; + // Merge services (if any) + $services = (array)$t->services->value(); + foreach ($services as $srv) { + $name = strtoupper((string)$srv); + if (!in_array($name, $allowed_services, true)) { + $allowed_services[] = $name; + } + } + } + // Persist on lane cache (overwrites previous allowed services) + $lane->setLaneCache($lane_id, $lane::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES, $allowed_services); + $response->success(['lane_id' => $lane_id, 'allowed_services' => $allowed_services]); + }, [ + 'modules_selfserve_lane_services_set_allowed' => 'Set allowed services for a lane based on currently shown tasks (post-Q&A)' + ]); + + /** Modules > Self Serve > Lane > Relay > Enable MACHINE (manual, gated by allowed services) */ + $this->post('/modules/self-serve/lane/relay/machine/enable', function () { + global $response; + self::requirePermission('modules_selfserve_lane_relay_enable_machine'); + $selfserve = new selfserve(); + // Validate parameters + self::requireParameters(['lane_id']); + $lane_id = (int)$this->getParameter('lane_id'); + self::requireType($lane_id, self::type_int()); + self::requireMinValue($lane_id, 1); + $duration = null; + if ($this->isParametersSet(['duration'])) { + $duration = (int)$this->getParameter('duration'); + self::requireMinValue($duration, 1); + } + $lane = $selfserve->lane($lane_id); + try { + $lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration); + $response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE', 'enabled' => true, 'duration' => $duration]); + } catch (\Exception $e) { + $msg = $e->getMessage(); + // If gating prevented activation, respond with 403 + if (str_contains(strtoupper($msg), 'NOT ALLOWED')) { + $response->error($msg, 403); + } + $response->error('Failed to enable MACHINE relay: ' . $msg, 400); + } + }, [ + 'modules_selfserve_lane_relay_enable_machine' => 'Manually enable MACHINE relay for a lane (requires allowed task to be present)' + ]); } } \ No newline at end of file