From c38a4379bd9de4907f31ce32d73db89cbdbf1b04 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 23 Apr 2026 13:31:57 +0200 Subject: [PATCH] Add department hardware workspace and plate scanner updates --- docker-compose.example.yml | 17 +- docker-compose.prod.standalone.yml | 424 +++++++++ docker-compose.yml | 39 +- services/edge-broker/test/config.test.mjs | 31 +- .../nginx/app/.phpunit.cache/test-results | 2 +- .../nginx/app/build/logs/api-server.err.log | 42 + .../app/classes/department_gate_config.php | 27 +- services/nginx/app/classes/shelly.php | 53 +- .../app/classes/shelly_relay_inventory.php | 243 +++++ services/nginx/app/interfaces/shelly_i.php | 10 +- ...e_gateway_department_workspace_service.php | 865 ++++++++++++++++++ .../classes/edge_gateway_manager.php | 44 +- .../routes/moduleEdgeGatewayRoute.php | 32 + .../nginx/app/objects/department_gates_o.php | 52 +- .../nginx/app/objects/plate_scanners_o.php | 156 +++- .../nginx/app/routes/departmentLanesRoute.php | 47 + .../app/routes/machineButtonPressRoute.php | 11 + .../nginx/app/routes/plateScannersRoute.php | 67 +- .../app/tests/Api/PlateScannersApiTest.php | 125 +++ .../app/tests/Api/api_coverage_manifest.php | 1 + .../composer-entrypoint-autoload-recovery.sh | 49 + .../Bird/DepartmentGatesRelayOpenTest.php | 79 ++ .../DepartmentGateConfigRelayTest.php | 30 + ...artmentLaneRelayOptionsRouteWiringTest.php | 10 + ...GatewayDepartmentWorkspaceContractTest.php | 17 + .../Selfserve/EdgeGatewayManagerUrlTest.php | 25 +- .../EdgeGatewayModuleRouteWiringTest.php | 2 + .../PlateScannerWorkspaceContractTest.php | 23 + .../Selfserve/ShellyRelayInventoryTest.php | 81 ++ services/php/docker-entrypoint.sh | 17 +- 30 files changed, 2561 insertions(+), 60 deletions(-) create mode 100644 docker-compose.prod.standalone.yml create mode 100644 services/nginx/app/classes/shelly_relay_inventory.php create mode 100644 services/nginx/app/modules/edgegateway/classes/edge_gateway_department_workspace_service.php create mode 100644 services/nginx/app/tests/Api/PlateScannersApiTest.php create mode 100644 services/nginx/app/tests/Tooling/composer-entrypoint-autoload-recovery.sh create mode 100644 services/nginx/app/tests/Unit/Bird/DepartmentGatesRelayOpenTest.php create mode 100644 services/nginx/app/tests/Unit/Selfserve/DepartmentGateConfigRelayTest.php create mode 100644 services/nginx/app/tests/Unit/Selfserve/DepartmentLaneRelayOptionsRouteWiringTest.php create mode 100644 services/nginx/app/tests/Unit/Selfserve/EdgeGatewayDepartmentWorkspaceContractTest.php create mode 100644 services/nginx/app/tests/Unit/Selfserve/PlateScannerWorkspaceContractTest.php create mode 100644 services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 6cc669c2..c9a12211 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -53,8 +53,21 @@ services: container_name: edge-broker environment: EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} - ports: - - "4300:4300" + labels: + - "traefik.enable=true" + - "traefik.http.routers.edge-broker-api.rule=Host(`api.example.com`) && PathPrefix(`/edge-broker`)" + - "traefik.http.routers.edge-broker-api.entrypoints=websecure" + - "traefik.http.routers.edge-broker-api.tls=true" + - "traefik.http.routers.edge-broker-api.tls.certresolver=le" + - "traefik.http.routers.edge-broker-api.middlewares=edge-broker-strip" + - "traefik.http.routers.edge-broker-api.service=edge-broker" + - "traefik.http.routers.edge-broker-local.rule=Host(`localhost`) && PathPrefix(`/api/edge-broker`)" + - "traefik.http.routers.edge-broker-local.entrypoints=web" + - "traefik.http.routers.edge-broker-local.middlewares=edge-broker-strip-local" + - "traefik.http.routers.edge-broker-local.service=edge-broker" + - "traefik.http.middlewares.edge-broker-strip.stripPrefix.prefixes=/edge-broker" + - "traefik.http.middlewares.edge-broker-strip-local.stripPrefix.prefixes=/api/edge-broker" + - "traefik.http.services.edge-broker.loadbalancer.server.port=4300" caddy: image: caddy:2.7.6-alpine diff --git a/docker-compose.prod.standalone.yml b/docker-compose.prod.standalone.yml new file mode 100644 index 00000000..da4f465b --- /dev/null +++ b/docker-compose.prod.standalone.yml @@ -0,0 +1,424 @@ +services: + traefik: + image: traefik:2.11 + container_name: traefik + ports: + - "80:80" + - "443:443" + - "4433:4433" + - "9100:9100" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./services/traefik/traefik.yml:/etc/traefik/traefik.yml:ro + - ./services/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro + - ./services/traefik/acme.json:/acme.json + - ./services/traefik/acme-io.json:/acme-io.json + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.rule=Host(`traefik.truckwash.dk`)" + - "traefik.http.routers.traefik.entrypoints=websecure" + - "traefik.http.routers.traefik.tls=true" + - "traefik.http.routers.traefik.tls.certresolver=le" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.middlewares=dashboard-allow-local@file,dashboard-auth@file" + - "traefik.http.routers.traefik-http.rule=Host(`traefik.truckwash.dk`)" + - "traefik.http.routers.traefik-http.entrypoints=web" + - "traefik.http.routers.traefik-http.middlewares=redirect-to-https@file" + - "traefik.http.routers.traefik-http.service=api@internal" + - "traefik.http.routers.traefik-local.rule=Host(`traefik.localhost`)" + - "traefik.http.routers.traefik-local.entrypoints=web" + - "traefik.http.routers.traefik-local.service=api@internal" + - "traefik.http.routers.traefik-local.middlewares=dashboard-allow-local@file,dashboard-auth@file" + + redis: + image: redis:7 + container_name: redis + volumes: + - nnks_redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + redis-staging: + image: redis:7 + container_name: redis-staging + volumes: + - nnks_redis_staging:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + mysql-debug: + image: mysql:8.4 + container_name: mysql-debug + environment: + MYSQL_ROOT_PASSWORD: ${CONFIG_DB_DEBUG_PASSWORD:-debug_root_password} + MYSQL_DATABASE: ${CONFIG_DB_DEBUG_DATABASE:-nnks_db_debug} + ports: + - "3307:3306" + volumes: + - db_debug_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "MYSQL_PWD=$$MYSQL_ROOT_PASSWORD mysqladmin -u root ping --silent"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + + edge-broker: + build: + context: . + dockerfile: services/edge-broker/Dockerfile + container_name: edge-broker + environment: + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + labels: + - "traefik.enable=true" + - "traefik.http.routers.edge-broker-api.rule=Host(`api.truckwash.dk`) && PathPrefix(`/edge-broker`)" + - "traefik.http.routers.edge-broker-api.entrypoints=websecure" + - "traefik.http.routers.edge-broker-api.tls=true" + - "traefik.http.routers.edge-broker-api.tls.certresolver=le" + - "traefik.http.routers.edge-broker-api.middlewares=secure-headers@file,edge-broker-strip" + - "traefik.http.routers.edge-broker-api.priority=200" + - "traefik.http.routers.edge-broker-api.service=edge-broker" + - "traefik.http.routers.edge-broker-api-io.rule=Host(`api.truckwash.io`) && PathPrefix(`/edge-broker`)" + - "traefik.http.routers.edge-broker-api-io.entrypoints=websecure" + - "traefik.http.routers.edge-broker-api-io.tls=true" + - "traefik.http.routers.edge-broker-api-io.tls.certresolver=le_io" + - "traefik.http.routers.edge-broker-api-io.middlewares=secure-headers@file,edge-broker-strip" + - "traefik.http.routers.edge-broker-api-io.priority=200" + - "traefik.http.routers.edge-broker-api-io.service=edge-broker" + - "traefik.http.routers.edge-broker-api-staging.rule=Host(`api.truckwash.io`) && PathPrefix(`/edge-broker`)" + - "traefik.http.routers.edge-broker-api-staging.entrypoints=websecure-staging" + - "traefik.http.routers.edge-broker-api-staging.tls=true" + - "traefik.http.routers.edge-broker-api-staging.tls.certresolver=le_io" + - "traefik.http.routers.edge-broker-api-staging.middlewares=secure-headers@file,edge-broker-strip" + - "traefik.http.routers.edge-broker-api-staging.priority=200" + - "traefik.http.routers.edge-broker-api-staging.service=edge-broker" + - "traefik.http.routers.edge-broker-local.rule=Host(`localhost`) && PathPrefix(`/api/edge-broker`)" + - "traefik.http.routers.edge-broker-local.entrypoints=web" + - "traefik.http.routers.edge-broker-local.middlewares=secure-headers@file,edge-broker-strip-local" + - "traefik.http.routers.edge-broker-local.priority=200" + - "traefik.http.routers.edge-broker-local.service=edge-broker" + - "traefik.http.routers.edge-broker-local-secure.rule=Host(`localhost`) && PathPrefix(`/api/edge-broker`)" + - "traefik.http.routers.edge-broker-local-secure.entrypoints=websecure" + - "traefik.http.routers.edge-broker-local-secure.tls=true" + - "traefik.http.routers.edge-broker-local-secure.middlewares=secure-headers@file,edge-broker-strip-local" + - "traefik.http.routers.edge-broker-local-secure.priority=200" + - "traefik.http.routers.edge-broker-local-secure.service=edge-broker" + - "traefik.http.routers.edge-broker-local-staging.rule=Host(`localhost`) && PathPrefix(`/api/edge-broker`)" + - "traefik.http.routers.edge-broker-local-staging.entrypoints=websecure-staging" + - "traefik.http.routers.edge-broker-local-staging.tls=true" + - "traefik.http.routers.edge-broker-local-staging.middlewares=secure-headers@file,edge-broker-strip-local" + - "traefik.http.routers.edge-broker-local-staging.priority=200" + - "traefik.http.routers.edge-broker-local-staging.service=edge-broker" + - "traefik.http.middlewares.edge-broker-strip.stripPrefix.prefixes=/edge-broker" + - "traefik.http.middlewares.edge-broker-strip-local.stripPrefix.prefixes=/api/edge-broker" + - "traefik.http.services.edge-broker.loadbalancer.server.port=4300" + + caddy: + image: caddy:2.7.6-alpine + container_name: caddy + depends_on: + - php1 + - php2 + - php3 + - php4 + - php5 + volumes: + - ./services/nginx/app:/var/www/html + - ./services/caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - ./services/caddy/logs:/var/log/caddy + labels: + - "traefik.enable=true" + - "traefik.http.routers.api.rule=Host(`api.truckwash.dk`)" + - "traefik.http.routers.api.entrypoints=websecure" + - "traefik.http.routers.api.tls=true" + - "traefik.http.routers.api.tls.domains[0].main=api.truckwash.dk" + - "traefik.http.routers.api.tls.certresolver=le" + - "traefik.http.routers.api.service=caddy" + - "traefik.http.routers.api.middlewares=secure-headers@file,api-ratelimit@file" + - "traefik.http.routers.api-io.rule=Host(`api.truckwash.io`)" + - "traefik.http.routers.api-io.entrypoints=websecure" + - "traefik.http.routers.api-io.tls=true" + - "traefik.http.routers.api-io.tls.domains[0].main=api.truckwash.io" + - "traefik.http.routers.api-io.tls.certresolver=le_io" + - "traefik.http.routers.api-io.service=caddy" + - "traefik.http.routers.api-io.middlewares=secure-headers@file,api-ratelimit@file" + - "traefik.http.routers.api-http.rule=Host(`api.truckwash.dk`) || Host(`api.truckwash.io`)" + - "traefik.http.routers.api-http.entrypoints=web" + - "traefik.http.routers.api-http.middlewares=redirect-to-https@file" + - "traefik.http.routers.api-http.service=caddy" + - "traefik.http.routers.local.rule=Host(`localhost`)" + - "traefik.http.routers.local.entrypoints=web" + - "traefik.http.routers.local.service=caddy" + - "traefik.http.routers.local.middlewares=secure-headers@file" + - "traefik.http.routers.local-secure.rule=Host(`localhost`)" + - "traefik.http.routers.local-secure.entrypoints=websecure" + - "traefik.http.routers.local-secure.tls=true" + - "traefik.http.routers.local-secure.service=caddy" + - "traefik.http.routers.local-secure.middlewares=secure-headers@file" + - "traefik.http.routers.local-api.rule=Host(`localhost`) && PathPrefix(`/api`)" + - "traefik.http.routers.local-api.entrypoints=web" + - "traefik.http.routers.local-api.middlewares=strip-api-prefix@file,secure-headers@file" + - "traefik.http.routers.local-api.service=caddy" + - "traefik.http.routers.local-api.priority=100" + - "traefik.http.routers.local-api-secure.rule=Host(`localhost`) && PathPrefix(`/api`)" + - "traefik.http.routers.local-api-secure.entrypoints=websecure" + - "traefik.http.routers.local-api-secure.tls=true" + - "traefik.http.routers.local-api-secure.middlewares=strip-api-prefix@file,secure-headers@file" + - "traefik.http.routers.local-api-secure.service=caddy" + - "traefik.http.routers.local-api-secure.priority=100" + - "traefik.http.services.caddy.loadbalancer.server.port=80" + + caddy-staging: + image: caddy:2.7.6-alpine + container_name: caddy-staging + depends_on: + - php-staging + volumes: + - ./services/nginx/staging:/var/www/html + - ./services/caddy/Caddyfile-staging:/etc/caddy/Caddyfile:ro + - ./services/caddy/logs-staging:/var/log/caddy + labels: + - "traefik.enable=true" + - "traefik.http.routers.api-staging.rule=Host(`api.truckwash.io`)" + - "traefik.http.routers.api-staging.entrypoints=websecure-staging" + - "traefik.http.routers.api-staging.tls=true" + - "traefik.http.routers.api-staging.tls.domains[0].main=api.truckwash.io" + - "traefik.http.routers.api-staging.tls.certresolver=le_io" + - "traefik.http.routers.api-staging.service=caddy-staging" + - "traefik.http.routers.api-staging.middlewares=secure-headers@file,api-ratelimit@file" + - "traefik.http.routers.local-staging.rule=Host(`localhost`)" + - "traefik.http.routers.local-staging.entrypoints=websecure-staging" + - "traefik.http.routers.local-staging.service=caddy-staging" + - "traefik.http.routers.local-staging.middlewares=secure-headers@file" + - "traefik.http.routers.local-staging-secure.rule=Host(`localhost`)" + - "traefik.http.routers.local-staging-secure.entrypoints=websecure-staging" + - "traefik.http.routers.local-staging-secure.tls=true" + - "traefik.http.routers.local-staging-secure.service=caddy-staging" + - "traefik.http.routers.local-staging-secure.middlewares=secure-headers@file" + - "traefik.http.routers.local-staging-api.rule=Host(`localhost`) && PathPrefix(`/api`)" + - "traefik.http.routers.local-staging-api.entrypoints=websecure-staging" + - "traefik.http.routers.local-staging-api.middlewares=strip-api-prefix@file,secure-headers@file" + - "traefik.http.routers.local-staging-api.service=caddy-staging" + - "traefik.http.routers.local-staging-api.priority=100" + - "traefik.http.routers.local-staging-api-secure.rule=Host(`localhost`) && PathPrefix(`/api`)" + - "traefik.http.routers.local-staging-api-secure.entrypoints=websecure-staging" + - "traefik.http.routers.local-staging-api-secure.tls=true" + - "traefik.http.routers.local-staging-api-secure.middlewares=strip-api-prefix@file,secure-headers@file" + - "traefik.http.routers.local-staging-api-secure.service=caddy-staging" + - "traefik.http.routers.local-staging-api-secure.priority=100" + - "traefik.http.services.caddy-staging.loadbalancer.server.port=80" + + php1: + build: + context: . + dockerfile: services/php/Dockerfile + container_name: php1 + depends_on: + - redis + - edge-broker + command: ["php-fpm"] + env_file: + - .env + environment: + AUTO_COMPOSER_INSTALL: "true" + EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + volumes: + - ./services/nginx/app:/var/www/html + - ./services/edge-agent/dist:/services/edge-agent/dist:ro + - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro + - ./services/php/logs:/var/log/php + + php2: + build: + context: . + dockerfile: services/php/Dockerfile + container_name: php2 + depends_on: + - redis + - edge-broker + command: ["php-fpm"] + env_file: + - .env + environment: + AUTO_COMPOSER_INSTALL: "false" + EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + volumes: + - ./services/nginx/app:/var/www/html + - ./services/edge-agent/dist:/services/edge-agent/dist:ro + - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro + - ./services/php/logs:/var/log/php + + php3: + build: + context: . + dockerfile: services/php/Dockerfile + container_name: php3 + depends_on: + - redis + - edge-broker + command: ["php-fpm"] + env_file: + - .env + environment: + AUTO_COMPOSER_INSTALL: "false" + EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + volumes: + - ./services/nginx/app:/var/www/html + - ./services/edge-agent/dist:/services/edge-agent/dist:ro + - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro + - ./services/php/logs:/var/log/php + + php4: + build: + context: . + dockerfile: services/php/Dockerfile + container_name: php4 + depends_on: + - redis + - edge-broker + command: ["php-fpm"] + env_file: + - .env + environment: + AUTO_COMPOSER_INSTALL: "false" + EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + volumes: + - ./services/nginx/app:/var/www/html + - ./services/edge-agent/dist:/services/edge-agent/dist:ro + - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro + - ./services/php/logs:/var/log/php + + php5: + build: + context: . + dockerfile: services/php/Dockerfile + container_name: php5 + depends_on: + - redis + - edge-broker + command: ["php-fpm"] + env_file: + - .env + environment: + AUTO_COMPOSER_INSTALL: "false" + EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + volumes: + - ./services/nginx/app:/var/www/html + - ./services/edge-agent/dist:/services/edge-agent/dist:ro + - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro + - ./services/php/logs:/var/log/php + + php-staging: + build: + context: . + dockerfile: services/php/Dockerfile + container_name: php-staging + depends_on: + - redis-staging + - edge-broker + command: ["php-fpm"] + env_file: + - .env.staging + environment: + AUTO_COMPOSER_INSTALL: "false" + EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + volumes: + - ./services/nginx/staging:/var/www/html + - ./services/edge-agent/dist:/services/edge-agent/dist:ro + - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro + - ./services/php/logs-staging:/var/log/php + + php-cron: + build: + context: . + dockerfile: services/php/Dockerfile + container_name: php-cron + depends_on: + - redis + - edge-broker + command: ["sh", "-c", "while true; do php index.php run cron; sleep 60; done"] + env_file: + - .env + environment: + AUTO_COMPOSER_INSTALL: "false" + EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} + EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + volumes: + - ./services/nginx/app:/var/www/html + - ./services/edge-agent/dist:/services/edge-agent/dist:ro + - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro + - ./services/php/logs:/var/log/php + + portainer: + image: portainer/portainer-ce:2.21.4 + container_name: portainer + profiles: + - dev + ports: + - "9443:9443" + - "9000:9000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data + + jaeger: + image: jaegertracing/all-in-one:1.53 + container_name: jaeger + profiles: + - dev + environment: + - COLLECTOR_ZIPKIN_HTTP_PORT=9411 + ports: + - "16686:16686" + + n8n: + image: n8nio/n8n:latest + container_name: n8n + restart: always + environment: + - N8N_HOST=n8n.truckwash.io + - N8N_PORT=5678 + - N8N_PROTOCOL=https + - NODE_ENV=production + - WEBHOOK_URL=https://n8n.truckwash.io/ + - GENERIC_TIMEZONE=${CONFIG_TIMEZONE:-Europe/Copenhagen} + volumes: + - n8n_data:/home/node/.n8n + labels: + - "traefik.enable=true" + - "traefik.http.routers.n8n.rule=Host(`n8n.truckwash.io`)" + - "traefik.http.routers.n8n.entrypoints=websecure" + - "traefik.http.routers.n8n.tls=true" + - "traefik.http.routers.n8n.tls.certresolver=le_io" + - "traefik.http.routers.n8n.service=n8n" + - "traefik.http.routers.n8n-http.rule=Host(`n8n.truckwash.io`)" + - "traefik.http.routers.n8n-http.entrypoints=web" + - "traefik.http.routers.n8n-http.middlewares=redirect-to-https@file" + - "traefik.http.routers.n8n-http.service=n8n" + - "traefik.http.services.n8n.loadbalancer.server.port=5678" + +volumes: + db_data: + db_debug_data: + nnks_redis: + nnks_redis_staging: + es_data: + portainer_data: + fleet-server-data: + elastic-agent-data: + n8n_data: diff --git a/docker-compose.yml b/docker-compose.yml index 3f53f96c..dbf9dd49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,8 +87,43 @@ services: container_name: edge-broker environment: EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} - ports: - - "4300:4300" + labels: + - "traefik.enable=true" + - "traefik.http.routers.edge-broker-api.rule=Host(`api.truckwash.dk`) && PathPrefix(`/edge-broker`)" + - "traefik.http.routers.edge-broker-api.entrypoints=websecure" + - "traefik.http.routers.edge-broker-api.tls=true" + - "traefik.http.routers.edge-broker-api.tls.certresolver=le" + - "traefik.http.routers.edge-broker-api.middlewares=secure-headers@file,edge-broker-strip" + - "traefik.http.routers.edge-broker-api.service=edge-broker" + - "traefik.http.routers.edge-broker-api-io.rule=Host(`api.truckwash.io`) && PathPrefix(`/edge-broker`)" + - "traefik.http.routers.edge-broker-api-io.entrypoints=websecure" + - "traefik.http.routers.edge-broker-api-io.tls=true" + - "traefik.http.routers.edge-broker-api-io.tls.certresolver=le_io" + - "traefik.http.routers.edge-broker-api-io.middlewares=secure-headers@file,edge-broker-strip" + - "traefik.http.routers.edge-broker-api-io.service=edge-broker" + - "traefik.http.routers.edge-broker-api-staging.rule=Host(`api.truckwash.io`) && PathPrefix(`/edge-broker`)" + - "traefik.http.routers.edge-broker-api-staging.entrypoints=websecure-staging" + - "traefik.http.routers.edge-broker-api-staging.tls=true" + - "traefik.http.routers.edge-broker-api-staging.tls.certresolver=le_io" + - "traefik.http.routers.edge-broker-api-staging.middlewares=secure-headers@file,edge-broker-strip" + - "traefik.http.routers.edge-broker-api-staging.service=edge-broker" + - "traefik.http.routers.edge-broker-local.rule=Host(`localhost`) && PathPrefix(`/api/edge-broker`)" + - "traefik.http.routers.edge-broker-local.entrypoints=web" + - "traefik.http.routers.edge-broker-local.middlewares=secure-headers@file,edge-broker-strip-local" + - "traefik.http.routers.edge-broker-local.service=edge-broker" + - "traefik.http.routers.edge-broker-local-secure.rule=Host(`localhost`) && PathPrefix(`/api/edge-broker`)" + - "traefik.http.routers.edge-broker-local-secure.entrypoints=websecure" + - "traefik.http.routers.edge-broker-local-secure.tls=true" + - "traefik.http.routers.edge-broker-local-secure.middlewares=secure-headers@file,edge-broker-strip-local" + - "traefik.http.routers.edge-broker-local-secure.service=edge-broker" + - "traefik.http.routers.edge-broker-local-staging.rule=Host(`localhost`) && PathPrefix(`/api/edge-broker`)" + - "traefik.http.routers.edge-broker-local-staging.entrypoints=websecure-staging" + - "traefik.http.routers.edge-broker-local-staging.tls=true" + - "traefik.http.routers.edge-broker-local-staging.middlewares=secure-headers@file,edge-broker-strip-local" + - "traefik.http.routers.edge-broker-local-staging.service=edge-broker" + - "traefik.http.middlewares.edge-broker-strip.stripPrefix.prefixes=/edge-broker" + - "traefik.http.middlewares.edge-broker-strip-local.stripPrefix.prefixes=/api/edge-broker" + - "traefik.http.services.edge-broker.loadbalancer.server.port=4300" caddy: image: caddy:2.7.6-alpine diff --git a/services/edge-broker/test/config.test.mjs b/services/edge-broker/test/config.test.mjs index 4edf5f26..6bb58c1a 100644 --- a/services/edge-broker/test/config.test.mjs +++ b/services/edge-broker/test/config.test.mjs @@ -15,6 +15,7 @@ function readRequiredSource(...pathSegments) { const baseComposeSource = readRequiredSource("docker-compose.yml"); const exampleComposeSource = readRequiredSource("docker-compose.example.yml"); +const standaloneProdComposeSource = readRequiredSource("docker-compose.prod.standalone.yml"); const traefikSource = [ readRequiredSource("services", "traefik", "traefik.yml"), readRequiredSource("services", "traefik", "traefik.prod.yml"), @@ -35,14 +36,32 @@ test("traefik does not expose a dedicated public edge broker port", () => { assert.doesNotMatch(traefikSource, /edge-broker:\s*\n\s*address:\s*":4300"/); }); -test("base docker compose exposes the edge broker service on port 4300", () => { - assert.match(baseComposeSource, /\bedge-broker:\b/); - assert.match(baseComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "4300:4300"/); +test("base docker compose routes edge broker traffic through traefik", () => { + const serviceBlock = readComposeServiceBlock(baseComposeSource, "edge-broker"); + assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.truckwash\.dk`\) && PathPrefix\(`\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api-io\.rule=Host\(`api\.truckwash\.io`\) && PathPrefix\(`\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.rule=Host\(`localhost`\) && PathPrefix\(`\/api\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.middlewares\.edge-broker-strip\.stripPrefix\.prefixes=\/edge-broker/); + assert.match(serviceBlock, /traefik\.http\.middlewares\.edge-broker-strip-local\.stripPrefix\.prefixes=\/api\/edge-broker/); + assert.match(serviceBlock, /traefik\.http\.services\.edge-broker\.loadbalancer\.server\.port=4300/); }); -test("example docker compose exposes the edge broker service on port 4300", () => { - assert.match(exampleComposeSource, /\bedge-broker:\b/); - assert.match(exampleComposeSource, /edge-broker:\s*\n[\s\S]*?\n\s+ports:\s*\n\s+- "4300:4300"/); +test("example docker compose routes edge broker traffic through traefik", () => { + const serviceBlock = readComposeServiceBlock(exampleComposeSource, "edge-broker"); + assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.example\.com`\) && PathPrefix\(`\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.rule=Host\(`localhost`\) && PathPrefix\(`\/api\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.services\.edge-broker\.loadbalancer\.server\.port=4300/); +}); + +test("standalone production compose routes edge broker traffic through traefik", () => { + const serviceBlock = readComposeServiceBlock(standaloneProdComposeSource, "edge-broker"); + assert.doesNotMatch(serviceBlock, /\n\s+ports:\s*\n[\s\S]*?\n\s+- "4300:4300"/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api\.rule=Host\(`api\.truckwash\.dk`\) && PathPrefix\(`\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-api-io\.rule=Host\(`api\.truckwash\.io`\) && PathPrefix\(`\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.routers\.edge-broker-local\.rule=Host\(`localhost`\) && PathPrefix\(`\/api\/edge-broker`\)/); + assert.match(serviceBlock, /traefik\.http\.services\.edge-broker\.loadbalancer\.server\.port=4300/); }); test("php services receive broker websocket environment defaults", () => { diff --git a/services/nginx/app/.phpunit.cache/test-results b/services/nginx/app/.phpunit.cache/test-results index 019ff3c6..63b6610c 100644 --- a/services/nginx/app/.phpunit.cache/test-results +++ b/services/nginx/app/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":"pest_3.8.6","defects":{"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":1,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":7,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":7,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":8,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":1,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":1,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":1,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":1,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":7,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":8,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":1,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":1,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":8,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":1,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":1,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":8,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":7,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":7,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":7,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":7,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":8,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_PHP_agent_artifacts_and_management_polling_config":8,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__and_operation_endpoints_without_shell_transport_wiring":8,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_the_source_tree_layout":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_mounted_and_baked_in_artifact_directories_before_repo_fallbacks":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_reassigns_invoice_collections_when_changing_an_order_across_the_draft_customer_boundary":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_when_certificate_metadata_changes_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_for_legacy_field_value_updates_through_the_alias_endpoint":7,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":1,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":1,"P\\Tests\\Api\\ApiFixturesCleanupTest::__pest_evaluable_it_removes_generated_customer_traces_during_fixture_cleanup":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_includes_economic_runtime_config_for_uncached_auth_sessions":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":1,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_the_raw_202_gather_webhook_contract_over_HTTP":1,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_still_prompts_for_department_selection_when_only_one_department_is_eligible":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_accepts_the_legacy_initial_webhook_body_with_top_level_call_identifiers":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_a_raw_400_transport_error_for_malformed_webhook_payloads":1,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_lists_the_draft_customer_config_entry_in_economic_config_responses":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_round_trips_the_draft_customer_config_value_through_economic_config_updates":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_order_draft_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_booked_invoice_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_collected_invoice_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\PingApiTest::__pest_evaluable_it_returns_the_ping_contract":1,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_sends_a_Stripe_invoice_by_email_and_persists_the_hosted_invoice_association":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_conflict_when_a_Stripe_hosted_invoice_is_already_active_for_the_order":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_allows_sending_a_new_Stripe_hosted_invoice_when_the_existing_association_is_already_terminal":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_voids_an_unpaid_Stripe_hosted_invoice_and_clears_the_local_order_association":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_refuses_to_cancel_a_paid_Stripe_hosted_invoice":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":8,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":7,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_v2_operator_facing_edge_gateway_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":7},"times":{"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_creates_a_64_char_auth_token_for_an_existing_user":0.11,"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_throws_a_clear_exception_when_customer_user_is_missing":0.017,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_passkey_challenge_legacy_script_green_during_migration":0.028,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_register_CVR_legacy_script_green_during_migration":0.029,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_deterministic_department_digit_map_and_caps_to_9_options":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_department_ids_by_selected_digit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_gate_type_digit_to_entrance_or_exit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_stores__loads_and_clears_ivr_state_with_ttl":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_validates_state_caller_fingerprint_matching":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_department_and_gate_prompts":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_creates_permission_nodes_linked_to_subuser_permission_keys":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_rejects_empty_permission_definitions":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_button_arrays__json_and_csv_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_button_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_vehicle_type_values_and_null_semantics":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_vehicle_type_values":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_non_numeric_vehicle_type_strings":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_keeps_lane_service_enum_contract_for_MACHINE":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_booking_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_selfserve_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubusersRoutePermissionLinkTest::__pest_evaluable_it_keeps_subusers_route_list_permission_linked_to_SUBUSERS__LIST_node":0.005,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_keeps_route_and_service_entity_type_registries_in_sync_with_expanded_coverage":0,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":0.009,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_wires_local_e_conomic_customer_index_into_customer_related_search_entities":0.003,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_registers_cron_tasks_that_keep_the_e_conomic_search_index_refreshed":0.003,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_generic_db_object_mutation_flows":0.004,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_object_property_mutations":0.003,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_when_module_config_values_change":0.002,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_registers_cron_maintenance_task_for_system_search_cache":0.002,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_redacts_obvious_sensitive_fragments_before_sending_query_to_intent_parser":0.001,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_builds_payload_with_redacted_query_and_parses_strict_JSON_output":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_falls_back_safely_when_OpenAI_is_disabled":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_handles_malformed_OpenAI_response_payloads_without_throwing":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_uses_parser_cache_for_identical_query_and_allowed_type_combinations":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_caps_alias_and_hint_payloads_from_OpenAI_and_filters_hints_to_allowed_types":0.006,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":0.007,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":0.007,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":0.005,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_normalizes_type_lists_from_csv_json_and_arrays":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_parses_booleans_and_clamps_integers_using_route_defaults":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_exposes_expected_searchable_entity_types":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_system_wide_search_GET_and_POST_endpoints_with_intent_debug_support":0.003,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_superuser_cache_clear_and_rebuild_endpoints_for_system_search":0.003,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_passes_permission_and_own_scope_context_into_system_search_service":0.003,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_invoke_intent_parser_when_lexical_confidence_is_already_high":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_on_low_confidence_lexical_results_and_applies_hints_only_boosts":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_lets_parser_entity_hints_override_explicit_include_filters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_returns_fallback_parser_metadata_when_parser_fails_gracefully":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_scopes_query_cache_by_permission_context_to_avoid_cross_user_cache_leakage":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_caps_AI_driven_expanded_terms_to_prevent_query_amplification":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_expands_danish_discount_wording_into_lexical_discount_synonyms":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_for_intent_driven_natural_language_queries_even_when_lexical_score_is_high":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_association_hints_to_pull_related_customer_records_from_non_customer_matches":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_prefers_newer_records_when_relevance_scores_are_comparable":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_keeps_explicit_identifier_matches_ahead_of_newer_but_weaker_records":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_promotes_invoices_orders_order_bookings_and_customers_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_prioritizes_cancelled_bookings_over_active_bookings":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_heavily_demotes_configured_low_priority_entity_types_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_tokenizes_unicode_names_without_stripping_non_ascii_letters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_treat_explicit_identifier_queries_as_intent_driven":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_requires_broader_term_coverage_for_multi_word_scoring":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_enriches_object_attachment_results_with_associated_customer_context":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_includes_the_economic_customer_index_in_cache_dependencies_for_customer_scoped_results":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_falls_back_to_invoice_date_ranges_when_invoice_names_are_missing":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_derives_xlvask_customer_numbers_only_from_digits_only_extern_ids":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_replaces_unnamed_user_titles_with_the_customer_context_name":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_the_goal_criteria_label_for_department_goal_titles":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_reuses_parser_cache_entries_for_repeated_natural_language_intent_requests":0.106,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_clears_parser_cache_namespace_via_clearAll_to_force_a_fresh_parse":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_bumps_per_table_cache_versions_when_a_dirty_table_marker_is_registered":0.081,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_fixed_pricing_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_wash_subscriptions_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_registers_economic_v2_backfill_command_in_cli_dispatcher":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_provides_a_backfill_cron_script_entrypoint":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_exact__match_when_totals_lines_and_departments_are_identical":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_total__mismatch_when_only_totals_differ":0.015,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_line_level_mismatches_for_quantity_and_price":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_departmental_distribution_mismatches":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_missing__target_when_draft_or_booked_target_is_unavailable":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_draft_invoice_lines_including_departmental_distributions":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_marks_text_only_zero_value_lines_as_non_billable_and_keeps_deterministic_key":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_booked_invoices_and_computes_net_total_delta_from_lines":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_contains_internal_normalization_path_with_departmental_metadata_support":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_supports_barred_filtering_when_listing_e_conomic_customers":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_implements_a_dedicated_v2_e_conomic_revenue_statistics_service_and_route":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_all_collected_invoice_economic_v2_routes_with_explicit_permissions":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_version_aware_distribution_and_pricing_history_v2_routes":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_caches_v2_distribution_responses_with_a_configurable_redis_ttl":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_fixed_pricing_versions_from_create_and_delete_flows":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_vehicle_subscription_versions_for_create_update_delete_flows":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_discount_override_versions_from_superuser_discounts_route":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_keeps_legacy_compare_endpoint_path_for_backward_compatibility":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_implements_effective_range_lifecycle_methods_for_all_versioned_entities":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_closes_previous_active_interval_before_inserting_a_new_version":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_includes_best_effort_backfill_with_provenance_and_confidence_metadata":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_anchors_historical_resolution_on_order_created__at_timestamps_in_distribution_service":0.004,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_order_ids_in_net_amount_calculation":0.007,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_customer_arrays_in_date_range_transaction_fetches":0.003,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_uses_centralized_duplicate_filtering_for_possible_duplicate_detection":0.003,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_per_scope_and_date_range":0.007,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_requires_superuser_permission_for_invoicing_period_distribution_all_endpoint":0.017,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_uses_shared_date_range_normalization_across_invoicing_period_endpoints":0.032,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_normalizes_a_valid_invoicing_date_range_to_full_day_timestamps":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_invalid_date_formats":0.012,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_descending_date_ranges":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_finds_duplicate_orders_regardless_of_original_input_order":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_runs_best_effort_backfill_when_fixed_pricing_version_history_is_empty":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_self_serve_conditions_with_combined_AND_and_OR_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_prefers_condition_results_over_question_answers_when_evaluating_task_gates":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":0.01,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_machine_types__eligibility__summaries__and_machine_start_webhook_routes":0.014,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_machine_type_support_wired_into_lanes__tasks__and_conditions_routes":0.018,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_normalizes_webhook_methods_and_falls_back_to_POST_for_unsupported_verbs":0,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_n8n_query_keys":0,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_workflow_lifecycle_endpoints_for_the_n8n_module":0.002,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_execution_and_webhook_trigger_endpoints_for_the_n8n_module":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":0,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_encodes_raw_filter_query_values_that_include_timestamps":0,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_avoids_double_encoding_query_values_that_are_already_escaped":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":0,"P\\Tests\\Unit\\Bookings\\OrderBookingRouteIdempotencyGuardTest::__pest_evaluable_it_guards_order_booking_creation_with_redis_backed_idempotency":0.24,"P\\Tests\\Unit\\DynamicImages\\DynamicImagePreRenderCronWiringTest::__pest_evaluable_it_registers_dynamic_image_pre_render_cron_task_and_related_helpers":0.005,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":0.226,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":0.017,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":0.014,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":0.011,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":0.016,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":0.013,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_both_own_and_elevated_permissions_when_both_are_missing":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_only_elevated_permission_when_own_permission_exists_but_own_context_fails":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_allows_elevated_permission_path_without_forbidden_and_validates_department_access":0,"P\\Tests\\Unit\\Permissions\\ForbiddenResponseWiringTest::__pest_evaluable_it_routes_and_trait_use_forbidden_responses_for_permission_and_ownership_denials":0.018,"P\\Tests\\Unit\\Permissions\\GroupPermissionSessionInvalidationWiringTest::__pest_evaluable_it_invalidates_related_user_permission_and_auth_session_caches_when_group_permissions_change":0.004,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_snake__case_fields":0.066,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_camelCase_aliases":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_normalizes_ENTIRE__DURATION_to_ignore_target__duration__every":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_defaults_recurring_target__duration__every_to_1_when_omitted":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_keeps_strict_legacy_mode_when_target__duration_is_invalid":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_computes_weekly_target_using_touched_ISO_weeks_for_March_2026":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_applies_target__duration__every_for_weekly_cadence":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_prorates_ENTIRE__DURATION_target_by_overlap_days":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_splits_advanced_target_by_override_weights_per_department":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_preserves_legacy_target_behavior_when_target__duration_is_missing":0,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_monthly_renderer_script_green":0.147,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_department_daily_renderer_script_green":0.147,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":0.007,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_uses_company_scoped_workfeed_endpoints_and_raw_Authorization_header":0.003,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_normalizes_documented_shift_query_parameters":0.001,"P\\Tests\\Unit\\Workfeed\\WorkfeedConfigRouteWiringTest::__pest_evaluable_it_registers_module_config_endpoints_for_workfeed":0.004,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_workfeed_query_keys":0,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_employee_and_department_endpoints_for_the_workfeed_module":0.004,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_shift_endpoints_for_the_workfeed_module":0.002,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_for_the_hour_slot_based_on_overlap":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_extracts_department_id_from_supported_workfeed_shift_shapes":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_wrapped_workfeed_collections_from_common_response_keys":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_includes_a_date_key_in_department_weather_timeline_entries":0.003,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_from_start_of_yesterday_to_end_of_today":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_an_explicit_selected_date_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_across_multiple_departments_for_one_hour_slot":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_department_id_input_from_scalar_csv_and_nested_array_values":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_explicit_date__from_and_date__to_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_resolves_forecast_day_count_for_a_given_timeline_range_with_sane_limits":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_resolves_weather_coordinates_from_valid_coordinates_only":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_null_weather_coordinates_when_all_department_locations_are_invalid":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_classifies_weatherapi_no_matching_location_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_does_not_classify_non_location_weatherapi_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_an_empty_forecast_fallback_when_coordinates_are_unavailable":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_for_department_sets_and_timeline_ranges":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_order_insensitive_cache_keys_for_equivalent_department_id_sets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_marks_non_started_slots_as_unknown_regardless_of_wash_hour_ratio":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherPreloadCronWiringTest::__pest_evaluable_it_registers_and_implements_cron_preloading_for_department_weather_cache":0.007,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_stale_ttl_defaults_and_clamps_stale_ttl_below_fresh_ttl":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_hot_activity_ttl_defaults_and_clamps_to_a_positive_value":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_fresh_cached_payloads_without_invoking_the_resolver":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_stale_cached_payloads_and_enqueues_a_refresh_signal":0.01,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_recomputes_and_rewrites_cache_payloads_when_stale_window_is_exceeded":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_records_hot_keys_order_insensitively_and_applies_preload_target_caps":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_batched_wash_count_rows_into_hourly_slot_totals":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_wires_department_weather_route_to_use_batched_wash_aggregation":0.007,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":0,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_ensures_economic_module_rows_in_one_sanitized_batch":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_returns_id_keyed_economic_module_payload_with_null_defaults":0,"P\\Tests\\Unit\\Orders\\OrdersRouteListBatchingWiringTest::__pest_evaluable_it_wires_GET__orders_through_batched_enrichment_and_stripe_snapshot_caching":0.006,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_cashier_names_from_cache_and_fetches_missing_ones_in_batch":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_deterministic_map_for_duplicate_and_unsorted_cashier_ids":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_implements_batched_cashier_name_lookup_with_cache_first_fallback_behavior":0.006,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_sanitizes_and_deduplicates_cashier_ids_before_batch_lookup":0.003,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_overtime_minutes_when_a_shift_carries_an_extended_approval_end_timestamp":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_unapproved_overtime_from_a_bounded_shift_updateTime_fallback":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_does_not_treat_late_unapproved_administrative_edits_as_overtime":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_changes_weather_timeline_cache_key_when_department_weather_targets_change":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_evaluates_healthy_degraded_and_unhealthy_statuses_from_configured_department_targets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_returns_unknown_when_configured_targets_are_missing_for_department_status_evaluation":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":0.046,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":0.006,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_registers_department_weather_target_routes_with_explicit_read_and_manage_permissions":0.004,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_persists_department_weather_targets_using_canonical_department_variable_keys":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_relay_status_get_and_set_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_batches_sequential_machine_relay_status_requests_into_a_single_Shelly_get_call":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_retries_missing_Shelly_status_payloads_until_relay_status_becomes_ready":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_times_out_with_a_clear_Shelly_readiness_error_when_payload_remains_missing":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_direct_set_switch_and_seeds_cache_so_immediate_status_read_does_not_call_Shelly_get":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_lane_gate_open_endpoint":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_in_progress_self_serve_wash_details_endpoint":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":0.132,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":0.001,"P\\Tests\\Unit\\Selfserve\\DepartmentSelfServeEnabledRelaySyncWiringTest::__pest_evaluable_it_syncs_lane_relay_states_when_department_self_serve_enabled_flag_changes":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_supports_hard_relay_set_even_when_lane_status_is_CLOSED":0,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_enables_cleaner_relay_on_wash_start_command_and_webhook_flow":0.012,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_relay_off_when_a_self_serve_wash_session_is_completed":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_exit_port_by_switching_relay__in__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_entrance_port_by_switching_relay__out__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_typed_task_gates_with_strict_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_validates_typed_task_gates_for_known_references":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_fails_validation_when_typed_task_gates_reference_unknown_entities":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_config_draft_publish_rollback_lifecycle_endpoints":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_legacy_self_serve_CRUD_routes_syncing_canonical_drafts":0.013,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_keeps_cleaner_relay_enable_wired_into_machine_relay_start_paths":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":0.277,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_encodes_config_json_payloads_with_apostrophes_before_persistence":0,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_and_cleaner_relays_off_when_a_self_serve_wash_session_is_completed":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_vehicle_type_override_into_self_serve_preview_and_synchronization_routes":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_local_demo_responses_and_skips_Shelly_cloud_calls_for_demo_relay_ids":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_default_OFF_status_for_demo_relay_ids_without_Shelly_lookups":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_excludes_demo_relay_ids_from_Shelly_status_batch_payloads":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_keeps_demo_relay_gate_open_queued_but_skips_Shelly_switch_calls":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_allowed_services_route_through_machine_relay_visibility_sync":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enables_MACHINE_relay_when_MACHINE_is_visible_in_allowed_services":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_turns_MACHINE_relay_off_immediately_when_MACHINE_is_not_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_does_not_enable_MACHINE_relay_when_allowEnable_is_false_even_if_MACHINE_is_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_is_a_safe_no_op_when_MACHINE_relay_is_not_configured":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_synchronizes_machine_relay_from_visible_services_during_session_synchronization":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_adds_explicit_relay_disable_session_helper_for_visibility_driven_OFF_transitions":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_property_gate_command_permissions":0.008,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneCommandEnumTest::__pest_evaluable_it_parses_property_gate_lane_commands":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_entrance_gate_for_OPEN__PROPERTY__ACCESS__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_exit_gate_for_OPEN__PROPERTY__EXIT__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_blocks_property_gate_commands_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_throws_a_clear_error_when_entrance_gate_is_missing_for_property_access_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_access_command_failures":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashFlowMachineAllowedWiringTest::__pest_evaluable_it_reads_machine_allowed_state_from_session_summary_payload_safely":0.005,"P\\Tests\\Unit\\Selfserve\\DbObjectPaginationLegacyColumnCompatibilityTest::__pest_evaluable_it_guards_pagination_against_searchable_fields_missing_from_legacy_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_dynamic__images__vehicle__type_column_for_legacy_selfserve_wash_session_task_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_billable_minutes_when_elapsed_minutes_exceed_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_elapsed_minutes_equal_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_included_minutes_exceed_elapsed_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_handles_zero_and_low_elapsed_wash_time_edge_cases":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_wash_included_minutes_into_self_serve_module_config":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_blocks_explicit_relay_writes_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_disabled_no_op_from_machine_relay_visibility_sync_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":0.007,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":0.019,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":0.072,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":0.013,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_exit_command_failures":0.002,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_caller_id_is_not_confirmed":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_voice_call_facade_methods_to_documented_endpoints_and_methods":0.039,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_recordings_insights_log_and_flash_methods_to_documented_resources":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_encodes_path_segments_before_building_outbound_endpoints":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_serializes_query_parameters_for_GET_transport_requests":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_4xx_and_5xx_transport_failures_into_informative_exceptions":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":0.005,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":0.004,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_accepts_valid_payloads_for_create_and_nested_call_flow_commands":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_rejects_unknown_fields_and_invalid_enum_values_in_strict_schemas":0.014,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_supports_CSV_normalization_in_schema_validation_for_log_filters":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_validates_flash_hangup_payloads_for_both_supported_request_shapes":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_fails_before_forward_step_when_strict_validation_fails_and_forwards_when_valid":0,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_full_Bird_voice_call_route_surface_with_strict_validation_and_permissions":0.005,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_flash_hangup_endpoint_and_keeps_end_alias_wired_as_compatibility_path":0.004,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":0.006,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":0.005,"P\\Tests\\Unit\\Selfserve\\DepartmentGatesRelaysRouteWiringTest::__pest_evaluable_it_validates_department_gate_config_on_both_create_and_update_routes":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_wash__started__at_column_for_legacy_selfserve_wash_session_schemas":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_stores_wash__started__at_in_self_serve_wash_sessions_when_machine_start_is_triggered":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_passes_lane_wash_start_time_into_the_session_machine_start_marker":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_resolves_in_progress_wash__started__at_from_session_with_compatibility_fallbacks":0.003,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_say_payload_and_applies_hangup_default":0.019,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_log_query_csv_and_integer_fields":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_nested_create_call_payload_sections_and_drops_unknown_keys":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_keeps_gather_nested_say_payload_unchanged_when_hangup_is_omitted":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_supports_no_body_and_flash_hangup_payload_normalization":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_2_second_Shelly_gate_for_back_to_back_requests":0.023,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enforces_a_2_second_gap_between_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_keeps_back_to_back_get_switch_requests_ordered_through_the_relay_controller":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_executes_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_does_not_print_Shelly_switch_responses_to_output":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_enforces_a_global_2_second_Shelly_gate_in_sendPostRequest":0.004,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_uses_Redis_NX_PX_semantics_for_cross_request_Shelly_rate_limiting":0.003,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_enforces_the_2_second_Shelly_gate_across_separate_request_contexts":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_does_not_delay_when_the_Shelly_gate_is_already_expired":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":0.201,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_relay_is_offline_and_only_disables_configured_relays":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_bills_manual_self_serve_stop_using_full_elapsed_minutes_without_included_minute_reduction":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_keeps_included_minute_reduction_for_automatic_mode":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_on_first_dtmf_input_captured_within_the_300_second_window":0.005,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_send_Slack_when_dtmf_input_repeats_without_change":0.008,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_again_when_dtmf_input_changes_within_the_window":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_gathering_and_does_not_hang_up_before_300_seconds_have_elapsed":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_announces_timeout_waits_10_seconds_and_sends_hangup_once_at_or_after_300_seconds":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_polling_until_terminal_status_and_only_then_finalizes":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_finalizes_immediately_when_webhook_payload_is_already_terminal_before_timeout_logic":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_duplicate_timeout_or_hangup_actions_across_retries_and_lock_contention":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_state_when_polling_fails_so_completion_is_not_falsely_reported":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_dtmf_input_repeats_without_change":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_answers_immediately_once_and_does_not_re_answer_on_subsequent_webhook_retries":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_with_retry_loop_semantics_until_input_is_entered":0.004,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_result_provides_keys_field_instead_of_dtmf":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_conditions_variable_keys_carries_the_entered_value":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_to_check_every_2_seconds_with_retry_loop_semantics_until_input_is_entered":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_ignores_non_numeric_customer__number_values_from_request":0.042,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_casts_numeric_customer__number_strings_to_int_before_lookup":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_prefers_user__id_when_both_user__id_and_customer__number_are_valid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_falls_back_to_customer__number_when_user__id_is_invalid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_normalizes_request_identifiers_before_user_lookups":0.004,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_rejects_non_positive_and_non_digit_request_values":0.003,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_pagination_results_when_present":0.006,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_collection_count_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_returns_zero_when_neither_pagination_nor_collection_is_available":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_counts_object_based_collections_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_uses_collection_when_present":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_falls_back_to_paymentTerms_when_collection_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_supports_top_level_array_payloads":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_returns_an_empty_array_when_no_known_collection_shape_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_economic__endpoint__t_when_explicitly_requested_and_configured":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_economic__endpoint__t_when_grant__2_is_missing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_economic__endpoint__t":0.014,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_economic__endpoint__t":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_legacy_economic__m_when_available":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_legacy_economic__m_when_grant__2_is_missing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_legacy_economic__m":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_legacy_economic__m":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_unfiltered_total_when_no_customer_filters_are_active":0.004,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_search_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_barred_filter_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_treats__null__search_and_barred_values_as_no_filter":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_filtered_total_when_unfiltered_total_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_maps_e_conomic_integration_failures_in_customer_listing_to_explicit_502_responses":0.014,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_logs_LIST__CUSTOMERS_success_only_after_pagination_and_customer_mapping_are_completed":0.009,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_guards_against_malformed_customer_list_payloads_before_calling_paginate":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_endpoint_trait_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_legacy_economic__m_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_returns_raw_payload_unchanged_on_successful_HTTP_statuses":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_accepts_valid_list_payloads_that_include_collection_and_pagination_results":0.02,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_pagination_is_missing_from_the_response_payload":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_upstream_responds_with_an_error_shaped_payload":0,"P\\Tests\\Unit\\Router\\RouterThrowableHandlingTest::__pest_evaluable_it_catches_throwables_during_auto_route_loading_and_maps_them_to_internal_server_errors":0.003,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_does_not_request_e_conomic_customer_data_when_customer_number_is_zero":0.101,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_short_circuits_e_conomic_customer_lookup_for_non_positive_customer_numbers":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_queued_economic_invoice_endpoints_in_economicInvoiceRoute":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_defines_economic_transfer_queue_jobs_schema_bootstrap_table_and_tracking_columns":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_provides_queue_processor_class_constants_and_processing_entrypoint":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_registers_economic_transfer_queue_cron_task_and_handler":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_paths_in_openapi":0.01,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_schemas_in_openapi":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_quantity_for_e_conomic_export":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_price_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_keeps_positive_quantity_and_price_items_billable_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicInvoiceDraftZeroItemSkipWiringTest::__pest_evaluable_it_skips_zero_cost_and_zero_quantity_items_in_legacy_economic_draft_helper":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_malformed_order_item_payloads_for_e_conomic_export_safety":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueHardeningTest::__pest_evaluable_it_hardens_transfer_queue_with_type_validation_retry_caps_and_stale_lock_recovery":0.004,"P\\Tests\\Unit\\Router\\AutoloadRedisCacheValidationTest::__pest_evaluable_it_validates_cached_autoload_paths_before_returning_and_clears_stale_cache_entries":0.003,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_applies_created__at_bounds_directly_in_SQL_when_filtering_orders_by_registration_number":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_rejects_an_inverted_date_range_for_registration_lookups":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_returns_early_when_registration_number_is_blank":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_wires_queue_worker_class_loading_for_CLI_queue_action":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_economic_invoice_queue_endpoints_before_constructing_queue_service":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_collected_invoice_queue_endpoints_before_constructing_queue_service":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_gracefully_handles_unavailable_queue_dependencies_on_collected_invoice_queue_endpoints":0.005,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_the_queue_job_id_in_POST__collected_invoices_economic_enqueue_response":0.204,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_requires_id_and_at_least_one_mutable_field_for_PUT__collected_invoices_in_user_route":0.004,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":0.175,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_queued_contract_for_POST__collected_invoices_stripe_book":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_order_draft_export_route_queue_only":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_collected_invoice_draft_producing_routes_queue_only_in_orderInvoicesRoute":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_collected_invoice_and_stripe_draft_producing_endpoints_before_constructing_queue_service":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_unavailable_queue_dependency_responses_for_async_economic_transfer_endpoints":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_order_payloads_to_strict_positive_integer_ids":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_collected_invoice_payload_booleans_to_strict_bool_values":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_rejects_invalid_transfer_payloads_before_enqueue_write_attempts":0.036,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":0.284,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":0.012,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":0.011,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_guards_on_economic_invoice_queue_status_and_retry_routes":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_queue_status_lifecycle_endpoints":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_order_draft_export_route":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_collected_invoice_draft_producing_routes":0.004,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_economic":0.236,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_stripe_book":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_200_fallback_plus_202_queue_contracts_for_export_endpoints_and_keeps_503_on_queue_lifecycle_endpoints":0.009,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":0.014,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":0,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":0.195,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":0.003,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":0.003,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":0.009,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_marks_queued_transactions_and_clears_requires__action_when_all_actionable_work_is_already_queued":0.826,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_keeps_requires__action_true_when_a_customer_still_has_unqueued_actionable_transactions":0.045,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_blocks_fixed_pricing_or_subscription_customers_with_no_transactions_when_a_relevant_queue_job_exists":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_queues_admin_commands__exposes_agent_poll_result_handlers__and_keeps_heartbeats_from_mutating_discovery_state":0.003,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_defines_the_dispatchable_gateway_guard_on_the_loaded_manager_class":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"recent heartbeat stays online\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"late heartbeat degrades after one minute\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"stale heartbeat goes offline after five minutes\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"explicit offline reports stay offline even when heartbeat is fresh\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"missing heartbeat is treated as offline\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_marks_ready_discovery_as_stale_when_the_gateway_heartbeat_has_expired":0.147,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_edge_gateway_management_REST_endpoints":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_public_installer__claim__heartbeat__command_polling__and_internal_broker_auth_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeBrokerClientConfigTest::__pest_evaluable_it_uses_the_same_default_broker_url_and_shared_secret_fallback_as_the_docker_stack":0.267,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_forwarded_https_scheme_when_proxied":0.336,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_appends_forwarded_ports_when_the_forwarded_host_omits_them":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_infers_https_for_the_staging_api_host_when_only_the_https_port_is_present":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":0.075,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_normalizes_public_broker_overrides_to_https_and_browser_shell_urls_to_wss":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_keeps_localhost_broker_overrides_on_plain_http_and_ws_for_local_development":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_includes_node_pty_build_prerequisites_in_the_generated_install_script":0.007,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured_and_derives_the_broker_from_the_api_origin":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_normalizes_public_broker_overrides_to_https__strips_paths__and_browser_shell_urls_to_wss":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_merges_incoming_heartbeat_metadata_with_existing_gateway_metadata":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_v2_operator_facing_edge_gateway_routes":0.007,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_PHP_edge_agent_routes_for_operations_and_legacy_relay_command_polling":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_PHP_agent_artifacts_and_management_polling_config":0.011,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__and_operation_endpoints_without_shell_transport_wiring":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_keeps_relay_dispatch_and_discovery_queueing_on_the_edge_gateway_manager":0.089,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_loads_relay_command_helpers_on_the_manager_and_gateway_operations_on_the_dedicated_service":0.007,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":0.176,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_the_source_tree_layout":0.445,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_includes_the_container_mounted_artifact_directory_as_a_candidate":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_reads_install_artifacts_through_the_shared_locator":0.017,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_builds_update_payloads_with_checksums_from_resolved_artifact_paths":0.249,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_a_supported_runtime_layout":0.221,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_PHP_agent_artifacts_and_forwarded_https_scheme":0.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_explains_the_legacy_dist_mount_mismatch_when_php_artifacts_are_unavailable":0.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_mounted_and_baked_in_artifact_directories_before_repo_fallbacks":0.022,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_falls_back_to_baked_in_artifacts_when_the_mount_path_is_absent":0.002,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":0.024,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":1.109,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":2.935,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":1.375,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":0.982,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":3.423,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":1.13,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_reassigns_invoice_collections_when_changing_an_order_across_the_draft_customer_boundary":1.615,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":5.592,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":1.29,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":1.517,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":4.136,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":0.512,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":0.965,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":1.045,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":1.64,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_when_certificate_metadata_changes_through_the_primary_endpoint":3.683,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_for_legacy_field_value_updates_through_the_alias_endpoint":3.194,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayOperationLegacySchemaCompatibilityTest::__pest_evaluable_it_keeps_the_legacy_operation__type_column_compatible_with_v2_operation_queueing":0.018,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_builds_deterministic_keys_for_equivalent_contexts":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_stores_and_retrieves_full_order_bookings_payloads":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_ignores_malformed_cached_payloads_and_clears_order_bookings_list_caches":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsRouteCacheWiringTest::__pest_evaluable_it_caches_the_paginated_order_bookings_list_response_and_invalidates_it_on_booking_changes":0.008,"P\\Tests\\Unit\\Bookings\\VehicleSearchBookedMetadataRouteContractTest::__pest_evaluable_it_keeps_booked_vehicle_search_metadata_aligned_with_filtered_pending_order_bookings":0.005,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0.012,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_builds_deterministic_keys_for_equivalent_count_contexts":0.012,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_stores_and_retrieves_normalized_booking_counts":0.014,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_ignores_malformed_payloads_and_clears_order_bookings_count_caches":0.078,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsRouteWiringTest::__pest_evaluable_it_adds_a_cached_order_bookings_counts_endpoint_and_invalidates_it_on_booking_changes":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayFleetUsageStatisticsTest::__pest_evaluable_it_summarizes_fleet_usage_statistics_for_the_dashboard_landing_view":0.001,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":3.168,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":0.753,"P\\Tests\\Api\\ApiFixturesCleanupTest::__pest_evaluable_it_removes_generated_customer_traces_during_fixture_cleanup":8.099,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":4.515,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":1.271,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":0.888,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_includes_economic_runtime_config_for_uncached_auth_sessions":1.349,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":0.698,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":1.365,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":1.932,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":0.393,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_the_raw_202_gather_webhook_contract_over_HTTP":0.788,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_still_prompts_for_department_selection_when_only_one_department_is_eligible":0.732,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_accepts_the_legacy_initial_webhook_body_with_top_level_call_identifiers":0.692,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":1.515,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":1.719,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":0.587,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_a_raw_400_transport_error_for_malformed_webhook_payloads":0.419,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":1.048,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":0.378,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":0.59,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":1.554,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":0.788,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":1.552,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":1.006,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":1.191,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_lists_the_draft_customer_config_entry_in_economic_config_responses":0.947,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_round_trips_the_draft_customer_config_value_through_economic_config_updates":3.28,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_order_draft_exports_for_the_configured_draft_customer":0.86,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_booked_invoice_exports_for_the_configured_draft_customer":1.127,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_collected_invoice_exports_for_the_configured_draft_customer":0.888,"P\\Tests\\Api\\PingApiTest::__pest_evaluable_it_returns_the_ping_contract":0.877,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":1.281,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_sends_a_Stripe_invoice_by_email_and_persists_the_hosted_invoice_association":5.434,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_conflict_when_a_Stripe_hosted_invoice_is_already_active_for_the_order":4.756,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_allows_sending_a_new_Stripe_hosted_invoice_when_the_existing_association_is_already_terminal":3.943,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_voids_an_unpaid_Stripe_hosted_invoice_and_clears_the_local_order_association":3.746,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_refuses_to_cancel_a_paid_Stripe_hosted_invoice":4.131,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":0.726,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":1.326,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":1.159,"P\\Tests\\Unit\\Users\\EconomicCustomerModelParsingTest::__pest_evaluable_it_parses_cached_economic_customer_payloads_that_use_snake__case_customer__number":0.246,"P\\Tests\\Unit\\Users\\EconomicCustomerModelParsingTest::__pest_evaluable_it_leaves_the_economic_customer_model_empty_when_no_valid_customer_number_is_present":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayFleetUsageStatisticsTest::__pest_evaluable_it_derives_fleet_usage_directly_from_cached_gateway_row_summaries":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":0.075,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_adds_a_gateway_metadata_update_endpoint_with_label_and_primary_assignment_fields":0.01,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_reassigns_department_primary_gateways_through_dedicated_manager_helpers":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayShellPollingTransportTest::__pest_evaluable_it_removes_shell_access_from_the_edge_gateway_HTTP_contracts":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":0.041,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_deterministic_cache_keys":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_stores_and_retrieves_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_removes_deleted_gateways_from_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_module_scoped_edge_gateway_operator_routes":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_module_config_endpoints":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_config_endpoints_from_the_module_route_directory":0.015,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":0.022,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_caps_install_session_diagnostics_and_events_while_preserving_the_first_start_timestamp":0.028,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_clears_terminal_failure_details_after_a_successful_claim_update":0.014,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_marks_expired_non_terminal_install_sessions_as_terminal_when_read_back":0}} \ No newline at end of file +{"version":"pest_3.8.6","defects":{"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":1,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":7,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":7,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":8,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":1,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":1,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":1,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":1,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":7,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":8,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":1,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":1,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":8,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":1,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":1,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":8,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":7,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":7,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":7,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":7,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":8,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_PHP_agent_artifacts_and_management_polling_config":8,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__and_operation_endpoints_without_shell_transport_wiring":8,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_the_source_tree_layout":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_mounted_and_baked_in_artifact_directories_before_repo_fallbacks":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_reassigns_invoice_collections_when_changing_an_order_across_the_draft_customer_boundary":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_when_certificate_metadata_changes_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_for_legacy_field_value_updates_through_the_alias_endpoint":7,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":1,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":1,"P\\Tests\\Api\\ApiFixturesCleanupTest::__pest_evaluable_it_removes_generated_customer_traces_during_fixture_cleanup":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_includes_economic_runtime_config_for_uncached_auth_sessions":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":1,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_the_raw_202_gather_webhook_contract_over_HTTP":1,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_still_prompts_for_department_selection_when_only_one_department_is_eligible":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_accepts_the_legacy_initial_webhook_body_with_top_level_call_identifiers":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_a_raw_400_transport_error_for_malformed_webhook_payloads":1,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_lists_the_draft_customer_config_entry_in_economic_config_responses":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_round_trips_the_draft_customer_config_value_through_economic_config_updates":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_order_draft_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_booked_invoice_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_collected_invoice_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\PingApiTest::__pest_evaluable_it_returns_the_ping_contract":1,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_sends_a_Stripe_invoice_by_email_and_persists_the_hosted_invoice_association":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_conflict_when_a_Stripe_hosted_invoice_is_already_active_for_the_order":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_allows_sending_a_new_Stripe_hosted_invoice_when_the_existing_association_is_already_terminal":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_voids_an_unpaid_Stripe_hosted_invoice_and_clears_the_local_order_association":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_refuses_to_cancel_a_paid_Stripe_hosted_invoice":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":8,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":7,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_v2_operator_facing_edge_gateway_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":7,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_returns_the_updated_lane_id_after_editing_a_scanner_whose_null_lane_was_already_cached":7},"times":{"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_creates_a_64_char_auth_token_for_an_existing_user":0.11,"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_throws_a_clear_exception_when_customer_user_is_missing":0.017,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_passkey_challenge_legacy_script_green_during_migration":0.028,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_register_CVR_legacy_script_green_during_migration":0.029,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_deterministic_department_digit_map_and_caps_to_9_options":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_department_ids_by_selected_digit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_gate_type_digit_to_entrance_or_exit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_stores__loads_and_clears_ivr_state_with_ttl":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_validates_state_caller_fingerprint_matching":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_department_and_gate_prompts":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_creates_permission_nodes_linked_to_subuser_permission_keys":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_rejects_empty_permission_definitions":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_button_arrays__json_and_csv_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_button_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_vehicle_type_values_and_null_semantics":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_vehicle_type_values":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_non_numeric_vehicle_type_strings":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_keeps_lane_service_enum_contract_for_MACHINE":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_booking_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_selfserve_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubusersRoutePermissionLinkTest::__pest_evaluable_it_keeps_subusers_route_list_permission_linked_to_SUBUSERS__LIST_node":0.005,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_keeps_route_and_service_entity_type_registries_in_sync_with_expanded_coverage":0,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":0.009,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_wires_local_e_conomic_customer_index_into_customer_related_search_entities":0.003,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_registers_cron_tasks_that_keep_the_e_conomic_search_index_refreshed":0.003,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_generic_db_object_mutation_flows":0.004,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_object_property_mutations":0.003,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_when_module_config_values_change":0.002,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_registers_cron_maintenance_task_for_system_search_cache":0.002,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_redacts_obvious_sensitive_fragments_before_sending_query_to_intent_parser":0.001,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_builds_payload_with_redacted_query_and_parses_strict_JSON_output":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_falls_back_safely_when_OpenAI_is_disabled":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_handles_malformed_OpenAI_response_payloads_without_throwing":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_uses_parser_cache_for_identical_query_and_allowed_type_combinations":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_caps_alias_and_hint_payloads_from_OpenAI_and_filters_hints_to_allowed_types":0.006,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":0.007,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":0.007,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":0.005,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_normalizes_type_lists_from_csv_json_and_arrays":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_parses_booleans_and_clamps_integers_using_route_defaults":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_exposes_expected_searchable_entity_types":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_system_wide_search_GET_and_POST_endpoints_with_intent_debug_support":0.003,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_superuser_cache_clear_and_rebuild_endpoints_for_system_search":0.003,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_passes_permission_and_own_scope_context_into_system_search_service":0.003,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_invoke_intent_parser_when_lexical_confidence_is_already_high":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_on_low_confidence_lexical_results_and_applies_hints_only_boosts":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_lets_parser_entity_hints_override_explicit_include_filters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_returns_fallback_parser_metadata_when_parser_fails_gracefully":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_scopes_query_cache_by_permission_context_to_avoid_cross_user_cache_leakage":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_caps_AI_driven_expanded_terms_to_prevent_query_amplification":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_expands_danish_discount_wording_into_lexical_discount_synonyms":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_for_intent_driven_natural_language_queries_even_when_lexical_score_is_high":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_association_hints_to_pull_related_customer_records_from_non_customer_matches":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_prefers_newer_records_when_relevance_scores_are_comparable":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_keeps_explicit_identifier_matches_ahead_of_newer_but_weaker_records":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_promotes_invoices_orders_order_bookings_and_customers_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_prioritizes_cancelled_bookings_over_active_bookings":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_heavily_demotes_configured_low_priority_entity_types_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_tokenizes_unicode_names_without_stripping_non_ascii_letters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_treat_explicit_identifier_queries_as_intent_driven":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_requires_broader_term_coverage_for_multi_word_scoring":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_enriches_object_attachment_results_with_associated_customer_context":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_includes_the_economic_customer_index_in_cache_dependencies_for_customer_scoped_results":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_falls_back_to_invoice_date_ranges_when_invoice_names_are_missing":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_derives_xlvask_customer_numbers_only_from_digits_only_extern_ids":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_replaces_unnamed_user_titles_with_the_customer_context_name":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_the_goal_criteria_label_for_department_goal_titles":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_reuses_parser_cache_entries_for_repeated_natural_language_intent_requests":0.106,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_clears_parser_cache_namespace_via_clearAll_to_force_a_fresh_parse":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_bumps_per_table_cache_versions_when_a_dirty_table_marker_is_registered":0.081,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_fixed_pricing_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_wash_subscriptions_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_registers_economic_v2_backfill_command_in_cli_dispatcher":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_provides_a_backfill_cron_script_entrypoint":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_exact__match_when_totals_lines_and_departments_are_identical":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_total__mismatch_when_only_totals_differ":0.015,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_line_level_mismatches_for_quantity_and_price":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_departmental_distribution_mismatches":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_missing__target_when_draft_or_booked_target_is_unavailable":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_draft_invoice_lines_including_departmental_distributions":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_marks_text_only_zero_value_lines_as_non_billable_and_keeps_deterministic_key":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_booked_invoices_and_computes_net_total_delta_from_lines":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_contains_internal_normalization_path_with_departmental_metadata_support":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_supports_barred_filtering_when_listing_e_conomic_customers":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_implements_a_dedicated_v2_e_conomic_revenue_statistics_service_and_route":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_all_collected_invoice_economic_v2_routes_with_explicit_permissions":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_version_aware_distribution_and_pricing_history_v2_routes":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_caches_v2_distribution_responses_with_a_configurable_redis_ttl":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_fixed_pricing_versions_from_create_and_delete_flows":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_vehicle_subscription_versions_for_create_update_delete_flows":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_discount_override_versions_from_superuser_discounts_route":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_keeps_legacy_compare_endpoint_path_for_backward_compatibility":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_implements_effective_range_lifecycle_methods_for_all_versioned_entities":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_closes_previous_active_interval_before_inserting_a_new_version":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_includes_best_effort_backfill_with_provenance_and_confidence_metadata":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_anchors_historical_resolution_on_order_created__at_timestamps_in_distribution_service":0.004,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_order_ids_in_net_amount_calculation":0.007,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_customer_arrays_in_date_range_transaction_fetches":0.003,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_uses_centralized_duplicate_filtering_for_possible_duplicate_detection":0.003,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_per_scope_and_date_range":0.007,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_requires_superuser_permission_for_invoicing_period_distribution_all_endpoint":0.017,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_uses_shared_date_range_normalization_across_invoicing_period_endpoints":0.032,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_normalizes_a_valid_invoicing_date_range_to_full_day_timestamps":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_invalid_date_formats":0.012,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_descending_date_ranges":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_finds_duplicate_orders_regardless_of_original_input_order":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_runs_best_effort_backfill_when_fixed_pricing_version_history_is_empty":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_self_serve_conditions_with_combined_AND_and_OR_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_prefers_condition_results_over_question_answers_when_evaluating_task_gates":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":0.01,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_machine_types__eligibility__summaries__and_machine_start_webhook_routes":0.014,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_machine_type_support_wired_into_lanes__tasks__and_conditions_routes":0.018,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_normalizes_webhook_methods_and_falls_back_to_POST_for_unsupported_verbs":0,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_n8n_query_keys":0,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_workflow_lifecycle_endpoints_for_the_n8n_module":0.002,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_execution_and_webhook_trigger_endpoints_for_the_n8n_module":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":0,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_encodes_raw_filter_query_values_that_include_timestamps":0,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_avoids_double_encoding_query_values_that_are_already_escaped":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":0,"P\\Tests\\Unit\\Bookings\\OrderBookingRouteIdempotencyGuardTest::__pest_evaluable_it_guards_order_booking_creation_with_redis_backed_idempotency":0.24,"P\\Tests\\Unit\\DynamicImages\\DynamicImagePreRenderCronWiringTest::__pest_evaluable_it_registers_dynamic_image_pre_render_cron_task_and_related_helpers":0.005,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":0.226,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":0.017,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":0.014,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":0.011,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":0.016,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":0.013,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_both_own_and_elevated_permissions_when_both_are_missing":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_only_elevated_permission_when_own_permission_exists_but_own_context_fails":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_allows_elevated_permission_path_without_forbidden_and_validates_department_access":0,"P\\Tests\\Unit\\Permissions\\ForbiddenResponseWiringTest::__pest_evaluable_it_routes_and_trait_use_forbidden_responses_for_permission_and_ownership_denials":0.018,"P\\Tests\\Unit\\Permissions\\GroupPermissionSessionInvalidationWiringTest::__pest_evaluable_it_invalidates_related_user_permission_and_auth_session_caches_when_group_permissions_change":0.004,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_snake__case_fields":0.066,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_camelCase_aliases":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_normalizes_ENTIRE__DURATION_to_ignore_target__duration__every":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_defaults_recurring_target__duration__every_to_1_when_omitted":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_keeps_strict_legacy_mode_when_target__duration_is_invalid":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_computes_weekly_target_using_touched_ISO_weeks_for_March_2026":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_applies_target__duration__every_for_weekly_cadence":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_prorates_ENTIRE__DURATION_target_by_overlap_days":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_splits_advanced_target_by_override_weights_per_department":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_preserves_legacy_target_behavior_when_target__duration_is_missing":0,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_monthly_renderer_script_green":0.147,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_department_daily_renderer_script_green":0.147,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":0.007,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_uses_company_scoped_workfeed_endpoints_and_raw_Authorization_header":0.003,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_normalizes_documented_shift_query_parameters":0.001,"P\\Tests\\Unit\\Workfeed\\WorkfeedConfigRouteWiringTest::__pest_evaluable_it_registers_module_config_endpoints_for_workfeed":0.004,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_workfeed_query_keys":0,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_employee_and_department_endpoints_for_the_workfeed_module":0.004,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_shift_endpoints_for_the_workfeed_module":0.002,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_for_the_hour_slot_based_on_overlap":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_extracts_department_id_from_supported_workfeed_shift_shapes":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_wrapped_workfeed_collections_from_common_response_keys":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_includes_a_date_key_in_department_weather_timeline_entries":0.003,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_from_start_of_yesterday_to_end_of_today":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_an_explicit_selected_date_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_across_multiple_departments_for_one_hour_slot":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_department_id_input_from_scalar_csv_and_nested_array_values":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_explicit_date__from_and_date__to_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_resolves_forecast_day_count_for_a_given_timeline_range_with_sane_limits":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_resolves_weather_coordinates_from_valid_coordinates_only":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_null_weather_coordinates_when_all_department_locations_are_invalid":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_classifies_weatherapi_no_matching_location_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_does_not_classify_non_location_weatherapi_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_an_empty_forecast_fallback_when_coordinates_are_unavailable":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_for_department_sets_and_timeline_ranges":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_order_insensitive_cache_keys_for_equivalent_department_id_sets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_marks_non_started_slots_as_unknown_regardless_of_wash_hour_ratio":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherPreloadCronWiringTest::__pest_evaluable_it_registers_and_implements_cron_preloading_for_department_weather_cache":0.007,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_stale_ttl_defaults_and_clamps_stale_ttl_below_fresh_ttl":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_hot_activity_ttl_defaults_and_clamps_to_a_positive_value":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_fresh_cached_payloads_without_invoking_the_resolver":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_stale_cached_payloads_and_enqueues_a_refresh_signal":0.01,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_recomputes_and_rewrites_cache_payloads_when_stale_window_is_exceeded":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_records_hot_keys_order_insensitively_and_applies_preload_target_caps":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_batched_wash_count_rows_into_hourly_slot_totals":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_wires_department_weather_route_to_use_batched_wash_aggregation":0.007,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":0,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_ensures_economic_module_rows_in_one_sanitized_batch":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_returns_id_keyed_economic_module_payload_with_null_defaults":0,"P\\Tests\\Unit\\Orders\\OrdersRouteListBatchingWiringTest::__pest_evaluable_it_wires_GET__orders_through_batched_enrichment_and_stripe_snapshot_caching":0.006,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_cashier_names_from_cache_and_fetches_missing_ones_in_batch":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_deterministic_map_for_duplicate_and_unsorted_cashier_ids":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_implements_batched_cashier_name_lookup_with_cache_first_fallback_behavior":0.006,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_sanitizes_and_deduplicates_cashier_ids_before_batch_lookup":0.003,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_overtime_minutes_when_a_shift_carries_an_extended_approval_end_timestamp":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_unapproved_overtime_from_a_bounded_shift_updateTime_fallback":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_does_not_treat_late_unapproved_administrative_edits_as_overtime":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_changes_weather_timeline_cache_key_when_department_weather_targets_change":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_evaluates_healthy_degraded_and_unhealthy_statuses_from_configured_department_targets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_returns_unknown_when_configured_targets_are_missing_for_department_status_evaluation":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":0.046,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":0.006,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_registers_department_weather_target_routes_with_explicit_read_and_manage_permissions":0.004,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_persists_department_weather_targets_using_canonical_department_variable_keys":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_relay_status_get_and_set_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_batches_sequential_machine_relay_status_requests_into_a_single_Shelly_get_call":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_retries_missing_Shelly_status_payloads_until_relay_status_becomes_ready":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_times_out_with_a_clear_Shelly_readiness_error_when_payload_remains_missing":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_direct_set_switch_and_seeds_cache_so_immediate_status_read_does_not_call_Shelly_get":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_lane_gate_open_endpoint":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_in_progress_self_serve_wash_details_endpoint":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":0.132,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":0.001,"P\\Tests\\Unit\\Selfserve\\DepartmentSelfServeEnabledRelaySyncWiringTest::__pest_evaluable_it_syncs_lane_relay_states_when_department_self_serve_enabled_flag_changes":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_supports_hard_relay_set_even_when_lane_status_is_CLOSED":0,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_enables_cleaner_relay_on_wash_start_command_and_webhook_flow":0.012,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_relay_off_when_a_self_serve_wash_session_is_completed":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_exit_port_by_switching_relay__in__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_entrance_port_by_switching_relay__out__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_typed_task_gates_with_strict_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_validates_typed_task_gates_for_known_references":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_fails_validation_when_typed_task_gates_reference_unknown_entities":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_config_draft_publish_rollback_lifecycle_endpoints":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_legacy_self_serve_CRUD_routes_syncing_canonical_drafts":0.013,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_keeps_cleaner_relay_enable_wired_into_machine_relay_start_paths":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":0.277,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_encodes_config_json_payloads_with_apostrophes_before_persistence":0,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_and_cleaner_relays_off_when_a_self_serve_wash_session_is_completed":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_vehicle_type_override_into_self_serve_preview_and_synchronization_routes":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_local_demo_responses_and_skips_Shelly_cloud_calls_for_demo_relay_ids":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_default_OFF_status_for_demo_relay_ids_without_Shelly_lookups":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_excludes_demo_relay_ids_from_Shelly_status_batch_payloads":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_keeps_demo_relay_gate_open_queued_but_skips_Shelly_switch_calls":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_allowed_services_route_through_machine_relay_visibility_sync":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enables_MACHINE_relay_when_MACHINE_is_visible_in_allowed_services":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_turns_MACHINE_relay_off_immediately_when_MACHINE_is_not_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_does_not_enable_MACHINE_relay_when_allowEnable_is_false_even_if_MACHINE_is_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_is_a_safe_no_op_when_MACHINE_relay_is_not_configured":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_synchronizes_machine_relay_from_visible_services_during_session_synchronization":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_adds_explicit_relay_disable_session_helper_for_visibility_driven_OFF_transitions":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_property_gate_command_permissions":0.008,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneCommandEnumTest::__pest_evaluable_it_parses_property_gate_lane_commands":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_entrance_gate_for_OPEN__PROPERTY__ACCESS__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_exit_gate_for_OPEN__PROPERTY__EXIT__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_blocks_property_gate_commands_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_throws_a_clear_error_when_entrance_gate_is_missing_for_property_access_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_access_command_failures":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashFlowMachineAllowedWiringTest::__pest_evaluable_it_reads_machine_allowed_state_from_session_summary_payload_safely":0.005,"P\\Tests\\Unit\\Selfserve\\DbObjectPaginationLegacyColumnCompatibilityTest::__pest_evaluable_it_guards_pagination_against_searchable_fields_missing_from_legacy_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_dynamic__images__vehicle__type_column_for_legacy_selfserve_wash_session_task_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_billable_minutes_when_elapsed_minutes_exceed_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_elapsed_minutes_equal_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_included_minutes_exceed_elapsed_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_handles_zero_and_low_elapsed_wash_time_edge_cases":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_wash_included_minutes_into_self_serve_module_config":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_blocks_explicit_relay_writes_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_disabled_no_op_from_machine_relay_visibility_sync_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":0.007,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":0.019,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":0.072,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":0.013,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_exit_command_failures":0.002,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_caller_id_is_not_confirmed":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_voice_call_facade_methods_to_documented_endpoints_and_methods":0.039,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_recordings_insights_log_and_flash_methods_to_documented_resources":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_encodes_path_segments_before_building_outbound_endpoints":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_serializes_query_parameters_for_GET_transport_requests":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_4xx_and_5xx_transport_failures_into_informative_exceptions":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":0.005,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":0.004,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_accepts_valid_payloads_for_create_and_nested_call_flow_commands":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_rejects_unknown_fields_and_invalid_enum_values_in_strict_schemas":0.014,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_supports_CSV_normalization_in_schema_validation_for_log_filters":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_validates_flash_hangup_payloads_for_both_supported_request_shapes":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_fails_before_forward_step_when_strict_validation_fails_and_forwards_when_valid":0,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_full_Bird_voice_call_route_surface_with_strict_validation_and_permissions":0.005,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_flash_hangup_endpoint_and_keeps_end_alias_wired_as_compatibility_path":0.004,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":0.006,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":0.005,"P\\Tests\\Unit\\Selfserve\\DepartmentGatesRelaysRouteWiringTest::__pest_evaluable_it_validates_department_gate_config_on_both_create_and_update_routes":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_wash__started__at_column_for_legacy_selfserve_wash_session_schemas":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_stores_wash__started__at_in_self_serve_wash_sessions_when_machine_start_is_triggered":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_passes_lane_wash_start_time_into_the_session_machine_start_marker":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_resolves_in_progress_wash__started__at_from_session_with_compatibility_fallbacks":0.003,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_say_payload_and_applies_hangup_default":0.019,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_log_query_csv_and_integer_fields":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_nested_create_call_payload_sections_and_drops_unknown_keys":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_keeps_gather_nested_say_payload_unchanged_when_hangup_is_omitted":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_supports_no_body_and_flash_hangup_payload_normalization":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_2_second_Shelly_gate_for_back_to_back_requests":0.023,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enforces_a_2_second_gap_between_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_keeps_back_to_back_get_switch_requests_ordered_through_the_relay_controller":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_executes_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_does_not_print_Shelly_switch_responses_to_output":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_enforces_a_global_2_second_Shelly_gate_in_sendPostRequest":0.004,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_uses_Redis_NX_PX_semantics_for_cross_request_Shelly_rate_limiting":0.003,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_enforces_the_2_second_Shelly_gate_across_separate_request_contexts":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_does_not_delay_when_the_Shelly_gate_is_already_expired":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":0.201,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_relay_is_offline_and_only_disables_configured_relays":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_bills_manual_self_serve_stop_using_full_elapsed_minutes_without_included_minute_reduction":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_keeps_included_minute_reduction_for_automatic_mode":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_on_first_dtmf_input_captured_within_the_300_second_window":0.005,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_send_Slack_when_dtmf_input_repeats_without_change":0.008,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_again_when_dtmf_input_changes_within_the_window":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_gathering_and_does_not_hang_up_before_300_seconds_have_elapsed":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_announces_timeout_waits_10_seconds_and_sends_hangup_once_at_or_after_300_seconds":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_polling_until_terminal_status_and_only_then_finalizes":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_finalizes_immediately_when_webhook_payload_is_already_terminal_before_timeout_logic":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_duplicate_timeout_or_hangup_actions_across_retries_and_lock_contention":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_state_when_polling_fails_so_completion_is_not_falsely_reported":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_dtmf_input_repeats_without_change":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_answers_immediately_once_and_does_not_re_answer_on_subsequent_webhook_retries":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_with_retry_loop_semantics_until_input_is_entered":0.004,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_result_provides_keys_field_instead_of_dtmf":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_conditions_variable_keys_carries_the_entered_value":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_to_check_every_2_seconds_with_retry_loop_semantics_until_input_is_entered":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_ignores_non_numeric_customer__number_values_from_request":0.042,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_casts_numeric_customer__number_strings_to_int_before_lookup":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_prefers_user__id_when_both_user__id_and_customer__number_are_valid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_falls_back_to_customer__number_when_user__id_is_invalid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_normalizes_request_identifiers_before_user_lookups":0.004,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_rejects_non_positive_and_non_digit_request_values":0.003,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_pagination_results_when_present":0.006,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_collection_count_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_returns_zero_when_neither_pagination_nor_collection_is_available":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_counts_object_based_collections_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_uses_collection_when_present":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_falls_back_to_paymentTerms_when_collection_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_supports_top_level_array_payloads":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_returns_an_empty_array_when_no_known_collection_shape_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_economic__endpoint__t_when_explicitly_requested_and_configured":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_economic__endpoint__t_when_grant__2_is_missing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_economic__endpoint__t":0.014,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_economic__endpoint__t":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_legacy_economic__m_when_available":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_legacy_economic__m_when_grant__2_is_missing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_legacy_economic__m":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_legacy_economic__m":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_unfiltered_total_when_no_customer_filters_are_active":0.004,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_search_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_barred_filter_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_treats__null__search_and_barred_values_as_no_filter":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_filtered_total_when_unfiltered_total_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_maps_e_conomic_integration_failures_in_customer_listing_to_explicit_502_responses":0.014,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_logs_LIST__CUSTOMERS_success_only_after_pagination_and_customer_mapping_are_completed":0.009,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_guards_against_malformed_customer_list_payloads_before_calling_paginate":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_endpoint_trait_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_legacy_economic__m_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_returns_raw_payload_unchanged_on_successful_HTTP_statuses":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_accepts_valid_list_payloads_that_include_collection_and_pagination_results":0.02,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_pagination_is_missing_from_the_response_payload":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_upstream_responds_with_an_error_shaped_payload":0,"P\\Tests\\Unit\\Router\\RouterThrowableHandlingTest::__pest_evaluable_it_catches_throwables_during_auto_route_loading_and_maps_them_to_internal_server_errors":0.003,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_does_not_request_e_conomic_customer_data_when_customer_number_is_zero":0.101,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_short_circuits_e_conomic_customer_lookup_for_non_positive_customer_numbers":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_queued_economic_invoice_endpoints_in_economicInvoiceRoute":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_defines_economic_transfer_queue_jobs_schema_bootstrap_table_and_tracking_columns":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_provides_queue_processor_class_constants_and_processing_entrypoint":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_registers_economic_transfer_queue_cron_task_and_handler":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_paths_in_openapi":0.01,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_schemas_in_openapi":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_quantity_for_e_conomic_export":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_price_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_keeps_positive_quantity_and_price_items_billable_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicInvoiceDraftZeroItemSkipWiringTest::__pest_evaluable_it_skips_zero_cost_and_zero_quantity_items_in_legacy_economic_draft_helper":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_malformed_order_item_payloads_for_e_conomic_export_safety":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueHardeningTest::__pest_evaluable_it_hardens_transfer_queue_with_type_validation_retry_caps_and_stale_lock_recovery":0.004,"P\\Tests\\Unit\\Router\\AutoloadRedisCacheValidationTest::__pest_evaluable_it_validates_cached_autoload_paths_before_returning_and_clears_stale_cache_entries":0.003,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_applies_created__at_bounds_directly_in_SQL_when_filtering_orders_by_registration_number":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_rejects_an_inverted_date_range_for_registration_lookups":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_returns_early_when_registration_number_is_blank":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_wires_queue_worker_class_loading_for_CLI_queue_action":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_economic_invoice_queue_endpoints_before_constructing_queue_service":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_collected_invoice_queue_endpoints_before_constructing_queue_service":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_gracefully_handles_unavailable_queue_dependencies_on_collected_invoice_queue_endpoints":0.005,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_the_queue_job_id_in_POST__collected_invoices_economic_enqueue_response":0.204,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_requires_id_and_at_least_one_mutable_field_for_PUT__collected_invoices_in_user_route":0.004,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":0.175,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_queued_contract_for_POST__collected_invoices_stripe_book":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_order_draft_export_route_queue_only":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_collected_invoice_draft_producing_routes_queue_only_in_orderInvoicesRoute":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_collected_invoice_and_stripe_draft_producing_endpoints_before_constructing_queue_service":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_unavailable_queue_dependency_responses_for_async_economic_transfer_endpoints":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_order_payloads_to_strict_positive_integer_ids":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_collected_invoice_payload_booleans_to_strict_bool_values":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_rejects_invalid_transfer_payloads_before_enqueue_write_attempts":0.036,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":0.284,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":0.012,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":0.011,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_guards_on_economic_invoice_queue_status_and_retry_routes":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_queue_status_lifecycle_endpoints":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_order_draft_export_route":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_collected_invoice_draft_producing_routes":0.004,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_economic":0.236,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_stripe_book":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_200_fallback_plus_202_queue_contracts_for_export_endpoints_and_keeps_503_on_queue_lifecycle_endpoints":0.009,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":0.014,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":0,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":0.195,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":0.003,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":0.003,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":0.009,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_marks_queued_transactions_and_clears_requires__action_when_all_actionable_work_is_already_queued":0.826,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_keeps_requires__action_true_when_a_customer_still_has_unqueued_actionable_transactions":0.045,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_blocks_fixed_pricing_or_subscription_customers_with_no_transactions_when_a_relevant_queue_job_exists":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_queues_admin_commands__exposes_agent_poll_result_handlers__and_keeps_heartbeats_from_mutating_discovery_state":0.003,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_defines_the_dispatchable_gateway_guard_on_the_loaded_manager_class":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"recent heartbeat stays online\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"late heartbeat degrades after one minute\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"stale heartbeat goes offline after five minutes\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"explicit offline reports stay offline even when heartbeat is fresh\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"missing heartbeat is treated as offline\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_marks_ready_discovery_as_stale_when_the_gateway_heartbeat_has_expired":0.254,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_edge_gateway_management_REST_endpoints":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_public_installer__claim__heartbeat__command_polling__and_internal_broker_auth_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeBrokerClientConfigTest::__pest_evaluable_it_uses_the_same_default_broker_url_and_shared_secret_fallback_as_the_docker_stack":0.267,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_forwarded_https_scheme_when_proxied":0.336,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_appends_forwarded_ports_when_the_forwarded_host_omits_them":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_infers_https_for_the_staging_api_host_when_only_the_https_port_is_present":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":0.204,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_normalizes_public_broker_overrides_to_https_and_browser_shell_urls_to_wss":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_keeps_localhost_broker_overrides_on_plain_http_and_ws_for_local_development":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_includes_node_pty_build_prerequisites_in_the_generated_install_script":0.007,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured_and_derives_the_broker_from_the_api_origin":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_normalizes_public_broker_overrides_to_https__strips_paths__and_browser_shell_urls_to_wss":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_merges_incoming_heartbeat_metadata_with_existing_gateway_metadata":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_v2_operator_facing_edge_gateway_routes":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_PHP_edge_agent_routes_for_operations_and_legacy_relay_command_polling":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_PHP_agent_artifacts_and_management_polling_config":0.011,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__and_operation_endpoints_without_shell_transport_wiring":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_keeps_relay_dispatch_and_discovery_queueing_on_the_edge_gateway_manager":0.08,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_loads_relay_command_helpers_on_the_manager_and_gateway_operations_on_the_dedicated_service":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":0.168,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_the_source_tree_layout":0.445,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_includes_the_container_mounted_artifact_directory_as_a_candidate":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_reads_install_artifacts_through_the_shared_locator":0.016,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_builds_update_payloads_with_checksums_from_resolved_artifact_paths":0.232,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_a_supported_runtime_layout":0.225,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_PHP_agent_artifacts_and_forwarded_https_scheme":0.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_explains_the_legacy_dist_mount_mismatch_when_php_artifacts_are_unavailable":0.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_mounted_and_baked_in_artifact_directories_before_repo_fallbacks":0.022,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_falls_back_to_baked_in_artifacts_when_the_mount_path_is_absent":0.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":0.026,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":1.109,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":2.935,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":1.375,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":0.982,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":3.423,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":1.13,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_reassigns_invoice_collections_when_changing_an_order_across_the_draft_customer_boundary":1.615,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":5.592,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":1.29,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":1.517,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":4.136,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":0.512,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":0.965,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":1.045,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":1.64,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_when_certificate_metadata_changes_through_the_primary_endpoint":3.683,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_for_legacy_field_value_updates_through_the_alias_endpoint":3.194,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayOperationLegacySchemaCompatibilityTest::__pest_evaluable_it_keeps_the_legacy_operation__type_column_compatible_with_v2_operation_queueing":0.017,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_builds_deterministic_keys_for_equivalent_contexts":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_stores_and_retrieves_full_order_bookings_payloads":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_ignores_malformed_cached_payloads_and_clears_order_bookings_list_caches":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsRouteCacheWiringTest::__pest_evaluable_it_caches_the_paginated_order_bookings_list_response_and_invalidates_it_on_booking_changes":0.008,"P\\Tests\\Unit\\Bookings\\VehicleSearchBookedMetadataRouteContractTest::__pest_evaluable_it_keeps_booked_vehicle_search_metadata_aligned_with_filtered_pending_order_bookings":0.005,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0.012,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_builds_deterministic_keys_for_equivalent_count_contexts":0.012,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_stores_and_retrieves_normalized_booking_counts":0.014,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_ignores_malformed_payloads_and_clears_order_bookings_count_caches":0.078,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsRouteWiringTest::__pest_evaluable_it_adds_a_cached_order_bookings_counts_endpoint_and_invalidates_it_on_booking_changes":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayFleetUsageStatisticsTest::__pest_evaluable_it_summarizes_fleet_usage_statistics_for_the_dashboard_landing_view":0,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":0.271,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":0.097,"P\\Tests\\Api\\ApiFixturesCleanupTest::__pest_evaluable_it_removes_generated_customer_traces_during_fixture_cleanup":8.099,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":4.515,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":1.271,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":0.888,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_includes_economic_runtime_config_for_uncached_auth_sessions":1.349,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":0.698,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":1.365,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":1.932,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":0.393,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_the_raw_202_gather_webhook_contract_over_HTTP":0.788,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_still_prompts_for_department_selection_when_only_one_department_is_eligible":0.732,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_accepts_the_legacy_initial_webhook_body_with_top_level_call_identifiers":0.692,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":1.515,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":1.719,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":0.587,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_a_raw_400_transport_error_for_malformed_webhook_payloads":0.419,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":1.048,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":0.378,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":0.59,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":1.554,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":0.788,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":1.552,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":1.006,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":1.191,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_lists_the_draft_customer_config_entry_in_economic_config_responses":0.947,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_round_trips_the_draft_customer_config_value_through_economic_config_updates":3.28,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_order_draft_exports_for_the_configured_draft_customer":0.86,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_booked_invoice_exports_for_the_configured_draft_customer":1.127,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_collected_invoice_exports_for_the_configured_draft_customer":0.888,"P\\Tests\\Api\\PingApiTest::__pest_evaluable_it_returns_the_ping_contract":0.877,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":1.281,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_sends_a_Stripe_invoice_by_email_and_persists_the_hosted_invoice_association":5.434,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_conflict_when_a_Stripe_hosted_invoice_is_already_active_for_the_order":4.756,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_allows_sending_a_new_Stripe_hosted_invoice_when_the_existing_association_is_already_terminal":3.943,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_voids_an_unpaid_Stripe_hosted_invoice_and_clears_the_local_order_association":3.746,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_refuses_to_cancel_a_paid_Stripe_hosted_invoice":4.131,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":0.726,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":1.326,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":1.159,"P\\Tests\\Unit\\Users\\EconomicCustomerModelParsingTest::__pest_evaluable_it_parses_cached_economic_customer_payloads_that_use_snake__case_customer__number":0.246,"P\\Tests\\Unit\\Users\\EconomicCustomerModelParsingTest::__pest_evaluable_it_leaves_the_economic_customer_model_empty_when_no_valid_customer_number_is_present":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayFleetUsageStatisticsTest::__pest_evaluable_it_derives_fleet_usage_directly_from_cached_gateway_row_summaries":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":0.619,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_adds_a_gateway_metadata_update_endpoint_with_label_and_primary_assignment_fields":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_reassigns_department_primary_gateways_through_dedicated_manager_helpers":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayShellPollingTransportTest::__pest_evaluable_it_removes_shell_access_from_the_edge_gateway_HTTP_contracts":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":0.047,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_deterministic_cache_keys":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_stores_and_retrieves_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_removes_deleted_gateways_from_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_module_scoped_edge_gateway_operator_routes":0.016,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_module_config_endpoints":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_config_endpoints_from_the_module_route_directory":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":0.014,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_caps_install_session_diagnostics_and_events_while_preserving_the_first_start_timestamp":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_clears_terminal_failure_details_after_a_successful_claim_update":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_marks_expired_non_terminal_install_sessions_as_terminal_when_read_back":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_websocket_broker_urls_on_the_traefik_broker_path":0,"P\\Tests\\Unit\\Selfserve\\DepartmentGateConfigRelayTest::__pest_evaluable_it_validates_relay_gate_configs_and_keeps_relay_specific_fields_in_the_payload":0.016,"P\\Tests\\Unit\\Selfserve\\DepartmentGateConfigRelayTest::__pest_evaluable_it_rejects_relay_gate_configs_without_a_logical_relay_id":0.011,"P\\Tests\\Unit\\Bird\\DepartmentGatesRelayOpenTest::__pest_evaluable_it_dispatches_relay_backed_gates_through_the_edge_gateway_relay_manager":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayDepartmentWorkspaceContractTest::__pest_evaluable_it_defines_the_department_hardware_workspace_service_payload_surface":0.005,"P\\Tests\\Unit\\Selfserve\\PlateScannerWorkspaceContractTest::__pest_evaluable_it_adds_lane_aware_scanner_management_and_a_dedicated_rotate_key_action":0.005,"P\\Tests\\Unit\\Selfserve\\PlateScannerWorkspaceContractTest::__pest_evaluable_it_uses_the_scanner_default_lane_before_requiring_an_explicit_lane__id_in_machine_button_webhooks":0.003,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_status_responses_into_the_Shelly_cloud_payload_shape":0.015,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_switch_responses_into_the_Shelly_cloud_payload_shape":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_requires_a_valid_department_id_for_gateway_transport_requests":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_rejects_unsupported_gateway_transport_endpoints":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_surfaces_binding_lookup_failures_while_resolving_relay_state_through_the_gateway_transport":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_surfaces_offline_gateway_failures_while_dispatching_relay_switch_commands":0,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_returns_the_updated_lane_id_after_editing_a_scanner_whose_null_lane_was_already_cached":21.835,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_rejects_plate_scanner_edits_when_the_permission_is_missing":7.837,"P\\Tests\\Unit\\Selfserve\\ShellyRelayInventoryTest::__pest_evaluable_it_normalizes_owned_Shelly_devices_into_relay_select_options":0.277,"P\\Tests\\Unit\\Selfserve\\ShellyRelayInventoryTest::__pest_evaluable_it_fails_fast_when_Shelly_inventory_does_not_include_owned_devices_status":0.015,"P\\Tests\\Unit\\Selfserve\\DepartmentLaneRelayOptionsRouteWiringTest::__pest_evaluable_it_registers_a_department_lane_relay_options_endpoint_backed_by_Shelly_inventory":0.081}} \ No newline at end of file diff --git a/services/nginx/app/build/logs/api-server.err.log b/services/nginx/app/build/logs/api-server.err.log index de37266e..42aae4a1 100644 --- a/services/nginx/app/build/logs/api-server.err.log +++ b/services/nginx/app/build/logs/api-server.err.log @@ -10565,3 +10565,45 @@ [Tue Apr 21 12:54:05 2026] 127.0.0.1:47012 Closing [Tue Apr 21 12:54:05 2026] 127.0.0.1:47026 Accepted [Tue Apr 21 12:54:06 2026] 127.0.0.1:47026 Closing +[Thu Apr 23 11:09:21 2026] PHP 8.2.15 Development Server (http://127.0.0.1:37467) started +[Thu Apr 23 11:09:21 2026] 127.0.0.1:45138 Accepted +[Thu Apr 23 11:09:23 2026] 127.0.0.1:45138 Closing +[Thu Apr 23 11:09:23 2026] 127.0.0.1:45152 Accepted +[Thu Apr 23 11:09:27 2026] 127.0.0.1:45152 Closing +[Thu Apr 23 11:09:28 2026] 127.0.0.1:45158 Accepted +[Thu Apr 23 11:09:32 2026] 127.0.0.1:45158 Closing +[Thu Apr 23 11:09:38 2026] 127.0.0.1:40568 Accepted +[Thu Apr 23 11:09:42 2026] 127.0.0.1:40568 Closing +[Thu Apr 23 11:10:39 2026] PHP 8.2.15 Development Server (http://127.0.0.1:42453) started +[Thu Apr 23 11:10:39 2026] 127.0.0.1:57264 Accepted +[Thu Apr 23 11:10:41 2026] 127.0.0.1:57264 Closing +[Thu Apr 23 11:10:41 2026] 127.0.0.1:57272 Accepted +[Thu Apr 23 11:10:45 2026] 127.0.0.1:57272 Closing +[Thu Apr 23 11:10:45 2026] 127.0.0.1:57278 Accepted +[Thu Apr 23 11:10:49 2026] 127.0.0.1:57278 Closing +[Thu Apr 23 11:10:49 2026] 127.0.0.1:41688 Accepted +[Thu Apr 23 11:10:55 2026] 127.0.0.1:41688 Closing +[Thu Apr 23 11:11:02 2026] 127.0.0.1:43002 Accepted +[Thu Apr 23 11:11:05 2026] 127.0.0.1:43002 Closing +[Thu Apr 23 11:11:27 2026] PHP 8.2.15 Development Server (http://127.0.0.1:45343) started +[Thu Apr 23 11:11:27 2026] 127.0.0.1:47492 Accepted +[Thu Apr 23 11:11:29 2026] 127.0.0.1:47492 Closing +[Thu Apr 23 11:11:29 2026] 127.0.0.1:44236 Accepted +[Thu Apr 23 11:11:34 2026] 127.0.0.1:44236 Closing +[Thu Apr 23 11:11:34 2026] 127.0.0.1:44242 Accepted +[Thu Apr 23 11:11:38 2026] 127.0.0.1:44242 Closing +[Thu Apr 23 11:11:38 2026] 127.0.0.1:44250 Accepted +[Thu Apr 23 11:11:43 2026] 127.0.0.1:44250 Closing +[Thu Apr 23 11:11:49 2026] 127.0.0.1:52682 Accepted +[Thu Apr 23 11:11:53 2026] 127.0.0.1:52682 Closing +[Thu Apr 23 11:15:24 2026] PHP 8.2.15 Development Server (http://127.0.0.1:45041) started +[Thu Apr 23 11:15:24 2026] 127.0.0.1:54634 Accepted +[Thu Apr 23 11:15:25 2026] 127.0.0.1:54634 Closing +[Thu Apr 23 11:15:25 2026] 127.0.0.1:54642 Accepted +[Thu Apr 23 11:15:30 2026] 127.0.0.1:54642 Closing +[Thu Apr 23 11:15:30 2026] 127.0.0.1:40348 Accepted +[Thu Apr 23 11:15:34 2026] 127.0.0.1:40348 Closing +[Thu Apr 23 11:15:34 2026] 127.0.0.1:40350 Accepted +[Thu Apr 23 11:15:39 2026] 127.0.0.1:40350 Closing +[Thu Apr 23 11:15:45 2026] 127.0.0.1:56004 Accepted +[Thu Apr 23 11:15:49 2026] 127.0.0.1:56004 Closing diff --git a/services/nginx/app/classes/department_gate_config.php b/services/nginx/app/classes/department_gate_config.php index ec7c1f77..6fcbe199 100644 --- a/services/nginx/app/classes/department_gate_config.php +++ b/services/nginx/app/classes/department_gate_config.php @@ -9,15 +9,19 @@ class department_gate_config public string $type; public ?string $phone_number = null; public ?int $call_duration_threshold = null; + public ?string $relay_id = null; + public ?int $pulse_seconds = null; /** * @param array $config */ public function __construct(array $config = []) { - $this->type = (string)($config['type'] ?? ''); + $this->type = strtoupper(trim((string)($config['type'] ?? ''))); $this->phone_number = isset($config['phone_number']) ? (string)$config['phone_number'] : null; $this->call_duration_threshold = isset($config['call_duration_threshold']) ? (int)$config['call_duration_threshold'] : null; + $this->relay_id = isset($config['relay_id']) ? trim((string)$config['relay_id']) : null; + $this->pulse_seconds = isset($config['pulse_seconds']) ? (int)$config['pulse_seconds'] : null; } /** @@ -37,6 +41,14 @@ class department_gate_config $array['call_duration_threshold'] = $this->call_duration_threshold; } + if ($this->relay_id !== null) { + $array['relay_id'] = $this->relay_id; + } + + if ($this->pulse_seconds !== null) { + $array['pulse_seconds'] = $this->pulse_seconds; + } + return $array; } @@ -58,6 +70,19 @@ class department_gate_config if ($this->call_duration_threshold === null) { throw new Exception('Call duration threshold is required for PHONE_CALL gate type'); } + return; } + + if ($this->type === 'RELAY') { + if ($this->relay_id === null || $this->relay_id === '') { + throw new Exception('relay_id is required for RELAY gate type'); + } + if ($this->pulse_seconds !== null && $this->pulse_seconds < 0) { + throw new Exception('pulse_seconds must be a positive integer for RELAY gate type'); + } + return; + } + + throw new Exception('Unsupported gate config type: ' . $this->type); } } diff --git a/services/nginx/app/classes/shelly.php b/services/nginx/app/classes/shelly.php index be115d8f..b18d0b61 100644 --- a/services/nginx/app/classes/shelly.php +++ b/services/nginx/app/classes/shelly.php @@ -54,7 +54,7 @@ class shelly implements shelly_i self::requireValidSecretKey(); // Send the request $response = match ($method) { - //'GET' => self::sendGetRequest($endpoint, $data), + 'GET' => self::sendGetRequest($endpoint, $data), 'POST' => self::sendPostRequest($endpoint, $data), //'PUT' => self::sendPutRequest($endpoint, $data), //'DELETE' => self::sendDeleteRequest($endpoint, $data), @@ -186,9 +186,56 @@ class shelly implements shelly_i return json_decode($response); } - function appendAuthKeyToQuery(string $url): string + /** + * @inheritDoc + * @throws Exception + */ + function sendGetRequest(string $endpoint, array $data): array|object|null { - return $url . '?auth_key=' . $this->config->secret_key->getVariableValue(); + self::requireModuleEnabled(); + self::requireValidSecretKey(); + self::requireValidServerURL(); + $this->waitForShellyRateLimitWindow(); + + $ch = curl_init(); + curl_setopt( + $ch, + CURLOPT_URL, + self::appendAuthKeyToQuery($this->config->server_url->getVariableValue() . $endpoint, $data) + ); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPGET, true); + + $response = curl_exec($ch); + $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if (curl_errno($ch)) { + self::exception( + [ + 'method' => 'GET', + 'endpoint' => $endpoint, + 'data' => $data, + 'response' => $response, + ], + $status_code + ); + } + + curl_close($ch); + + if ($response === false) { + return null; + } + + return json_decode($response); + } + + function appendAuthKeyToQuery(string $url, array $query = []): string + { + $query['auth_key'] = $this->config->secret_key->getVariableValue(); + $separator = str_contains($url, '?') ? '&' : '?'; + + return $url . $separator . http_build_query($query); } /** diff --git a/services/nginx/app/classes/shelly_relay_inventory.php b/services/nginx/app/classes/shelly_relay_inventory.php new file mode 100644 index 00000000..162335bc --- /dev/null +++ b/services/nginx/app/classes/shelly_relay_inventory.php @@ -0,0 +1,243 @@ +client = $client; + } + + public function setInventoryFetcher(callable $fetcher): self + { + $this->inventory_fetcher = $fetcher; + return $this; + } + + /** + * @return array> + * @throws Exception + */ + public function listRelayOptions(): array + { + $devices_status = $this->fetchOwnedDevicesStatus(); + $options_by_id = []; + + foreach ($devices_status as $device) { + $option = $this->buildRelayOption($this->normalizeToArray($device)); + if ($option === null) { + continue; + } + + $options_by_id[$option['id']] = $option; + } + + $options = array_values($options_by_id); + usort($options, static function (array $left, array $right): int { + $online_compare = ((int)!($left['online'] ?? false)) <=> ((int)!($right['online'] ?? false)); + if ($online_compare !== 0) { + return $online_compare; + } + + $name_compare = strcasecmp((string)($left['name'] ?? ''), (string)($right['name'] ?? '')); + if ($name_compare !== 0) { + return $name_compare; + } + + return strcmp((string)($left['id'] ?? ''), (string)($right['id'] ?? '')); + }); + + return $options; + } + + /** + * @return array + * @throws Exception + */ + private function fetchOwnedDevicesStatus(): array + { + $payload = is_callable($this->inventory_fetcher) + ? ($this->inventory_fetcher)() + : $this->getClient()->sendGetRequest('/device/all_status', [ + 'show_info' => 'true', + 'no_shared' => 'true', + ]); + + $normalized = $this->normalizeToArray($payload); + $is_ok = $normalized['isok'] ?? null; + if ($is_ok === false) { + throw new Exception('Shelly relay inventory request failed'); + } + + $devices_status = $normalized['data']['devices_status'] ?? null; + if (!is_array($devices_status)) { + throw new Exception('Shelly relay inventory response was missing devices_status'); + } + + return $devices_status; + } + + private function getClient(): shelly + { + if ($this->client instanceof shelly) { + return $this->client; + } + + $this->client = new shelly(); + return $this->client; + } + + /** + * @param array $device + * @return array|null + */ + private function buildRelayOption(array $device): ?array + { + if ($device === [] || !$this->isRelayCapableDevice($device)) { + return null; + } + + $device_id = $this->extractFirstString([ + $device['_dev_info']['id'] ?? null, + $device['id'] ?? null, + ]); + if ($device_id === '') { + return null; + } + + $device_name = $this->extractFirstString([ + $device['name'] ?? null, + $device['_dev_info']['name'] ?? null, + $device['settings']['name'] ?? null, + $device['settings']['device']['name'] ?? null, + $device['status']['name'] ?? null, + $device['status']['sys']['device']['name'] ?? null, + ]); + $device_code = $this->extractFirstString([ + $device['_dev_info']['code'] ?? null, + $device['code'] ?? null, + ]); + + return [ + 'id' => $device_id, + 'name' => $this->buildRelayLabel($device_id, $device_name, $device_code), + 'device_id' => $device_id, + 'device_name' => $device_name !== '' ? $device_name : null, + 'code' => $device_code !== '' ? $device_code : null, + 'online' => $this->extractOnlineState($device), + ]; + } + + /** + * @param array $device + */ + private function isRelayCapableDevice(array $device): bool + { + if ($this->payloadContainsRelayState($device)) { + return true; + } + + if ($this->payloadContainsRelayState($device['status'] ?? null)) { + return true; + } + + return $this->payloadContainsRelayState($device['settings'] ?? null); + } + + private function payloadContainsRelayState(array|object|null $payload): bool + { + $normalized = $this->normalizeToArray($payload); + if ($normalized === []) { + return false; + } + + foreach (['switch:0', 'switch_0', 'switch0'] as $switch_key) { + if (array_key_exists($switch_key, $normalized)) { + return true; + } + } + + foreach (['relays', 'switches'] as $collection_key) { + if (isset($normalized[$collection_key]) && is_array($normalized[$collection_key]) && $normalized[$collection_key] !== []) { + return true; + } + } + + return false; + } + + /** + * @param array $device + */ + private function extractOnlineState(array $device): bool + { + $online = $device['_dev_info']['online'] ?? $device['online'] ?? null; + if (is_bool($online)) { + return $online; + } + + if (is_numeric($online)) { + return (int)$online === 1; + } + + return false; + } + + private function buildRelayLabel(string $device_id, string $device_name, string $device_code): string + { + $label = $device_id; + if ($device_name !== '' && strcasecmp($device_name, $device_id) !== 0) { + $label = $device_name . ' (' . $device_id . ')'; + } + + if ($device_code !== '') { + return $label . ' · ' . $device_code; + } + + return $label; + } + + /** + * @param array $values + */ + private function extractFirstString(array $values): string + { + foreach ($values as $value) { + $normalized = trim((string)($value ?? '')); + if ($normalized !== '') { + return $normalized; + } + } + + return ''; + } + + /** + * @return array + */ + private function normalizeToArray(array|object|null $payload): array + { + if (is_array($payload)) { + return $payload; + } + + if (is_object($payload)) { + $encoded = json_encode($payload, JSON_UNESCAPED_UNICODE); + if (!is_string($encoded)) { + return []; + } + + $decoded = json_decode($encoded, true); + return is_array($decoded) ? $decoded : []; + } + + return []; + } +} diff --git a/services/nginx/app/interfaces/shelly_i.php b/services/nginx/app/interfaces/shelly_i.php index 592974c7..c20d8a66 100644 --- a/services/nginx/app/interfaces/shelly_i.php +++ b/services/nginx/app/interfaces/shelly_i.php @@ -37,6 +37,14 @@ interface shelly_i */ function requireValidServerUrl(): void; + /** + * Send a GET request to Shelly. + * @param string $endpoint The endpoint to send the request to + * @param array $data Query parameters to append to the request + * @return array|object|null The response from Shelly + */ + function sendGetRequest(string $endpoint, array $data): array|object|null; + /** * Send a POST request to the shelly * @param string $endpoint The endpoint to send the request to (e.g. "latest") @@ -44,4 +52,4 @@ interface shelly_i * @return array|object|null The response from the shelly */ function sendPostRequest(string $endpoint, array $data): array|object|null; -} \ No newline at end of file +} diff --git a/services/nginx/app/modules/edgegateway/classes/edge_gateway_department_workspace_service.php b/services/nginx/app/modules/edgegateway/classes/edge_gateway_department_workspace_service.php new file mode 100644 index 00000000..57722a29 --- /dev/null +++ b/services/nginx/app/modules/edgegateway/classes/edge_gateway_department_workspace_service.php @@ -0,0 +1,865 @@ +> + * @throws Exception + */ + public function listDepartmentSummaries(): array + { + $departments = (new departments_o())->list(true); + usort($departments, static function (array $left, array $right): int { + $leftPriority = (int)($left['order_priority'] ?? PHP_INT_MAX); + $rightPriority = (int)($right['order_priority'] ?? PHP_INT_MAX); + if ($leftPriority !== $rightPriority) { + return $leftPriority <=> $rightPriority; + } + + return (int)($left['id'] ?? 0) <=> (int)($right['id'] ?? 0); + }); + + $summaries = []; + foreach ($departments as $departmentRow) { + $departmentId = (int)($departmentRow['id'] ?? 0); + if ($departmentId <= 0) { + continue; + } + + $workspace = $this->buildDepartmentWorkspace($departmentId, false, $departmentRow); + $summaries[] = $workspace['summary']; + } + + return $summaries; + } + + /** + * @return array + * @throws Exception + */ + public function getDepartmentWorkspace(int $departmentId): array + { + return $this->buildDepartmentWorkspace($departmentId, true); + } + + /** + * @param array|null $departmentRow + * @return array + * @throws Exception + */ + private function buildDepartmentWorkspace(int $departmentId, bool $includeGateways, ?array $departmentRow = null): array + { + $department = (new departments_o())->select($departmentId); + if (!$department->exists()) { + throw new Exception('Department not found'); + } + + $departmentPayload = [ + 'id' => $departmentId, + 'name' => (string)$department->name->value(), + 'description' => (string)$department->description->value(), + 'order_priority' => (int)$department->order_priority->value(), + ]; + + $transportMode = $this->manager()->getDepartmentTransportMode($departmentId); + $gateways = $this->manager()->listGateways($departmentId, true); + $bindingsByRelayId = $this->indexBindingsByRelayId($gateways); + $relayCatalog = $this->indexRelayCatalog($departmentId); + $consumersByRelayId = []; + + $lanes = $this->buildLanePayloads($departmentId, $bindingsByRelayId, $relayCatalog, $consumersByRelayId); + $gates = $this->buildGatePayloads($departmentId, $bindingsByRelayId, $relayCatalog, $consumersByRelayId); + $gateways = $this->applyBindingConsumerContexts($gateways, $consumersByRelayId); + $selfServe = $this->buildSelfServePayload($department, $lanes); + $scanners = $this->buildScannerPayloads($departmentId, $lanes); + $issues = $this->buildIssues($transportMode, $gateways, $lanes, $gates, $scanners, $selfServe); + $actions = $this->buildActions($departmentId, $gateways, $lanes, $gates, $scanners, $selfServe); + $summary = $this->buildSummary( + $departmentPayload, + $departmentRow, + $transportMode, + $gateways, + $lanes, + $gates, + $scanners, + $selfServe, + $issues + ); + + return [ + 'department' => $departmentPayload, + 'summary' => $summary, + 'gateways' => $includeGateways ? $gateways : [], + 'lanes' => $lanes, + 'self_serve' => $selfServe, + 'gates' => $gates, + 'scanners' => $scanners, + 'issues' => $issues, + 'actions' => $actions, + ]; + } + + /** + * @param array> $gateways + * @return array>> + */ + private function indexBindingsByRelayId(array $gateways): array + { + $bindingsByRelayId = []; + + foreach ($gateways as $gateway) { + $bindings = isset($gateway['bindings']) && is_array($gateway['bindings']) + ? (array)$gateway['bindings'] + : []; + + foreach ($bindings as $binding) { + if (!is_array($binding)) { + continue; + } + + $relayId = trim((string)($binding['relay_id'] ?? '')); + if ($relayId === '') { + continue; + } + + $binding['gateway_label'] = (string)($gateway['label'] ?? ('Gateway ' . ($gateway['id'] ?? ''))); + $binding['gateway_status'] = (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE); + $binding['is_primary_gateway'] = (bool)($gateway['is_primary'] ?? false); + $bindingsByRelayId[$relayId][] = $binding; + } + } + + foreach ($bindingsByRelayId as $relayId => $bindings) { + usort($bindings, static function (array $left, array $right): int { + return ((int)($right['is_primary_gateway'] ?? 0) <=> (int)($left['is_primary_gateway'] ?? 0)) + ?: ((int)($left['gateway_id'] ?? 0) <=> (int)($right['gateway_id'] ?? 0)); + }); + $bindingsByRelayId[$relayId] = $bindings; + } + + return $bindingsByRelayId; + } + + /** + * @return array> + * @throws Exception + */ + private function indexRelayCatalog(int $departmentId): array + { + $catalog = []; + foreach ((new department_relays_o())->getDepartmentRelays($departmentId) as $relay) { + if (!$relay->exists()) { + continue; + } + + $relayId = trim((string)$relay->relay_id->value()); + if ($relayId === '') { + continue; + } + + $catalog[$relayId] = $relay->asArray(); + } + + return $catalog; + } + + /** + * @param array>> $bindingsByRelayId + * @param array> $relayCatalog + * @param array>> $consumersByRelayId + * @return array> + * @throws Exception + */ + private function buildLanePayloads( + int $departmentId, + array $bindingsByRelayId, + array $relayCatalog, + array &$consumersByRelayId + ): array { + $lanes = []; + $slotMap = [ + 'relay_in_id' => 'ENTRY', + 'relay_out_id' => 'EXIT', + 'relay_machine_id' => 'MACHINE', + 'relay_machine_program_picker_id' => 'PROGRAM_PICKER', + 'relay_machine_cleaner_id' => 'CLEANER', + ]; + + foreach ((new department_lanes_o())->getDepartmentLanes($departmentId) as $lane) { + if (!$lane->exists()) { + continue; + } + + $relaySlots = []; + $boundRelayCount = 0; + foreach ($slotMap as $property => $slotName) { + $relayId = trim((string)$lane->{$property}->value()); + if ($relayId === '') { + continue; + } + + $consumersByRelayId[$relayId][] = [ + 'type' => 'lane', + 'id' => (int)$lane->id, + 'slot' => $slotName, + 'label' => (string)$lane->name->value(), + ]; + + $coverage = $this->buildRelayCoverage($relayId, $bindingsByRelayId); + if ((bool)($coverage['covered'] ?? false)) { + $boundRelayCount += 1; + } + + $relaySlots[] = [ + 'slot' => $slotName, + 'relay_id' => $relayId, + 'catalog' => $relayCatalog[$relayId] ?? null, + 'coverage' => $coverage, + ]; + } + + $requiredRelayCount = count($relaySlots); + $laneStatus = 'UNKNOWN'; + try { + $laneStatus = (string)$lane->getLaneStatus()->name; + } catch (\Throwable) { + } + + $lanes[] = [ + 'id' => (int)$lane->id, + 'department' => (int)$lane->department->value(), + 'name' => (string)$lane->name->value(), + 'relay_in_id' => $lane->relay_in_id->value() === null ? null : (string)$lane->relay_in_id->value(), + 'relay_out_id' => $lane->relay_out_id->value() === null ? null : (string)$lane->relay_out_id->value(), + 'relay_machine_id' => $lane->relay_machine_id->value() === null ? null : (string)$lane->relay_machine_id->value(), + 'relay_machine_program_picker_id' => $lane->relay_machine_program_picker_id->value() === null ? null : (string)$lane->relay_machine_program_picker_id->value(), + 'relay_machine_cleaner_id' => $lane->relay_machine_cleaner_id->value() === null ? null : (string)$lane->relay_machine_cleaner_id->value(), + 'dynamic_image_id' => $lane->dynamic_image_id->value() === null ? null : (int)$lane->dynamic_image_id->value(), + 'machine_type_id' => $lane->machine_type_id->value() === null ? null : (int)$lane->machine_type_id->value(), + 'status' => $laneStatus, + 'self_serve_products' => $lane->getSelfServeLaneProducts(), + 'relay_slots' => $relaySlots, + 'binding_coverage' => [ + 'required' => $requiredRelayCount, + 'bound' => $boundRelayCount, + 'missing' => max(0, $requiredRelayCount - $boundRelayCount), + 'state' => $requiredRelayCount === 0 + ? 'NOT_REQUIRED' + : ($boundRelayCount === $requiredRelayCount ? 'READY' : 'MISSING'), + ], + 'links' => [ + 'legacy' => '/superuser/department/lanes/' . (int)$lane->id, + 'self_serve_studio' => '/admin/' . $departmentId . '/modules/self-serve/studio', + ], + ]; + } + + return $lanes; + } + + /** + * @param array>> $bindingsByRelayId + * @param array> $relayCatalog + * @param array>> $consumersByRelayId + * @return array> + * @throws Exception + */ + private function buildGatePayloads( + int $departmentId, + array $bindingsByRelayId, + array $relayCatalog, + array &$consumersByRelayId + ): array { + $gates = []; + + foreach ((new department_gates_o())->getDepartmentGates($departmentId) as $gate) { + if (!$gate->exists()) { + continue; + } + + $config = (array)$gate->config->value(); + $gateType = strtoupper(trim((string)($config['type'] ?? 'UNKNOWN'))); + $relayId = trim((string)($config['relay_id'] ?? '')); + + if ($gateType === 'RELAY' && $relayId !== '') { + $consumersByRelayId[$relayId][] = [ + 'type' => 'gate', + 'id' => (int)$gate->id, + 'slot' => ((bool)$gate->is_entrance->value() ? 'ENTRANCE' : ((bool)$gate->is_exit->value() ? 'EXIT' : 'GENERAL')), + 'label' => (string)$gate->name->value(), + ]; + } + + $coverage = $gateType === 'RELAY' && $relayId !== '' + ? $this->buildRelayCoverage($relayId, $bindingsByRelayId) + : null; + + $gates[] = [ + 'id' => (int)$gate->id, + 'department' => (int)$gate->department->value(), + 'name' => (string)$gate->name->value(), + 'is_entrance' => (bool)$gate->is_entrance->value(), + 'is_exit' => (bool)$gate->is_exit->value(), + 'config' => $config, + 'transport_type' => $gateType, + 'config_complete' => $this->isGateConfigComplete($config), + 'relay' => $relayId !== '' ? ($relayCatalog[$relayId] ?? ['relay_id' => $relayId]) : null, + 'coverage' => $coverage, + ]; + } + + return $gates; + } + + /** + * @param array> $lanes + * @return array + * @throws Exception + */ + private function buildSelfServePayload(departments_o $department, array $lanes): array + { + $enabled = false; + try { + $enabled = $department->getSelfServeEnabled(); + } catch (\Throwable) { + } + + $readyLanes = array_values(array_filter($lanes, static function (array $lane): bool { + return (string)($lane['binding_coverage']['state'] ?? 'UNKNOWN') === 'READY'; + })); + + $taskRows = (new \objects\department_selfserve_tasks_o())->getFieldsWhere([ + 'department' => (int)$department->id, + 'deleted_at' => null, + ], ['id', 'lane', 'product']); + + $productIds = []; + foreach ($taskRows as $taskRow) { + if (isset($taskRow['product'])) { + $productIds[(int)$taskRow['product']] = true; + } + } + + return [ + 'enabled' => $enabled, + 'lane_count' => count($lanes), + 'ready_lanes' => count($readyLanes), + 'configured_task_count' => count($taskRows), + 'configured_product_count' => count($productIds), + 'readiness_state' => !$enabled + ? 'DISABLED' + : (count($lanes) === 0 ? 'UNCONFIGURED' : (count($readyLanes) === count($lanes) ? 'READY' : 'PARTIAL')), + 'links' => [ + 'studio' => '/admin/' . (int)$department->id . '/modules/self-serve/studio', + 'legacy' => '/superuser/selfserve', + ], + ]; + } + + /** + * @param array> $lanes + * @return array> + * @throws Exception + */ + private function buildScannerPayloads(int $departmentId, array $lanes): array + { + $laneIndex = []; + foreach ($lanes as $lane) { + $laneIndex[(int)$lane['id']] = $lane; + } + + $recentScansByScannerId = $this->groupRecentScansByScannerId($departmentId); + $scanners = []; + foreach ((new plate_scanners_o())->getDepartmentScanners($departmentId) as $scanner) { + if (!$scanner->exists()) { + continue; + } + + $scannerPayload = $scanner->asArray(); + $laneId = isset($scannerPayload['lane_id']) ? (int)($scannerPayload['lane_id'] ?? 0) : 0; + $assignedLane = $laneId > 0 ? ($laneIndex[$laneId] ?? null) : null; + $recentScans = $recentScansByScannerId[(int)$scanner->id] ?? []; + $recentScanAt = $recentScans !== [] ? ($recentScans[0]['created_at'] ?? null) : null; + + $assignmentState = $laneId <= 0 + ? 'UNASSIGNED' + : ($assignedLane === null + ? 'INVALID' + : (((int)($assignedLane['binding_coverage']['missing'] ?? 0) === 0) ? 'READY' : 'PARTIAL')); + + $scanners[] = [ + ...$scannerPayload, + 'assigned_lane' => $assignedLane, + 'assignment_state' => $assignmentState, + 'recent_scan_at' => $recentScanAt, + 'recent_scans' => $recentScans, + 'recent_scan_count' => count($recentScans), + ]; + } + + return $scanners; + } + + /** + * @return array>> + */ + private function groupRecentScansByScannerId(int $departmentId): array + { + $rows = (new plate_scans_o())->getFieldsWhere([ + 'department_id' => $departmentId, + ], ['id', 'plate_scanner_id', 'plate', 'bay_id', 'created_at']); + + usort($rows, static function (array $left, array $right): int { + $rightTimestamp = strtotime((string)($right['created_at'] ?? '')) ?: 0; + $leftTimestamp = strtotime((string)($left['created_at'] ?? '')) ?: 0; + return $rightTimestamp <=> $leftTimestamp ?: ((int)($right['id'] ?? 0) <=> (int)($left['id'] ?? 0)); + }); + + $grouped = []; + foreach ($rows as $row) { + $scannerId = (int)($row['plate_scanner_id'] ?? 0); + if ($scannerId <= 0) { + continue; + } + + if (!isset($grouped[$scannerId])) { + $grouped[$scannerId] = []; + } + + if (count($grouped[$scannerId]) >= 5) { + continue; + } + + $grouped[$scannerId][] = [ + 'id' => (int)($row['id'] ?? 0), + 'plate' => (string)($row['plate'] ?? ''), + 'bay_id' => isset($row['bay_id']) ? (string)$row['bay_id'] : null, + 'created_at' => isset($row['created_at']) ? (string)$row['created_at'] : null, + ]; + } + + return $grouped; + } + + /** + * @param array> $gateways + * @param array> $lanes + * @param array> $gates + * @param array> $scanners + * @return array> + */ + private function buildIssues( + string $transportMode, + array $gateways, + array $lanes, + array $gates, + array $scanners, + array $selfServe + ): array { + $issues = []; + + if ($gateways === []) { + $issues[] = [ + 'severity' => 'danger', + 'code' => 'NO_GATEWAY', + 'message' => 'No edge gateway has been claimed for this department.', + ]; + } + + $onlineGateways = array_values(array_filter($gateways, static function (array $gateway): bool { + return strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE; + })); + + if ($transportMode === edge_gateway_manager::TRANSPORT_MODE_GATEWAY && $onlineGateways === []) { + $issues[] = [ + 'severity' => 'danger', + 'code' => 'NO_ONLINE_GATEWAY', + 'message' => 'Gateway transport mode is enabled, but no department gateway is currently online.', + ]; + } + + foreach ($lanes as $lane) { + if ((int)($lane['binding_coverage']['missing'] ?? 0) > 0) { + $issues[] = [ + 'severity' => 'warning', + 'code' => 'LANE_BINDING_GAP', + 'message' => 'Lane ' . (string)$lane['name'] . ' is missing relay bindings.', + 'target_type' => 'lane', + 'target_id' => (int)$lane['id'], + ]; + } + } + + foreach ($gates as $gate) { + if (!($gate['config_complete'] ?? false)) { + $issues[] = [ + 'severity' => 'warning', + 'code' => 'GATE_CONFIG_INCOMPLETE', + 'message' => 'Gate ' . (string)$gate['name'] . ' has incomplete transport configuration.', + 'target_type' => 'gate', + 'target_id' => (int)$gate['id'], + ]; + continue; + } + + if (($gate['transport_type'] ?? '') === 'RELAY' && !($gate['coverage']['covered'] ?? false)) { + $issues[] = [ + 'severity' => 'warning', + 'code' => 'GATE_BINDING_MISSING', + 'message' => 'Gate ' . (string)$gate['name'] . ' is assigned to an unbound relay.', + 'target_type' => 'gate', + 'target_id' => (int)$gate['id'], + ]; + } + } + + foreach ($scanners as $scanner) { + if (($scanner['assignment_state'] ?? 'UNASSIGNED') === 'UNASSIGNED') { + $issues[] = [ + 'severity' => 'warning', + 'code' => 'SCANNER_UNASSIGNED', + 'message' => 'Scanner ' . (string)$scanner['name'] . ' is not assigned to a default lane.', + 'target_type' => 'scanner', + 'target_id' => (int)$scanner['id'], + ]; + } elseif (($scanner['assignment_state'] ?? '') === 'PARTIAL') { + $issues[] = [ + 'severity' => 'info', + 'code' => 'SCANNER_LANE_PARTIAL', + 'message' => 'Scanner ' . (string)$scanner['name'] . ' is assigned to a lane with missing relay coverage.', + 'target_type' => 'scanner', + 'target_id' => (int)$scanner['id'], + ]; + } + } + + if (($selfServe['enabled'] ?? false) && (int)($selfServe['ready_lanes'] ?? 0) < (int)($selfServe['lane_count'] ?? 0)) { + $issues[] = [ + 'severity' => 'warning', + 'code' => 'SELFSERVE_PARTIAL_READY', + 'message' => 'Self-serve is enabled, but one or more lanes are missing required relay coverage.', + ]; + } + + return $issues; + } + + /** + * @param array> $gateways + * @param array> $lanes + * @param array> $gates + * @param array> $scanners + * @return array> + */ + private function buildActions( + int $departmentId, + array $gateways, + array $lanes, + array $gates, + array $scanners, + array $selfServe + ): array { + $actions = [ + [ + 'code' => 'OPEN_GATEWAY_TAB', + 'label' => 'Open gateway controls', + 'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=gateways', + ], + ]; + + if ($gateways === []) { + $actions[] = [ + 'code' => 'INSTALL_GATEWAY', + 'label' => 'Install first edge gateway', + 'path' => '/superuser/configuration/edgegateway', + ]; + } + + foreach ($lanes as $lane) { + if ((int)($lane['binding_coverage']['missing'] ?? 0) > 0) { + $actions[] = [ + 'code' => 'REVIEW_LANE_BINDINGS', + 'label' => 'Resolve lane bindings', + 'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=lanes', + ]; + break; + } + } + + foreach ($gates as $gate) { + if (($gate['transport_type'] ?? '') === 'RELAY' && !($gate['coverage']['covered'] ?? false)) { + $actions[] = [ + 'code' => 'REVIEW_GATE_BINDINGS', + 'label' => 'Resolve gate relay bindings', + 'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=gates', + ]; + break; + } + } + + foreach ($scanners as $scanner) { + if (($scanner['assignment_state'] ?? 'UNASSIGNED') === 'UNASSIGNED') { + $actions[] = [ + 'code' => 'ASSIGN_SCANNERS', + 'label' => 'Assign scanners to lanes', + 'path' => '/superuser/departments/' . $departmentId . '/gateways?tab=scanners', + ]; + break; + } + } + + if (($selfServe['enabled'] ?? false) && (int)($selfServe['lane_count'] ?? 0) > 0) { + $actions[] = [ + 'code' => 'OPEN_SELFSERVE_STUDIO', + 'label' => 'Open self-serve studio', + 'path' => '/admin/' . $departmentId . '/modules/self-serve/studio', + ]; + } + + return $actions; + } + + /** + * @param array $departmentPayload + * @param array|null $departmentRow + * @param array> $gateways + * @param array> $lanes + * @param array> $gates + * @param array> $scanners + * @param array> $issues + * @return array + */ + private function buildSummary( + array $departmentPayload, + ?array $departmentRow, + string $transportMode, + array $gateways, + array $lanes, + array $gates, + array $scanners, + array $selfServe, + array $issues + ): array { + $onlineGatewayCount = count(array_filter($gateways, static function (array $gateway): bool { + return strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE; + })); + $primaryGateway = null; + foreach ($gateways as $gateway) { + if (!empty($gateway['is_primary'])) { + $primaryGateway = [ + 'id' => (int)$gateway['id'], + 'label' => (string)($gateway['label'] ?? ('Gateway ' . $gateway['id'])), + 'status' => (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE), + ]; + break; + } + } + + if ($primaryGateway === null && $gateways !== []) { + $gateway = $gateways[0]; + $primaryGateway = [ + 'id' => (int)$gateway['id'], + 'label' => (string)($gateway['label'] ?? ('Gateway ' . $gateway['id'])), + 'status' => (string)($gateway['status'] ?? edge_gateway_manager::STATUS_OFFLINE), + ]; + } + + $requiredRelayIds = []; + $coveredRelayIds = []; + foreach ($lanes as $lane) { + foreach ((array)($lane['relay_slots'] ?? []) as $slot) { + $relayId = trim((string)($slot['relay_id'] ?? '')); + if ($relayId === '') { + continue; + } + + $requiredRelayIds[$relayId] = true; + if (!empty($slot['coverage']['covered'])) { + $coveredRelayIds[$relayId] = true; + } + } + } + foreach ($gates as $gate) { + if (($gate['transport_type'] ?? '') !== 'RELAY') { + continue; + } + + $relayId = trim((string)($gate['relay']['relay_id'] ?? $gate['config']['relay_id'] ?? '')); + if ($relayId === '') { + continue; + } + + $requiredRelayIds[$relayId] = true; + if (!empty($gate['coverage']['covered'])) { + $coveredRelayIds[$relayId] = true; + } + } + + $assignedScannerCount = count(array_filter($scanners, static function (array $scanner): bool { + return (int)($scanner['lane_id'] ?? 0) > 0; + })); + + $recentScanAt = null; + foreach ($scanners as $scanner) { + $candidate = isset($scanner['recent_scan_at']) ? (string)$scanner['recent_scan_at'] : null; + if ($candidate === null || trim($candidate) === '') { + continue; + } + if ($recentScanAt === null || strtotime($candidate) > strtotime($recentScanAt)) { + $recentScanAt = $candidate; + } + } + + $relayGateCount = count(array_filter($gates, static function (array $gate): bool { + return ($gate['transport_type'] ?? '') === 'RELAY'; + })); + $phoneGateCount = count(array_filter($gates, static function (array $gate): bool { + return ($gate['transport_type'] ?? '') === 'PHONE_CALL'; + })); + + return [ + 'department_id' => (int)$departmentPayload['id'], + 'department_name' => (string)$departmentPayload['name'], + 'order_priority' => (int)($departmentRow['order_priority'] ?? $departmentPayload['order_priority'] ?? PHP_INT_MAX), + 'transport_mode' => $transportMode, + 'gateway_count' => count($gateways), + 'online_gateway_count' => $onlineGatewayCount, + 'primary_gateway' => $primaryGateway, + 'lane_count' => count($lanes), + 'self_serve_enabled' => (bool)($selfServe['enabled'] ?? false), + 'self_serve_ready_lanes' => (int)($selfServe['ready_lanes'] ?? 0), + 'required_relay_count' => count($requiredRelayIds), + 'bound_relay_count' => count($coveredRelayIds), + 'missing_binding_count' => max(0, count($requiredRelayIds) - count($coveredRelayIds)), + 'gate_count' => count($gates), + 'gate_transport_mix' => [ + 'relay' => $relayGateCount, + 'phone_call' => $phoneGateCount, + ], + 'scanner_count' => count($scanners), + 'assigned_scanner_count' => $assignedScannerCount, + 'recent_scan_at' => $recentScanAt, + 'issue_count' => count($issues), + 'health' => $this->deriveHealthState($transportMode, $gateways, $issues), + ]; + } + + /** + * @param array> $gateways + * @param array> $issues + */ + private function deriveHealthState(string $transportMode, array $gateways, array $issues): string + { + foreach ($issues as $issue) { + if (($issue['severity'] ?? '') === 'danger') { + return 'AT_RISK'; + } + } + + if ($transportMode === edge_gateway_manager::TRANSPORT_MODE_GATEWAY) { + foreach ($gateways as $gateway) { + if (strtoupper((string)($gateway['status'] ?? '')) === edge_gateway_manager::STATUS_ONLINE) { + return $issues === [] ? 'READY' : 'PARTIAL'; + } + } + return 'AT_RISK'; + } + + return $issues === [] ? 'READY' : 'PARTIAL'; + } + + /** + * @param array>> $bindingsByRelayId + * @return array + */ + private function buildRelayCoverage(string $relayId, array $bindingsByRelayId): array + { + $bindings = $bindingsByRelayId[$relayId] ?? []; + $primaryBinding = $bindings[0] ?? null; + + return [ + 'relay_id' => $relayId, + 'covered' => $bindings !== [], + 'status' => $bindings !== [] ? 'BOUND' : 'MISSING', + 'binding_count' => count($bindings), + 'primary_binding' => $primaryBinding, + 'bindings' => $bindings, + ]; + } + + /** + * @param array> $gateways + * @param array>> $consumersByRelayId + * @return array> + */ + private function applyBindingConsumerContexts(array $gateways, array $consumersByRelayId): array + { + foreach ($gateways as $gatewayIndex => $gateway) { + $bindings = isset($gateway['bindings']) && is_array($gateway['bindings']) + ? (array)$gateway['bindings'] + : []; + + foreach ($bindings as $bindingIndex => $binding) { + if (!is_array($binding)) { + continue; + } + + $relayId = trim((string)($binding['relay_id'] ?? '')); + if ($relayId === '') { + continue; + } + + $metadata = isset($binding['metadata']) && is_array($binding['metadata']) + ? (array)$binding['metadata'] + : []; + $consumerContexts = $consumersByRelayId[$relayId] ?? []; + $metadata['consumer_contexts'] = $consumerContexts; + $metadata['consumers'] = $consumerContexts; + $bindings[$bindingIndex]['metadata'] = $metadata; + $bindings[$bindingIndex]['consumer_contexts'] = $consumerContexts; + } + + $gateways[$gatewayIndex]['bindings'] = $bindings; + } + + return $gateways; + } + + /** + * @param array $config + */ + private function isGateConfigComplete(array $config): bool + { + $type = strtoupper(trim((string)($config['type'] ?? ''))); + if ($type === 'PHONE_CALL') { + return trim((string)($config['phone_number'] ?? '')) !== '' + && isset($config['call_duration_threshold']); + } + + if ($type === 'RELAY') { + return trim((string)($config['relay_id'] ?? '')) !== ''; + } + + return false; + } + + private function manager(): edge_gateway_manager + { + return $this->manager ?? new edge_gateway_manager(); + } +} diff --git a/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php b/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php index f0529e29..85967b2e 100644 --- a/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php +++ b/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php @@ -2999,24 +2999,11 @@ BASH; } $apiBaseUrl = $this->getApiBaseUrl(); - $parsed = parse_url($apiBaseUrl); - $host = $parsed['host'] ?? null; - if (!is_string($host) || trim($host) === '') { + if (trim($apiBaseUrl) === '') { return null; } - $scheme = strtolower((string)($parsed['scheme'] ?? 'http')) === 'https' ? 'https' : 'http'; - $port = (int)(getenv('EDGE_PUBLIC_BROKER_PORT') ?: 4300); - if ($port <= 0) { - $port = 4300; - } - - $hostWithPort = $host; - if (!(($scheme === 'https' && $port === 443) || ($scheme === 'http' && $port === 80))) { - $hostWithPort .= ':' . $port; - } - - return $scheme . '://' . $hostWithPort; + return rtrim($apiBaseUrl, '/') . '/edge-broker'; } private function buildBrokerPublicWebSocketUrl(string $path = ''): ?string @@ -3034,9 +3021,11 @@ BASH; } $port = isset($parsed['port']) ? ':' . (int)$parsed['port'] : ''; + $basePath = rtrim((string)($parsed['path'] ?? ''), '/'); $normalizedPath = '/' . ltrim($path, '/'); + $fullPath = $basePath . ($normalizedPath === '/' ? '' : $normalizedPath); - return $scheme . '://' . $host . $port . ($normalizedPath === '/' ? '' : $normalizedPath); + return $scheme . '://' . $host . $port . ($fullPath === '' ? '' : $fullPath); } private function buildBrokerInternalUrl(): ?string @@ -3133,6 +3122,29 @@ BASH; } $metadata['fallback_mode'] = $fallbackMode; + $consumerContexts = $binding['consumer_contexts'] ?? $binding['consumers'] ?? $metadata['consumer_contexts'] ?? $metadata['consumers'] ?? []; + if (!is_array($consumerContexts)) { + $consumerContexts = []; + } + $consumerContexts = array_values(array_filter(array_map(static function (mixed $consumer): ?array { + if (!is_array($consumer)) { + return null; + } + + $consumerType = trim((string)($consumer['type'] ?? '')); + if ($consumerType === '') { + return null; + } + + return [ + 'type' => $consumerType, + 'id' => isset($consumer['id']) ? (int)$consumer['id'] : null, + 'slot' => isset($consumer['slot']) ? (string)$consumer['slot'] : null, + 'label' => isset($consumer['label']) ? (string)$consumer['label'] : null, + ]; + }, $consumerContexts))); + $metadata['consumer_contexts'] = $consumerContexts; + $metadata['consumers'] = $consumerContexts; if (isset($binding['last_resolution']) && is_array($binding['last_resolution'])) { $metadata['last_resolution'] = (array)$binding['last_resolution']; diff --git a/services/nginx/app/modules/edgegateway/routes/moduleEdgeGatewayRoute.php b/services/nginx/app/modules/edgegateway/routes/moduleEdgeGatewayRoute.php index dc808eb6..edc9b27d 100644 --- a/services/nginx/app/modules/edgegateway/routes/moduleEdgeGatewayRoute.php +++ b/services/nginx/app/modules/edgegateway/routes/moduleEdgeGatewayRoute.php @@ -4,6 +4,7 @@ namespace routes; use classes\authentication; use classes\edge_gateway_manager; +use classes\edge_gateway_department_workspace_service; use classes\edge_gateway_operation_exception; use classes\edge_gateway_operation_service; use classes\edge_gateway_registry_service; @@ -23,6 +24,12 @@ class moduleEdgeGatewayRoute $this->get('/modules/edge-gateways', fn() => $this->handleListGateways(), [ 'modules_shelly_config' => 'List edge gateway module fleet', ]); + $this->get('/modules/edge-gateways/workspace/departments', fn() => $this->handleDepartmentWorkspaceList(), [ + 'modules_shelly_config' => 'List department hardware workspaces', + ]); + $this->get('/modules/edge-gateways/workspace/departments/{id}', fn() => $this->handleDepartmentWorkspaceDetail(), [ + 'modules_shelly_config' => 'View department hardware workspace detail', + ]); $this->get('/modules/edge-gateways/{id}', fn() => $this->handleGatewayDetail(), [ 'modules_shelly_config' => 'View edge gateway module detail', ]); @@ -102,6 +109,26 @@ class moduleEdgeGatewayRoute $response->success($gateway); } + private function handleDepartmentWorkspaceList(): void + { + global /** @var response $response */ $response; + $user = $this->requireModuleOperator(); + $summaries = $this->workspaces()->listDepartmentSummaries(); + (new logs_o())->add('modules_edgegateway', 'global', 1, $user->id, 'MODULES_EDGEGATEWAY_WORKSPACE_LIST', 'Listed department hardware workspace summaries'); + $response->success($summaries); + } + + private function handleDepartmentWorkspaceDetail(): void + { + global /** @var response $response */ $response; + $user = $this->requireModuleOperator(); + $departmentId = (int)$this->fromRoute('id'); + self::requireParameterIntPositive($departmentId, 'id'); + $this->requireDepartmentAccess((string)$departmentId); + (new logs_o())->add('modules_edgegateway', 'global', 1, $user->id, 'MODULES_EDGEGATEWAY_WORKSPACE_GET', 'Fetched department hardware workspace detail'); + $response->success($this->workspaces()->getDepartmentWorkspace($departmentId)); + } + private function handleGatewayTasksPage(): void { global /** @var response $response */ $response; @@ -395,4 +422,9 @@ class moduleEdgeGatewayRoute { return new edge_gateway_manager(); } + + private function workspaces(): edge_gateway_department_workspace_service + { + return new edge_gateway_department_workspace_service(); + } } diff --git a/services/nginx/app/objects/department_gates_o.php b/services/nginx/app/objects/department_gates_o.php index 68aabf9e..3a0f21dd 100644 --- a/services/nginx/app/objects/department_gates_o.php +++ b/services/nginx/app/objects/department_gates_o.php @@ -3,6 +3,7 @@ namespace objects; use classes\department_gate_config; +use classes\edge_gateway_manager; use classes\bird; use classes\db; use classes\object_property; @@ -262,6 +263,11 @@ class department_gates_o extends db return new slack(); } + protected function resolveEdgeGatewayManager(): edge_gateway_manager + { + return new edge_gateway_manager(); + } + /** * @return array> */ @@ -367,9 +373,25 @@ class department_gates_o extends db $this->requireSelected(); $config = (array)$this->config->value(); - if (!$this->matchesPhoneCallGateConfig($config)) { - throw new Exception('Unsupported gate type: ' . (string)($config['type'] ?? '')); + if ($this->matchesPhoneCallGateConfig($config)) { + $this->openPhoneCallGate($config); + return; } + + if (strtoupper(trim((string)($config['type'] ?? ''))) === 'RELAY') { + $this->openRelayBackedGate($config); + return; + } + + throw new Exception('Unsupported gate type: ' . (string)($config['type'] ?? '')); + } + + /** + * @param array $config + * @throws Exception + */ + protected function openPhoneCallGate(array $config): void + { if (!isset($config['phone_number'])) { throw new Exception('Phone number is required for PHONE_CALL gate type'); } @@ -391,4 +413,30 @@ class department_gates_o extends db throw new Exception('Failed to open gate relay via phone call', 0, $e); } } + + /** + * @param array $config + * @throws Exception + */ + protected function openRelayBackedGate(array $config): void + { + $relayId = trim((string)($config['relay_id'] ?? '')); + if ($relayId === '') { + throw new Exception('relay_id is required for RELAY gate type'); + } + + $departmentId = (int)$this->department->value(); + if ($departmentId <= 0) { + throw new Exception('Gate department is invalid'); + } + + $pulseSeconds = isset($config['pulse_seconds']) ? max(0, (int)$config['pulse_seconds']) : 1; + $manager = $this->resolveEdgeGatewayManager(); + $manager->dispatchRelaySwitch($departmentId, $relayId, true); + + if ($pulseSeconds > 0) { + usleep($pulseSeconds * 1000000); + $manager->dispatchRelaySwitch($departmentId, $relayId, false); + } + } } diff --git a/services/nginx/app/objects/plate_scanners_o.php b/services/nginx/app/objects/plate_scanners_o.php index 0d23570b..196b5b76 100644 --- a/services/nginx/app/objects/plate_scanners_o.php +++ b/services/nginx/app/objects/plate_scanners_o.php @@ -4,6 +4,7 @@ namespace objects; use classes\db; use classes\object_property; +use Exception; use traits\db_object_t; class plate_scanners_o extends db @@ -11,12 +12,16 @@ class plate_scanners_o extends db use db_object_t; public object_property $department_id; + public object_property $lane_id; public object_property $name; public object_property $notes; public object_property $api_key; + private static bool $schemaInitialized = false; + public function structure(): void { + self::ensureSchema(); $this->setTable('plate_scanners'); } @@ -41,22 +46,25 @@ class plate_scanners_o extends db public function getObjectProperties(): void { $this->department_id = new object_property($this->table, $this->id, 'department_id', 'int'); + $this->lane_id = new object_property($this->table, $this->id, 'lane_id', 'int'); $this->name = new object_property($this->table, $this->id, 'name', 'string'); $this->notes = new object_property($this->table, $this->id, 'notes', 'string'); $this->api_key = new object_property($this->table, $this->id, 'api_key', 'string'); } - public function add(int $department_id, string $name, string $notes): void + public function add(int $department_id, string $name, string $notes, ?int $lane_id = null): void { global $db, $response; try { // Generate an API key $api_key = bin2hex(random_bytes(32)); + $lane_id = $this->normalizeLaneId($department_id, $lane_id); // Avoid SQL injection $name = $db->escape_string($name); $notes = $db->escape_string($notes); + $laneValue = $lane_id === null ? 'NULL' : (string)$lane_id; // Create a new record in the database - $sql = "INSERT INTO $this->table (department_id, name, notes, api_key) VALUES ($department_id, '$name', '$notes', '$api_key')"; + $sql = "INSERT INTO $this->table (department_id, lane_id, name, notes, api_key) VALUES ($department_id, $laneValue, '$name', '$notes', '$api_key')"; $db->query($sql); // Get the id of the new record @@ -69,20 +77,26 @@ class plate_scanners_o extends db } } - public function edit(int $id, int $department_id, string $name, string $notes): void + public function edit( + int $id, + int $department_id, + string $name, + string $notes, + ?int $lane_id = null, + bool $laneIdProvided = false + ): void { - global $db, $response; - $this->id = $id; + global $response; try { - // Avoid SQL injection - $name = $db->escape_string($name); - $notes = $db->escape_string($notes); - // Update the record in the database - $sql = "UPDATE $this->table SET department_id = $department_id, name = '$name', notes = '$notes' WHERE id = $id"; - $db->query($sql); - - // Set the values of the object properties - $this->getObjectProperties(); + $this->select($id); + // Use object_property setters so cached field values are invalidated before we serialize the scanner. + $this->department_id->set($department_id); + $this->name->set($name); + $this->notes->set($notes); + if ($laneIdProvided) { + $lane_id = $this->normalizeLaneId($department_id, $lane_id); + $this->lane_id->set($lane_id); + } } catch (\Exception $e) { $response->error($e->getMessage()); } @@ -102,4 +116,116 @@ class plate_scanners_o extends db } return $this; } -} \ No newline at end of file + + /** + * @return array{id:int,department_id:int,lane_id:int|null,name:string,notes:string,api_key:string} + * @throws Exception + */ + public function asArray(): array + { + $this->requireSelected(); + + return [ + 'id' => (int)$this->id, + 'department_id' => (int)$this->department_id->value(), + 'lane_id' => $this->lane_id->value() === null ? null : (int)$this->lane_id->value(), + 'name' => (string)$this->name->value(), + 'notes' => (string)$this->notes->value(), + 'api_key' => (string)$this->api_key->value(), + ]; + } + + /** + * @return array + * @throws Exception + */ + public function getDepartmentScanners(int $departmentId): array + { + $scanners = []; + $rows = self::getFieldsWhere([ + 'department_id' => $departmentId, + ], ['id']); + + foreach ($rows as $row) { + $scanner = (new plate_scanners_o())->select((int)$row['id']); + if ($scanner->exists()) { + $scanners[] = $scanner; + } + } + + return $scanners; + } + + /** + * @throws Exception + */ + public function rotateApiKey(int $id): array + { + $scanner = $this->select($id); + if (!$scanner->exists()) { + throw new Exception('Number plate scanner not found'); + } + + $newApiKey = bin2hex(random_bytes(32)); + $scanner->api_key->set($newApiKey); + + return $scanner->asArray(); + } + + private function normalizeLaneId(int $departmentId, ?int $laneId): ?int + { + if ($laneId === null || $laneId <= 0) { + return null; + } + + $lane = (new department_lanes_o())->select($laneId); + if (!$lane->exists()) { + throw new Exception('Department lane not found'); + } + + if ((int)$lane->department->value() !== $departmentId) { + throw new Exception('The lane does not belong to the number plate scanner department'); + } + + return (int)$lane->id; + } + + private static function ensureSchema(): void + { + if (self::$schemaInitialized) { + return; + } + + global $db; + + if (!self::tableHasColumn('plate_scanners', 'lane_id')) { + $db->query("ALTER TABLE `plate_scanners` ADD COLUMN `lane_id` INT NULL AFTER `department_id`"); + } + + self::$schemaInitialized = true; + } + + private static function tableHasColumn(string $table, string $column): bool + { + global $db; + + $table = $db->escape_string($table); + $column = $db->escape_string($column); + $database = $db->escape_string($db->getDatabase()); + + $result = $db->query( + "SELECT COUNT(*) AS c + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = '$database' + AND TABLE_NAME = '$table' + AND COLUMN_NAME = '$column'" + ); + + if (!$result) { + return false; + } + + $row = $result->fetch_assoc(); + return ((int)($row['c'] ?? 0)) > 0; + } +} diff --git a/services/nginx/app/routes/departmentLanesRoute.php b/services/nginx/app/routes/departmentLanesRoute.php index 19b62646..6b0cfa9f 100644 --- a/services/nginx/app/routes/departmentLanesRoute.php +++ b/services/nginx/app/routes/departmentLanesRoute.php @@ -4,6 +4,7 @@ namespace routes; use classes\authentication; use classes\response; +use classes\shelly_relay_inventory; use dynamicimages\images\machine_1; use objects\categories_o; use objects\department_lanes_o; @@ -81,6 +82,52 @@ class departmentLanesRoute ] ); + $this->get('/department/lanes/relay-options', function () { + + global $response; + $this->requirePermission('list_department_lanes'); + $user = (new authentication())->get_user(); + + if ($user) { + try { + $options = (new shelly_relay_inventory())->listRelayOptions(); + (new logs_o())->add( + 'department_lanes', + 'global', + 1, + $user->id, + 'LIST_DEPARTMENT_LANE_RELAY_OPTIONS', + 'User listed available Shelly relay options' + ); + $response->success($options); + } catch (\Throwable $e) { + (new logs_o())->add( + 'department_lanes', + 'global', + 0, + $user->id, + 'LIST_DEPARTMENT_LANE_RELAY_OPTIONS', + 'Failed to list Shelly relay options: ' . $e->getMessage() + ); + $response->error('Failed to fetch Shelly relay options: ' . $e->getMessage(), 400); + } + } else { + (new logs_o())->add( + 'department_lanes', + 'global', + 1, + 0, + 'LIST_DEPARTMENT_LANE_RELAY_OPTIONS', + 'User tried to list Shelly relay options without being logged in' + ); + $response->error('Invalid session', 400); + } + }, + [ + 'list_department_lanes' => 'List available Shelly relay options for department lanes' + ] + ); + /** * Generate dynamic image for a department lane (machine UI) * diff --git a/services/nginx/app/routes/machineButtonPressRoute.php b/services/nginx/app/routes/machineButtonPressRoute.php index 260ddaa8..79693b66 100644 --- a/services/nginx/app/routes/machineButtonPressRoute.php +++ b/services/nginx/app/routes/machineButtonPressRoute.php @@ -69,6 +69,17 @@ class machineButtonPressRoute return (int)$lane->id; } + if ($plateScanner->lane_id->value() !== null) { + $lane = (new department_lanes_o())->select((int)$plateScanner->lane_id->value()); + if (!$lane->exists()) { + $response->error('The default lane configured for the plate scanner no longer exists', 404); + } + if ((int)$lane->department->value() !== (int)$plateScanner->department_id->value()) { + $response->error('The default lane does not belong to the plate scanner department', 403); + } + return (int)$lane->id; + } + $lanes = (new department_lanes_o())->getDepartmentLanes((int)$plateScanner->department_id->value()); if (count($lanes) === 1) { return (int)$lanes[0]->id; diff --git a/services/nginx/app/routes/plateScannersRoute.php b/services/nginx/app/routes/plateScannersRoute.php index 7ce0d3dd..ab9c9f97 100644 --- a/services/nginx/app/routes/plateScannersRoute.php +++ b/services/nginx/app/routes/plateScannersRoute.php @@ -34,7 +34,11 @@ class plateScannersRoute 'name', 'notes' ]) - ->listObjectsWithPaginationIfSet() + ->listObjectsWithPaginationIfSet( + static function (array $scanner): array { + return (new plate_scanners_o())->select((int)$scanner['id'])->asArray(); + } + ) ); } else { // Log the incident @@ -68,12 +72,19 @@ class plateScannersRoute if (!isset($data['notes'])) { $response->error('Notes is required', 400); } + $laneId = array_key_exists('lane_id', $data) && $data['lane_id'] !== null + ? (int)$data['lane_id'] + : null; // Add the number plate scanner - (new plate_scanners_o())->add($data['department_id'], $data['name'], $data['notes']); + $scanner = new plate_scanners_o(); + $scanner->add((int)$data['department_id'], (string)$data['name'], (string)$data['notes'], $laneId); // Log the incident (new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'ADD_NUMBER_PLATE_SCANNER', 'Successfully added a number plate scanner'); // Return a success message - $response->success(['message' => 'Number plate scanner added']); + $response->success([ + 'message' => 'Number plate scanner added', + 'scanner' => $scanner->asArray(), + ]); } else { // Log the incident (new logs_o())->add('numberplatescanners', 'global', 1, 0, 'ADD_NUMBER_PLATE_SCANNER', 'No user found, or invalid session'); @@ -109,12 +120,25 @@ class plateScannersRoute if (!isset($data['notes'])) { $response->error('Notes is required', 400); } + $laneIdProvided = array_key_exists('lane_id', $data); + $laneId = $laneIdProvided && $data['lane_id'] !== null ? (int)$data['lane_id'] : null; // Edit the number plate scanner - (new plate_scanners_o())->edit($data['id'], $data['department_id'], $data['name'], $data['notes']); + $scanner = new plate_scanners_o(); + $scanner->edit( + (int)$data['id'], + (int)$data['department_id'], + (string)$data['name'], + (string)$data['notes'], + $laneId, + $laneIdProvided + ); // Log the incident (new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'EDIT_NUMBER_PLATE_SCANNER', 'Successfully edited a number plate scanner'); // Return a success message - $response->success(['message' => 'Number plate scanner edited']); + $response->success([ + 'message' => 'Number plate scanner edited', + 'scanner' => $scanner->asArray(), + ]); } else { // Log the incident (new logs_o())->add('numberplatescanners', 'global', 1, 0, 'EDIT_NUMBER_PLATE_SCANNER', 'No user found, or invalid session'); @@ -127,6 +151,35 @@ class plateScannersRoute ] ); + $this->post('/numberplatescanners/{id}/rotate-key', function () { + global $response; + $this->requirePermission('edit_number_plate_scanner'); + $user = (new authentication())->get_user(); + + if (!$user) { + (new logs_o())->add('numberplatescanners', 'global', 1, 0, 'ROTATE_NUMBER_PLATE_SCANNER_KEY', 'No user found, or invalid session'); + $response->error('Invalid session', 400); + } + + $scannerId = (int)$this->fromRoute('id'); + self::requireParameterIntPositive($scannerId, 'id'); + $scanner = (new plate_scanners_o())->select($scannerId); + if (!$scanner->exists()) { + $response->error('Number plate scanner not found', 404); + } + + self::requireDepartmentAccess((int)$scanner->department_id->value()); + $rotatedScanner = (new plate_scanners_o())->rotateApiKey($scannerId); + (new logs_o())->add('numberplatescanners', 'global', 1, $user->id, 'ROTATE_NUMBER_PLATE_SCANNER_KEY', 'Successfully rotated a number plate scanner API key'); + $response->success([ + 'message' => 'Number plate scanner API key rotated', + 'scanner' => $rotatedScanner, + 'api_key' => (string)$rotatedScanner['api_key'], + ]); + }, [ + 'edit_number_plate_scanner' => 'Rotate a number plate scanner API key', + ]); + self::get('/department/numberplatescanners', function () { // Require the user to be logged in global $response; @@ -152,12 +205,14 @@ class plateScannersRoute 'department_id' => (int)self::getParameter('id') ], [ 'id', + 'lane_id', 'name', 'notes' ]); // Parse the result foreach ( $result as $key => $value ) { $result[$key]['id'] = (int)$value['id']; + $result[$key]['lane_id'] = $value['lane_id'] === null ? null : (int)$value['lane_id']; } // Return the list of plate scanners $response->success( @@ -176,4 +231,4 @@ class plateScannersRoute ] ); } -} \ No newline at end of file +} diff --git a/services/nginx/app/tests/Api/PlateScannersApiTest.php b/services/nginx/app/tests/Api/PlateScannersApiTest.php new file mode 100644 index 00000000..5db94a62 --- /dev/null +++ b/services/nginx/app/tests/Api/PlateScannersApiTest.php @@ -0,0 +1,125 @@ +createDepartment([ + 'name' => 'Scanner Department ' . uniqid('', false), + ]); + $session = api_fixtures()->createUserSession([ + 'add_department_lane', + 'add_number_plate_scanner', + 'edit_number_plate_scanner', + ]); + + $laneName = 'Lane ' . uniqid('', false); + $laneResponse = api_client()->post('/department/lanes', [ + 'department' => $department['id'], + 'name' => $laneName, + ], $session['headers']); + + $laneResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + $lane = api_test_runtime()->queryOne( + "SELECT id FROM department_lanes WHERE department = " . (int)$department['id'] . + " AND name = '" . api_test_runtime()->db()->real_escape_string($laneName) . "'" . + ' ORDER BY id DESC LIMIT 1' + ); + + expect($lane)->not->toBeNull(); + $laneId = (int)($lane['id'] ?? 0); + expect($laneId)->toBeGreaterThan(0); + api_fixtures()->cleanupDeleteById('department_lanes', $laneId); + + $scannerName = 'Scanner ' . uniqid('', false); + $createResponse = api_client()->post('/numberplatescanners', [ + 'department_id' => $department['id'], + 'name' => $scannerName, + 'notes' => 'Original notes', + ], $session['headers']); + + $createResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess() + ->assertMessage('Number plate scanner added'); + + $createdScanner = api_test_runtime()->queryOne( + "SELECT id FROM plate_scanners WHERE department_id = " . (int)$department['id'] . + " AND name = '" . api_test_runtime()->db()->real_escape_string($scannerName) . "'" . + ' ORDER BY id DESC LIMIT 1' + ); + + expect($createdScanner)->not->toBeNull(); + $scannerId = (int)($createdScanner['id'] ?? 0); + expect($scannerId)->toBeGreaterThan(0); + api_fixtures()->cleanupDeleteById('plate_scanners', $scannerId); + + $redis = api_test_runtime()->redis(); + expect($redis)->not->toBeNull(); + $cacheKeys = [ + "obj_prop:plate_scanners:{$scannerId}:lane_id" => '___NULL___', + "obj_prop:plate_scanners:{$scannerId}:name" => $scannerName, + "obj_prop:plate_scanners:{$scannerId}:notes" => 'Original notes', + ]; + foreach ($cacheKeys as $cacheKey => $value) { + $redis->set($cacheKey, $value); + } + + $updateResponse = api_client()->put('/numberplatescanners', [ + 'id' => $scannerId, + 'department_id' => $department['id'], + 'lane_id' => $laneId, + 'name' => 'Updated scanner', + 'notes' => 'Updated notes', + ], $session['headers']); + + $updateResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess() + ->assertMessage('Number plate scanner edited'); + + $redis->del(array_keys($cacheKeys)); + + $updatedScanner = $updateResponse->data()['scanner'] ?? null; + expect($updatedScanner)->toBeArray(); + expect($updatedScanner['id'] ?? null)->toBe($scannerId); + expect($updatedScanner['department_id'] ?? null)->toBe($department['id']); + expect($updatedScanner['lane_id'] ?? null)->toBe($laneId); + expect($updatedScanner['name'] ?? null)->toBe('Updated scanner'); + expect($updatedScanner['notes'] ?? null)->toBe('Updated notes'); + + $row = api_fixtures()->fetchRowById('plate_scanners', $scannerId); + + expect($row)->not->toBeNull(); + expect((int)($row['lane_id'] ?? 0))->toBe($laneId); + expect($row['name'] ?? null)->toBe('Updated scanner'); + expect($row['notes'] ?? null)->toBe('Updated notes'); +}); + +it('rejects plate scanner edits when the permission is missing', function (): void { + api_test_covers('PUT /numberplatescanners', 'auth'); + + $session = api_fixtures()->createUserSession([]); + + $response = api_client()->put('/numberplatescanners', [ + 'id' => 1, + 'department_id' => 1, + 'name' => 'No permission', + 'notes' => 'Should fail', + ], $session['headers']); + + $response + ->assertStatus(403) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMissingPermissions(['edit_number_plate_scanner']); +}); diff --git a/services/nginx/app/tests/Api/api_coverage_manifest.php b/services/nginx/app/tests/Api/api_coverage_manifest.php index 7cb68843..d39ffdce 100644 --- a/services/nginx/app/tests/Api/api_coverage_manifest.php +++ b/services/nginx/app/tests/Api/api_coverage_manifest.php @@ -14,6 +14,7 @@ return [ 'GET /orders', 'POST /orders', 'PUT /orders', + 'PUT /numberplatescanners', 'DELETE /orders', 'POST /bird/voice/calls/webhook/inbound', ], diff --git a/services/nginx/app/tests/Tooling/composer-entrypoint-autoload-recovery.sh b/services/nginx/app/tests/Tooling/composer-entrypoint-autoload-recovery.sh new file mode 100644 index 00000000..a43bcb6f --- /dev/null +++ b/services/nginx/app/tests/Tooling/composer-entrypoint-autoload-recovery.sh @@ -0,0 +1,49 @@ +#!/bin/sh +set -eu + +tmp_dir="$(mktemp -d)" +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT HUP INT TERM + +cat > "$tmp_dir/composer.json" <<'JSON' +{ + "name": "truckwash/composer-entrypoint-fixture", + "autoload": { + "classmap": [ + "src/" + ] + } +} +JSON + +mkdir -p "$tmp_dir/src" +cat > "$tmp_dir/src/FixtureClass.php" <<'PHP' +/dev/null 2>&1 + +printf '%s\n' ' "$tmp_dir/vendor/composer/autoload_real.php" + +AUTO_COMPOSER_INSTALL=true \ +APP_DIR="$tmp_dir" \ +MODULE_DIR="$tmp_dir/no-module" \ +LOG_FILE="$tmp_dir/composer-install.log" \ +/usr/local/bin/docker-entrypoint.sh \ +php -r "require \$argv[1]; echo class_exists('FixtureClass') ? 'autoload-ok' . PHP_EOL : 'autoload-missing' . PHP_EOL;" \ +"$tmp_dir/vendor/autoload.php" >/dev/null + +php -d display_errors=1 -r "require \$argv[1]; exit(class_exists('FixtureClass') ? 0 : 1);" "$tmp_dir/vendor/autoload.php" + +echo "composer-entrypoint-autoload-recovery-ok" diff --git a/services/nginx/app/tests/Unit/Bird/DepartmentGatesRelayOpenTest.php b/services/nginx/app/tests/Unit/Bird/DepartmentGatesRelayOpenTest.php new file mode 100644 index 00000000..086b9df2 --- /dev/null +++ b/services/nginx/app/tests/Unit/Bird/DepartmentGatesRelayOpenTest.php @@ -0,0 +1,79 @@ +switchCalls[] = [ + 'department_id' => $departmentId, + 'relay_id' => $logicalRelayId, + 'on' => $on, + ]; + + return [ + 'relay_id' => $logicalRelayId, + 'online' => true, + 'on' => $on, + ]; + } +} + +final class DepartmentGatesRelayOpenHarness extends department_gates_o +{ + public function __construct( + array $config, + int $departmentId, + private readonly DepartmentGatesRelayManagerFake $manager, + ) { + $this->id = 1001; + $departmentProperty = new object_property('department_gates', -1, 'department', 'int'); + $departmentProperty->set($departmentId); + $this->department = $departmentProperty; + + $configProperty = new object_property('department_gates', -1, 'config', 'json'); + $configProperty->set($config); + $this->config = $configProperty; + } + + public function requireSelected(): void + { + } + + protected function resolveEdgeGatewayManager(): edge_gateway_manager + { + return $this->manager; + } +} + +it('dispatches relay-backed gates through the edge gateway relay manager', function (): void { + $manager = new DepartmentGatesRelayManagerFake(); + $gate = new DepartmentGatesRelayOpenHarness([ + 'type' => 'RELAY', + 'relay_id' => 'ENTRY-GATE-1', + 'pulse_seconds' => 0, + ], 17, $manager); + + $gate->openGate(); + + expect($manager->switchCalls)->toBe([ + [ + 'department_id' => 17, + 'relay_id' => 'ENTRY-GATE-1', + 'on' => true, + ], + ]); +}); diff --git a/services/nginx/app/tests/Unit/Selfserve/DepartmentGateConfigRelayTest.php b/services/nginx/app/tests/Unit/Selfserve/DepartmentGateConfigRelayTest.php new file mode 100644 index 00000000..a469081c --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/DepartmentGateConfigRelayTest.php @@ -0,0 +1,30 @@ + 'RELAY', + 'relay_id' => 'ENTRY-GATE-1', + 'pulse_seconds' => 0, + ]); + + $config->validate(); + + expect($config->toArray())->toMatchArray([ + 'type' => 'RELAY', + 'relay_id' => 'ENTRY-GATE-1', + 'pulse_seconds' => 0, + ]); +}); + +it('rejects relay gate configs without a logical relay id', function (): void { + $config = new department_gate_config([ + 'type' => 'RELAY', + ]); + + expect(fn() => $config->validate()) + ->toThrow(Exception::class, 'relay_id is required for RELAY gate type'); +}); diff --git a/services/nginx/app/tests/Unit/Selfserve/DepartmentLaneRelayOptionsRouteWiringTest.php b/services/nginx/app/tests/Unit/Selfserve/DepartmentLaneRelayOptionsRouteWiringTest.php new file mode 100644 index 00000000..a9898d7c --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/DepartmentLaneRelayOptionsRouteWiringTest.php @@ -0,0 +1,10 @@ +not->toBeFalse(); + expect($route)->toContain("'/department/lanes/relay-options'"); + expect($route)->toContain('new shelly_relay_inventory()'); + expect($route)->toContain('LIST_DEPARTMENT_LANE_RELAY_OPTIONS'); +}); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayDepartmentWorkspaceContractTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayDepartmentWorkspaceContractTest.php new file mode 100644 index 00000000..b9a67382 --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayDepartmentWorkspaceContractTest.php @@ -0,0 +1,17 @@ +not->toBeFalse(); + expect($service)->toContain('class edge_gateway_department_workspace_service'); + expect($service)->toContain("'summary' => \$summary"); + expect($service)->toContain("'gateways' => \$includeGateways ? \$gateways : []"); + expect($service)->toContain("'lanes' => \$lanes"); + expect($service)->toContain("'self_serve' => \$selfServe"); + expect($service)->toContain("'gates' => \$gates"); + expect($service)->toContain("'scanners' => \$scanners"); + expect($service)->toContain("'issues' => \$issues"); + expect($service)->toContain("'actions' => \$actions"); + expect($service)->toContain("'consumer_contexts'"); +}); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php index 21c40a74..8e6089d8 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php @@ -30,6 +30,13 @@ function with_edge_gateway_server_state(array $server, callable $callback): void } } +function invoke_edge_gateway_private(object $instance, string $method, mixed ...$arguments): mixed +{ + $reflection = new ReflectionMethod($instance, $method); + $reflection->setAccessible(true); + return $reflection->invokeArgs($instance, $arguments); +} + it('builds install script urls with the compose edge gateway artifacts and forwarded https scheme', function (): void { with_edge_gateway_server_state([ 'HTTP_HOST' => 'api.truckwash.io:4433', @@ -64,7 +71,7 @@ it('builds install script urls with the compose edge gateway artifacts and forwa expect($script)->toContain('"composeFileName":"docker-compose.gateway.yml"'); expect($script)->toContain('"stateDatabasePath":"/opt/truckwash-edge-agent/runtime/gateway-state.sqlite"'); expect($script)->toContain('"operationPollTimeoutSeconds":20'); - expect($script)->toContain('"brokerUrl":"https://api.truckwash.io:4300"'); + expect($script)->toContain('"brokerUrl":"https://api.truckwash.io:4433/edge-broker"'); expect($script)->not->toContain('"shellActionPollTimeoutSeconds"'); }); }); @@ -94,6 +101,20 @@ it('prefers EDGE_PUBLIC_API_URL when explicitly configured', function (): void { expect($manager->getApiBaseUrl())->toBe('https://edge.example.test/api'); expect($manager->buildInstallScriptUrl('token-1'))->toBe('https://edge.example.test/api/edge-agent/install.sh?token=token-1'); expect($script)->toContain('"apiUrl":"https://edge.example.test/api"'); - expect($script)->toContain('"brokerUrl":"https://edge.example.test:4300"'); + expect($script)->toContain('"brokerUrl":"https://edge.example.test/api/edge-broker"'); + }); +}); + +it('builds websocket broker urls on the traefik broker path', function (): void { + with_edge_gateway_server_state([ + 'HTTP_HOST' => 'api.truckwash.io:4433', + 'HTTP_X_FORWARDED_PROTO' => 'https', + ], function (): void { + $manager = new EdgeGatewayManagerUrlHarness(); + + expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicUrl')) + ->toBe('https://api.truckwash.io:4433/edge-broker'); + expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicWebSocketUrl', '/ws/browser-shell')) + ->toBe('wss://api.truckwash.io:4433/edge-broker/ws/browser-shell'); }); }); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayModuleRouteWiringTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayModuleRouteWiringTest.php index 2285358d..2db2765b 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayModuleRouteWiringTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayModuleRouteWiringTest.php @@ -5,6 +5,8 @@ it('registers module-scoped edge gateway operator routes', function (): void { expect($route)->not->toBeFalse(); expect($route)->toContain("'/modules/edge-gateways'"); + expect($route)->toContain("'/modules/edge-gateways/workspace/departments'"); + expect($route)->toContain("'/modules/edge-gateways/workspace/departments/{id}'"); expect($route)->toContain("'/modules/edge-gateways/{id}'"); expect($route)->toContain("'/modules/edge-gateways/install-token'"); expect($route)->toContain("'/modules/edge-gateways/install-token/{id}/status'"); diff --git a/services/nginx/app/tests/Unit/Selfserve/PlateScannerWorkspaceContractTest.php b/services/nginx/app/tests/Unit/Selfserve/PlateScannerWorkspaceContractTest.php new file mode 100644 index 00000000..c8ff0759 --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/PlateScannerWorkspaceContractTest.php @@ -0,0 +1,23 @@ +not->toBeFalse(); + expect($route)->toContain("'/numberplatescanners/{id}/rotate-key'"); + expect($route)->toContain("'lane_id'"); + + expect($scannerObject)->not->toBeFalse(); + expect($scannerObject)->toContain('public object_property $lane_id;'); + expect($scannerObject)->toContain('public function rotateApiKey(int $id): array'); + expect($scannerObject)->toContain('ADD COLUMN `lane_id` INT NULL AFTER `department_id`'); +}); + +it('uses the scanner default lane before requiring an explicit lane_id in machine button webhooks', function (): void { + $route = file_get_contents(app_path('routes/machineButtonPressRoute.php')); + + expect($route)->not->toBeFalse(); + expect($route)->toContain('$plateScanner->lane_id->value()'); + expect($route)->toContain('default lane configured for the plate scanner'); +}); diff --git a/services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php b/services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php new file mode 100644 index 00000000..a11af8f7 --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php @@ -0,0 +1,81 @@ +setInventoryFetcher(static function (): array { + return [ + 'isok' => true, + 'data' => [ + 'devices_status' => [ + 'device-key-1' => [ + '_dev_info' => [ + 'id' => 'shelly-plus-01', + 'code' => 'SPSW-001PE16EU', + 'online' => 1, + ], + 'name' => 'Entry Gate', + 'status' => [ + 'switch:0' => [ + 'output' => false, + ], + ], + ], + 'device-key-2' => [ + '_dev_info' => [ + 'id' => 'shelly-legacy-02', + 'code' => 'SHSW-1', + 'online' => 0, + ], + 'relays' => [ + ['ison' => false], + ], + ], + 'device-key-3' => [ + '_dev_info' => [ + 'id' => 'shelly-sensor-01', + 'code' => 'SHHT-1', + 'online' => 1, + ], + 'sensor' => [ + 'temperature' => 21.5, + ], + ], + ], + ], + ]; + }); + + expect($inventory->listRelayOptions())->toBe([ + [ + 'id' => 'shelly-plus-01', + 'name' => 'Entry Gate (shelly-plus-01) · SPSW-001PE16EU', + 'device_id' => 'shelly-plus-01', + 'device_name' => 'Entry Gate', + 'code' => 'SPSW-001PE16EU', + 'online' => true, + ], + [ + 'id' => 'shelly-legacy-02', + 'name' => 'shelly-legacy-02 · SHSW-1', + 'device_id' => 'shelly-legacy-02', + 'device_name' => null, + 'code' => 'SHSW-1', + 'online' => false, + ], + ]); +}); + +it('fails fast when Shelly inventory does not include owned devices status', function (): void { + $inventory = (new shelly_relay_inventory())->setInventoryFetcher(static function (): array { + return [ + 'isok' => true, + 'data' => [], + ]; + }); + + expect(fn() => $inventory->listRelayOptions()) + ->toThrow(Exception::class, 'Shelly relay inventory response was missing devices_status'); +}); diff --git a/services/php/docker-entrypoint.sh b/services/php/docker-entrypoint.sh index e19ffb28..b9af64f1 100644 --- a/services/php/docker-entrypoint.sh +++ b/services/php/docker-entrypoint.sh @@ -2,9 +2,9 @@ set -e # Config -APP_DIR="/var/www/html" -MODULE_DIR="$APP_DIR/modules/washcertificates" -LOG_FILE="/var/log/php/composer-install.log" +APP_DIR="${APP_DIR:-/var/www/html}" +MODULE_DIR="${MODULE_DIR:-$APP_DIR/modules/washcertificates}" +LOG_FILE="${LOG_FILE:-/var/log/php/composer-install.log}" # Gate auto-install (set to "true" only on one PHP container, e.g. php1) AUTO_COMPOSER_INSTALL="${AUTO_COMPOSER_INSTALL:-true}" @@ -14,17 +14,28 @@ log() { printf "[entrypoint] %s\n" "$*"; } vendor_sanity_ok() { dir="$1" autoload_file="$dir/vendor/autoload.php" + autoload_real_file="$dir/vendor/composer/autoload_real.php" aws_s3_api_file="$dir/vendor/aws/aws-sdk-php/src/data/s3/2006-03-01/api-2.json.php" if [ ! -f "$autoload_file" ]; then return 1 fi + if [ ! -f "$autoload_real_file" ]; then + log "Vendor sanity check failed: missing $autoload_real_file" + return 1 + fi + if [ -f "$dir/composer.lock" ] && [ "$dir/composer.lock" -nt "$autoload_file" ]; then log "composer.lock is newer than vendor/autoload.php in $dir" return 1 fi + if ! php -d display_errors=1 -r 'require $argv[1];' "$autoload_file" >/dev/null 2>&1; then + log "Vendor sanity check failed for $autoload_file" + return 1 + fi + if [ -f "$aws_s3_api_file" ] && ! php -l "$aws_s3_api_file" >/dev/null 2>&1; then log "Vendor sanity check failed for $aws_s3_api_file" return 1