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.
This commit is contained in:
@@ -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/<domain>/YourTest.php` where `<domain>` 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/<domain>/<Name>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/<domain>/<Name>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 "\n<Name>Test 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:<feature>:<uuid>`). 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
|
||||
<?php
|
||||
if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); }
|
||||
// require_once WD . '/modules/foo/classes/bar.php';
|
||||
|
||||
function ok($m){ echo "\n\033[32m✔ $m\033[0m\n"; }
|
||||
function fail($m){ echo "\n\033[31m✖ $m\033[0m\n"; }
|
||||
$failed = 0;
|
||||
|
||||
// Arrange / Act
|
||||
$sum = 2 + 2;
|
||||
|
||||
// Assert
|
||||
if ($sum === 4) { ok('Basic arithmetic works (2 + 2 = 4)'); } else { fail('Expected 4'); $failed = 1; }
|
||||
|
||||
echo "\nExampleUnitTest completed.\n";
|
||||
exit($failed);
|
||||
```
|
||||
|
||||
- Integration‑style template (Redis example):
|
||||
```php
|
||||
<?php
|
||||
if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); }
|
||||
require_once WD . '/config.php'; // populates $REDIS_CONFIG when USE_ENV=true
|
||||
require_once WD . '/classes/redis.php';
|
||||
|
||||
function ok($m){ echo "\n\033[32m✔ $m\033[0m\n"; }
|
||||
function fail($m){ echo "\n\033[31m✖ $m\033[0m\n"; }
|
||||
$failed = 0;
|
||||
|
||||
// Use a namespaced test key
|
||||
$key = 'test:redis:example:' . uniqid('', true);
|
||||
|
||||
try {
|
||||
$r = new classes\redis();
|
||||
$r->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/<domain>/<Name>Test.php`
|
||||
- Run integration‑style test in Docker: `docker compose exec -T php1 php /var/www/html/tests/<domain>/<Name>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/<domain>/YourTest.php` where `<domain>` 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/<domain>/<Name>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/<domain>/<Name>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 "\n<Name>Test 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:<feature>:<uuid>`). 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
|
||||
<?php
|
||||
if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); }
|
||||
// require_once WD . '/modules/foo/classes/bar.php';
|
||||
|
||||
function ok($m){ echo "\n\033[32m✔ $m\033[0m\n"; }
|
||||
function fail($m){ echo "\n\033[31m✖ $m\033[0m\n"; }
|
||||
$failed = 0;
|
||||
|
||||
// Arrange / Act
|
||||
$sum = 2 + 2;
|
||||
|
||||
// Assert
|
||||
if ($sum === 4) { ok('Basic arithmetic works (2 + 2 = 4)'); } else { fail('Expected 4'); $failed = 1; }
|
||||
|
||||
echo "\nExampleUnitTest completed.\n";
|
||||
exit($failed);
|
||||
```
|
||||
|
||||
- Integration‑style template (Redis example):
|
||||
```php
|
||||
<?php
|
||||
if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); }
|
||||
require_once WD . '/config.php'; // populates $REDIS_CONFIG when USE_ENV=true
|
||||
require_once WD . '/classes/redis.php';
|
||||
|
||||
function ok($m){ echo "\n\033[32m✔ $m\033[0m\n"; }
|
||||
function fail($m){ echo "\n\033[31m✖ $m\033[0m\n"; }
|
||||
$failed = 0;
|
||||
|
||||
// Use a namespaced test key
|
||||
$key = 'test:redis:example:' . uniqid('', true);
|
||||
|
||||
try {
|
||||
$r = new classes\redis();
|
||||
$r->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/<domain>/<Name>Test.php`
|
||||
- Run integration‑style test in Docker: `docker compose exec -T php1 php /var/www/html/tests/<domain>/<Name>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).
|
||||
|
||||
@@ -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
|
||||
<?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.
|
||||
|
||||
@@ -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/<domain>/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`
|
||||
+118
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)'
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user