Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b99523366 | ||
|
|
6d739cfebc | ||
|
|
20071166f8 | ||
|
|
c23168afc5 | ||
|
|
72704b7806 | ||
|
|
f7485f0767 |
@@ -9,8 +9,8 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
qodana:
|
qodana:
|
||||||
# Use GitHub-hosted runners for PR scans so untrusted code never runs on persistent internal infrastructure.
|
# CI runs on the repository's self-hosted runner pool.
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
assign-task:
|
assign-task:
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
php:
|
php:
|
||||||
name: PHP ${{ matrix.suite }} (required)
|
name: PHP ${{ matrix.suite }} (required)
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
edge-agent:
|
edge-agent:
|
||||||
name: Edge Agent (required)
|
name: Edge Agent (required)
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
|
|
||||||
edge-broker:
|
edge-broker:
|
||||||
name: Edge Broker (required)
|
name: Edge Broker (required)
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -125,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
edge-gateway-backend:
|
edge-gateway-backend:
|
||||||
name: Edge Gateway Backend (required)
|
name: Edge Gateway Backend (required)
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
env:
|
env:
|
||||||
COMPOSE_FILE: docker-compose.yml:.github/docker-compose.ci.yml
|
COMPOSE_FILE: docker-compose.yml:.github/docker-compose.ci.yml
|
||||||
COMPOSE_PROJECT_NAME: edge-gateway-backend-${{ github.run_id }}-${{ github.run_attempt }}
|
COMPOSE_PROJECT_NAME: edge-gateway-backend-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
@@ -258,7 +258,7 @@ jobs:
|
|||||||
|
|
||||||
release-manager-gate:
|
release-manager-gate:
|
||||||
name: Release Manager gate
|
name: Release Manager gate
|
||||||
runs-on: ubuntu-latest
|
runs-on: [self-hosted, Linux, X64, default]
|
||||||
needs: [php, edge-agent, edge-broker, edge-gateway-backend]
|
needs: [php, edge-agent, edge-broker, edge-gateway-backend]
|
||||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -3,6 +3,8 @@ services:
|
|||||||
traefik:
|
traefik:
|
||||||
image: traefik:2.11
|
image: traefik:2.11
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
|
group_add:
|
||||||
|
- "${DOCKER_SOCKET_GID:-65534}"
|
||||||
ports:
|
ports:
|
||||||
- "${TRAEFIK_WEB_PORT:-80}:80"
|
- "${TRAEFIK_WEB_PORT:-80}:80"
|
||||||
- "${TRAEFIK_WEBSECURE_PORT:-443}:443"
|
- "${TRAEFIK_WEBSECURE_PORT:-443}:443"
|
||||||
@@ -102,6 +104,7 @@ services:
|
|||||||
image: mysql:8.4
|
image: mysql:8.4
|
||||||
container_name: mysql-debug
|
container_name: mysql-debug
|
||||||
profiles: [dev]
|
profiles: [dev]
|
||||||
|
command: ["mysqld", "--innodb-use-native-aio=0"]
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${CONFIG_DB_DEBUG_PASSWORD:?CONFIG_DB_DEBUG_PASSWORD is required for mysql-debug}
|
MYSQL_ROOT_PASSWORD: ${CONFIG_DB_DEBUG_PASSWORD:?CONFIG_DB_DEBUG_PASSWORD is required for mysql-debug}
|
||||||
MYSQL_DATABASE: ${CONFIG_DB_DEBUG_DATABASE:-nnks_db_debug}
|
MYSQL_DATABASE: ${CONFIG_DB_DEBUG_DATABASE:-nnks_db_debug}
|
||||||
|
|||||||
+41
-1
@@ -8869,6 +8869,47 @@ paths:
|
|||||||
'403':
|
'403':
|
||||||
$ref: '#/components/responses/Forbidden'
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
|
||||||
|
/modules/self-serve/lane/wash/my-active-wash:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Modules
|
||||||
|
summary: Get the authenticated customer's active self-serve wash
|
||||||
|
description: |
|
||||||
|
Returns the latest active self-serve wash for the authenticated customer,
|
||||||
|
without requiring the frontend to know or poll a lane id.
|
||||||
|
operationId: getMyActiveSelfServeWash
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Active self-serve wash details resolved
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
lane_id:
|
||||||
|
type: integer
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
in_progress:
|
||||||
|
type: boolean
|
||||||
|
elapsed_minutes:
|
||||||
|
type: integer
|
||||||
|
session:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
customer:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
vehicle:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
/modules/self-serve/sessions:
|
/modules/self-serve/sessions:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -18714,4 +18755,3 @@ components:
|
|||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPayload'
|
$ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPayload'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+132545
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -12204,3 +12204,251 @@
|
|||||||
[Tue May 26 09:31:07 2026] 127.0.0.1:38284 Closing
|
[Tue May 26 09:31:07 2026] 127.0.0.1:38284 Closing
|
||||||
[Tue May 26 09:31:08 2026] 127.0.0.1:38290 Accepted
|
[Tue May 26 09:31:08 2026] 127.0.0.1:38290 Accepted
|
||||||
[Tue May 26 09:31:12 2026] 127.0.0.1:38290 Closing
|
[Tue May 26 09:31:12 2026] 127.0.0.1:38290 Closing
|
||||||
|
[Tue Jun 2 12:55:52 2026] PHP 8.2.15 Development Server (http://127.0.0.1:45377) started
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52720 Accepted
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52720 Closing
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52734 Accepted
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52734 Closing
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52742 Accepted
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52742 Closing
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52750 Accepted
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52750 Closing
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52766 Accepted
|
||||||
|
[Tue Jun 2 12:55:52 2026] 127.0.0.1:52766 Closing
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52770 Accepted
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52770 Closing
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52784 Accepted
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52784 Closing
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52792 Accepted
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52792 Closing
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52802 Accepted
|
||||||
|
[Tue Jun 2 12:55:53 2026] 127.0.0.1:52802 Closing
|
||||||
|
[Tue Jun 2 12:59:15 2026] PHP 8.2.15 Development Server (http://127.0.0.1:42651) started
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41826 Accepted
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41826 Closing
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41842 Accepted
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41842 Closing
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41850 Accepted
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41850 Closing
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41860 Accepted
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41860 Closing
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41874 Accepted
|
||||||
|
[Tue Jun 2 12:59:15 2026] 127.0.0.1:41874 Closing
|
||||||
|
[Tue Jun 2 12:59:16 2026] 127.0.0.1:41884 Accepted
|
||||||
|
[Tue Jun 2 12:59:16 2026] 127.0.0.1:41884 Closing
|
||||||
|
[Tue Jun 2 12:59:16 2026] 127.0.0.1:41894 Accepted
|
||||||
|
[Tue Jun 2 12:59:16 2026] 127.0.0.1:41894 Closing
|
||||||
|
[Tue Jun 2 12:59:16 2026] 127.0.0.1:41902 Accepted
|
||||||
|
[Tue Jun 2 12:59:16 2026] 127.0.0.1:41902 Closing
|
||||||
|
[Tue Jun 2 12:59:16 2026] 127.0.0.1:41916 Accepted
|
||||||
|
[Tue Jun 2 12:59:37 2026] 127.0.0.1:41916 Closing
|
||||||
|
[Tue Jun 2 13:02:13 2026] PHP 8.2.15 Development Server (http://127.0.0.1:38777) started
|
||||||
|
[Tue Jun 2 13:02:13 2026] 127.0.0.1:37238 Accepted
|
||||||
|
[Tue Jun 2 13:02:13 2026] 127.0.0.1:37238 Closing
|
||||||
|
[Tue Jun 2 13:02:13 2026] 127.0.0.1:37252 Accepted
|
||||||
|
[Tue Jun 2 13:02:13 2026] 127.0.0.1:37252 Closing
|
||||||
|
[Tue Jun 2 13:02:13 2026] 127.0.0.1:37256 Accepted
|
||||||
|
[Tue Jun 2 13:02:13 2026] 127.0.0.1:37256 Closing
|
||||||
|
[Tue Jun 2 13:02:15 2026] 127.0.0.1:35516 Accepted
|
||||||
|
[Tue Jun 2 13:02:15 2026] 127.0.0.1:35516 Closing
|
||||||
|
[Tue Jun 2 13:02:15 2026] 127.0.0.1:35518 Accepted
|
||||||
|
[Tue Jun 2 13:02:15 2026] 127.0.0.1:35518 Closing
|
||||||
|
[Tue Jun 2 13:02:15 2026] 127.0.0.1:35528 Accepted
|
||||||
|
[Tue Jun 2 13:02:15 2026] 127.0.0.1:35528 Closing
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35544 Accepted
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35544 Closing
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35552 Accepted
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35552 Closing
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35568 Accepted
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35568 Closing
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35582 Accepted
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35582 Closing
|
||||||
|
[Tue Jun 2 13:02:16 2026] 127.0.0.1:35586 Accepted
|
||||||
|
[Tue Jun 2 13:02:17 2026] 127.0.0.1:35586 Closing
|
||||||
|
[Tue Jun 2 13:02:17 2026] 127.0.0.1:35588 Accepted
|
||||||
|
[Tue Jun 2 13:02:17 2026] 127.0.0.1:35588 Closing
|
||||||
|
[Tue Jun 2 13:02:19 2026] 127.0.0.1:35596 Accepted
|
||||||
|
[Tue Jun 2 13:02:20 2026] 127.0.0.1:35596 Closing
|
||||||
|
[Tue Jun 2 13:02:20 2026] 127.0.0.1:35600 Accepted
|
||||||
|
[Tue Jun 2 13:02:20 2026] 127.0.0.1:35600 Closing
|
||||||
|
[Tue Jun 2 13:02:20 2026] 127.0.0.1:35612 Accepted
|
||||||
|
[Tue Jun 2 13:02:20 2026] 127.0.0.1:35612 Closing
|
||||||
|
[Tue Jun 2 13:02:46 2026] PHP 8.2.15 Development Server (http://127.0.0.1:39573) started
|
||||||
|
[Tue Jun 2 13:02:46 2026] 127.0.0.1:43488 Accepted
|
||||||
|
[Tue Jun 2 13:02:46 2026] 127.0.0.1:43488 Closing
|
||||||
|
[Tue Jun 2 13:02:46 2026] 127.0.0.1:43494 Accepted
|
||||||
|
[Tue Jun 2 13:02:47 2026] 127.0.0.1:43494 Closing
|
||||||
|
[Tue Jun 2 13:02:47 2026] 127.0.0.1:43502 Accepted
|
||||||
|
[Tue Jun 2 13:02:47 2026] 127.0.0.1:43502 Closing
|
||||||
|
[Tue Jun 2 13:02:47 2026] 127.0.0.1:43504 Accepted
|
||||||
|
[Tue Jun 2 13:02:47 2026] 127.0.0.1:43504 Closing
|
||||||
|
[Tue Jun 2 13:02:47 2026] 127.0.0.1:43516 Accepted
|
||||||
|
[Tue Jun 2 13:02:47 2026] 127.0.0.1:43516 Closing
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43530 Accepted
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43530 Closing
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43540 Accepted
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43540 Closing
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43550 Accepted
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43550 Closing
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43562 Accepted
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43562 Closing
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43578 Accepted
|
||||||
|
[Tue Jun 2 13:02:53 2026] 127.0.0.1:43578 Closing
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51950 Accepted
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51950 Closing
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51952 Accepted
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51952 Closing
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51964 Accepted
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51964 Closing
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51978 Accepted
|
||||||
|
[Tue Jun 2 13:02:54 2026] 127.0.0.1:51978 Closing
|
||||||
|
[Tue Jun 2 13:02:55 2026] 127.0.0.1:51994 Accepted
|
||||||
|
[Tue Jun 2 13:02:55 2026] 127.0.0.1:51994 Closing
|
||||||
|
[Tue Jun 2 13:25:35 2026] PHP 8.2.15 Development Server (http://127.0.0.1:40811) started
|
||||||
|
[Tue Jun 2 13:25:35 2026] 127.0.0.1:43736 Accepted
|
||||||
|
[Tue Jun 2 13:25:35 2026] 127.0.0.1:43736 Closing
|
||||||
|
[Tue Jun 2 13:25:35 2026] 127.0.0.1:43740 Accepted
|
||||||
|
[Tue Jun 2 13:25:35 2026] 127.0.0.1:43740 Closing
|
||||||
|
[Tue Jun 2 13:25:35 2026] 127.0.0.1:43754 Accepted
|
||||||
|
[Tue Jun 2 13:25:35 2026] 127.0.0.1:43754 Closing
|
||||||
|
[Tue Jun 2 13:25:36 2026] 127.0.0.1:43768 Accepted
|
||||||
|
[Tue Jun 2 13:25:36 2026] 127.0.0.1:43768 Closing
|
||||||
|
[Tue Jun 2 13:25:36 2026] 127.0.0.1:43776 Accepted
|
||||||
|
[Tue Jun 2 13:25:36 2026] 127.0.0.1:43776 Closing
|
||||||
|
[Tue Jun 2 13:25:52 2026] 127.0.0.1:57442 Accepted
|
||||||
|
[Tue Jun 2 13:25:52 2026] 127.0.0.1:57442 Closing
|
||||||
|
[Tue Jun 2 13:25:52 2026] 127.0.0.1:57452 Accepted
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57452 Closing
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57460 Accepted
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57460 Closing
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57472 Accepted
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57472 Closing
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57486 Accepted
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57486 Closing
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57492 Accepted
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57492 Closing
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57508 Accepted
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57508 Closing
|
||||||
|
[Tue Jun 2 13:25:53 2026] 127.0.0.1:57522 Accepted
|
||||||
|
[Tue Jun 2 13:25:54 2026] 127.0.0.1:57522 Closing
|
||||||
|
[Tue Jun 2 13:25:54 2026] 127.0.0.1:60940 Accepted
|
||||||
|
[Tue Jun 2 13:25:54 2026] 127.0.0.1:60940 Closing
|
||||||
|
[Tue Jun 2 13:25:54 2026] 127.0.0.1:60950 Accepted
|
||||||
|
[Tue Jun 2 13:25:54 2026] 127.0.0.1:60950 Closing
|
||||||
|
[Tue Jun 2 13:44:42 2026] PHP 8.2.15 Development Server (http://127.0.0.1:37125) started
|
||||||
|
[Tue Jun 2 13:44:42 2026] 127.0.0.1:45382 Accepted
|
||||||
|
[Tue Jun 2 13:44:42 2026] 127.0.0.1:45382 Closing
|
||||||
|
[Tue Jun 2 13:44:42 2026] 127.0.0.1:45388 Accepted
|
||||||
|
[Tue Jun 2 13:44:42 2026] 127.0.0.1:45388 Closing
|
||||||
|
[Tue Jun 2 13:44:42 2026] 127.0.0.1:45398 Accepted
|
||||||
|
[Tue Jun 2 13:44:43 2026] 127.0.0.1:45398 Closing
|
||||||
|
[Tue Jun 2 13:44:43 2026] 127.0.0.1:45410 Accepted
|
||||||
|
[Tue Jun 2 13:44:46 2026] 127.0.0.1:45410 Closing
|
||||||
|
[Tue Jun 2 13:44:46 2026] 127.0.0.1:53928 Accepted
|
||||||
|
[Tue Jun 2 13:44:46 2026] 127.0.0.1:53928 Closing
|
||||||
|
[Tue Jun 2 13:44:51 2026] PHP 8.2.15 Development Server (http://127.0.0.1:34643) started
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46776 Accepted
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46776 Closing
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46782 Accepted
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46782 Closing
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46796 Accepted
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46796 Closing
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46806 Accepted
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46806 Closing
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46816 Accepted
|
||||||
|
[Tue Jun 2 13:44:51 2026] 127.0.0.1:46816 Closing
|
||||||
|
[Tue Jun 2 13:44:52 2026] 127.0.0.1:46824 Accepted
|
||||||
|
[Tue Jun 2 13:44:52 2026] 127.0.0.1:46824 Closing
|
||||||
|
[Tue Jun 2 13:44:53 2026] 127.0.0.1:46836 Accepted
|
||||||
|
[Tue Jun 2 13:44:53 2026] 127.0.0.1:46836 Closing
|
||||||
|
[Tue Jun 2 13:44:53 2026] 127.0.0.1:46846 Accepted
|
||||||
|
[Tue Jun 2 13:44:53 2026] 127.0.0.1:46846 Closing
|
||||||
|
[Tue Jun 2 13:44:53 2026] 127.0.0.1:46848 Accepted
|
||||||
|
[Tue Jun 2 13:44:53 2026] 127.0.0.1:46848 Closing
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45254 Accepted
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45254 Closing
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45264 Accepted
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45264 Closing
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45276 Accepted
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45276 Closing
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45282 Accepted
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45282 Closing
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45288 Accepted
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45288 Closing
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45296 Accepted
|
||||||
|
[Tue Jun 2 13:44:54 2026] 127.0.0.1:45296 Closing
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45312 Accepted
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45312 Closing
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45314 Accepted
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45314 Closing
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45328 Accepted
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45328 Closing
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45330 Accepted
|
||||||
|
[Tue Jun 2 13:44:55 2026] 127.0.0.1:45330 Closing
|
||||||
|
[Tue Jun 2 14:14:22 2026] PHP 8.2.15 Development Server (http://127.0.0.1:41283) started
|
||||||
|
[Tue Jun 2 14:14:22 2026] 127.0.0.1:54804 Accepted
|
||||||
|
[Tue Jun 2 14:14:22 2026] 127.0.0.1:54804 Closing
|
||||||
|
[Tue Jun 2 14:14:22 2026] 127.0.0.1:54812 Accepted
|
||||||
|
[Tue Jun 2 14:14:22 2026] 127.0.0.1:54812 Closing
|
||||||
|
[Tue Jun 2 14:14:22 2026] 127.0.0.1:54816 Accepted
|
||||||
|
[Tue Jun 2 14:14:22 2026] 127.0.0.1:54816 Closing
|
||||||
|
[Tue Jun 2 14:14:22 2026] 127.0.0.1:54826 Accepted
|
||||||
|
[Tue Jun 2 14:14:23 2026] 127.0.0.1:54826 Closing
|
||||||
|
[Tue Jun 2 14:14:23 2026] 127.0.0.1:54828 Accepted
|
||||||
|
[Tue Jun 2 14:14:25 2026] 127.0.0.1:54828 Closing
|
||||||
|
[Tue Jun 2 14:14:25 2026] 127.0.0.1:45644 Accepted
|
||||||
|
[Tue Jun 2 14:14:25 2026] 127.0.0.1:45644 Closing
|
||||||
|
[Tue Jun 2 14:14:42 2026] PHP 8.2.15 Development Server (http://127.0.0.1:40157) started
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46410 Accepted
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46410 Closing
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46414 Accepted
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46414 Closing
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46424 Accepted
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46424 Closing
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46440 Accepted
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46440 Closing
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46442 Accepted
|
||||||
|
[Tue Jun 2 14:14:42 2026] 127.0.0.1:46442 Closing
|
||||||
|
[Tue Jun 2 14:14:43 2026] 127.0.0.1:46458 Accepted
|
||||||
|
[Tue Jun 2 14:14:43 2026] 127.0.0.1:46458 Closing
|
||||||
|
[Tue Jun 2 14:16:10 2026] PHP 8.2.15 Development Server (http://127.0.0.1:38915) started
|
||||||
|
[Tue Jun 2 14:16:10 2026] 127.0.0.1:54470 Accepted
|
||||||
|
[Tue Jun 2 14:16:10 2026] 127.0.0.1:54470 Closing
|
||||||
|
[Tue Jun 2 14:16:10 2026] 127.0.0.1:54482 Accepted
|
||||||
|
[Tue Jun 2 14:16:11 2026] 127.0.0.1:54482 Closing
|
||||||
|
[Tue Jun 2 14:16:11 2026] 127.0.0.1:54492 Accepted
|
||||||
|
[Tue Jun 2 14:16:11 2026] 127.0.0.1:54492 Closing
|
||||||
|
[Tue Jun 2 14:16:11 2026] 127.0.0.1:54498 Accepted
|
||||||
|
[Tue Jun 2 14:16:11 2026] 127.0.0.1:54498 Closing
|
||||||
|
[Tue Jun 2 14:16:11 2026] 127.0.0.1:54508 Accepted
|
||||||
|
[Tue Jun 2 14:16:11 2026] 127.0.0.1:54508 Closing
|
||||||
|
[Tue Jun 2 14:16:24 2026] 127.0.0.1:48044 Accepted
|
||||||
|
[Tue Jun 2 14:16:24 2026] 127.0.0.1:48044 Closing
|
||||||
|
[Tue Jun 2 14:16:25 2026] 127.0.0.1:48060 Accepted
|
||||||
|
[Tue Jun 2 14:16:25 2026] 127.0.0.1:48060 Closing
|
||||||
|
[Tue Jun 2 14:16:27 2026] 127.0.0.1:48072 Accepted
|
||||||
|
[Tue Jun 2 14:16:27 2026] 127.0.0.1:48072 Closing
|
||||||
|
[Tue Jun 2 14:16:27 2026] 127.0.0.1:48088 Accepted
|
||||||
|
[Tue Jun 2 14:16:28 2026] 127.0.0.1:48088 Closing
|
||||||
|
[Tue Jun 2 14:16:28 2026] 127.0.0.1:48094 Accepted
|
||||||
|
[Tue Jun 2 14:16:28 2026] 127.0.0.1:48094 Closing
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55000 Accepted
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55000 Closing
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55006 Accepted
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55006 Closing
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55016 Accepted
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55016 Closing
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55018 Accepted
|
||||||
|
[Tue Jun 2 14:16:34 2026] 127.0.0.1:55018 Closing
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55022 Accepted
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55022 Closing
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55032 Accepted
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55032 Closing
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55048 Accepted
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55048 Closing
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55060 Accepted
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55060 Closing
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55074 Accepted
|
||||||
|
[Tue Jun 2 14:16:35 2026] 127.0.0.1:55074 Closing
|
||||||
|
[Tue Jun 2 14:17:06 2026] 127.0.0.1:34490 Accepted
|
||||||
|
[Tue Jun 2 14:17:06 2026] 127.0.0.1:34490 Closing
|
||||||
|
|||||||
@@ -1371,6 +1371,129 @@ class coolify_manager
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function deployGithubRunners(array $input, ?int $actorUserId = null): array
|
||||||
|
{
|
||||||
|
$this->ensureSchema();
|
||||||
|
|
||||||
|
$dryRun = $this->toBool($input['dry_run'] ?? null, false);
|
||||||
|
$instanceId = (int)($input['instance_id'] ?? 0);
|
||||||
|
if ($instanceId <= 0) {
|
||||||
|
$instanceId = $this->defaultInstanceId();
|
||||||
|
}
|
||||||
|
$instance = $this->getInstance($instanceId);
|
||||||
|
$repositories = $this->githubRunnerRepositories($input);
|
||||||
|
$labels = $this->githubRunnerLabels($input['labels'] ?? null);
|
||||||
|
$countPerRepo = $this->githubRunnerCount($input['count_per_repo'] ?? $input['runner_count_per_repo'] ?? null);
|
||||||
|
$serviceName = $this->githubRunnerServiceName($input['service_name'] ?? null);
|
||||||
|
$resourceUuid = $this->nullableString($input['service_uuid'] ?? null)
|
||||||
|
?? $this->nullableString($this->coolifyConfigValue('github_runner_service_uuid', ''));
|
||||||
|
$token = $this->githubRunnerToken($input);
|
||||||
|
$template = $this->githubRunnerComposeTemplate($repositories, $labels, $countPerRepo);
|
||||||
|
$hash = $this->composeHash($template);
|
||||||
|
|
||||||
|
$plan = [
|
||||||
|
'type' => 'deploy_github_runners',
|
||||||
|
'instance_id' => $instanceId,
|
||||||
|
'service_uuid' => $resourceUuid,
|
||||||
|
'service_name' => $serviceName,
|
||||||
|
'repositories' => $repositories,
|
||||||
|
'labels' => $labels,
|
||||||
|
'count_per_repo' => $countPerRepo,
|
||||||
|
'compose_hash' => $hash,
|
||||||
|
'action' => $resourceUuid === null ? 'create' : 'update',
|
||||||
|
'token_set' => $token !== '',
|
||||||
|
'token_source' => trim((string)($input['github_token'] ?? $input['token'] ?? '')) !== '' ? 'request' : 'config',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->audit(null, $instanceId, null, 'github_runners_planned', $actorUserId, 'info', $plan);
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'dry_run' => true,
|
||||||
|
'mutated' => false,
|
||||||
|
'planned' => [$plan],
|
||||||
|
'applied' => [],
|
||||||
|
'errors' => [],
|
||||||
|
'service_uuid' => $resourceUuid,
|
||||||
|
'service_name' => $serviceName,
|
||||||
|
'compose_hash' => $hash,
|
||||||
|
'repositories' => $repositories,
|
||||||
|
'labels' => $labels,
|
||||||
|
'count_per_repo' => $countPerRepo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
throw new RuntimeException('GitHub runner token is required to deploy self-hosted runners.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = $this->clientForInstance($instance);
|
||||||
|
$apiResult = [];
|
||||||
|
$action = $resourceUuid === null ? 'created' : 'updated';
|
||||||
|
if ($resourceUuid === null) {
|
||||||
|
$apiResult = $client->createService($this->githubRunnerServicePayload($instance, $input, $serviceName, $template, false));
|
||||||
|
$resourceUuid = trim((string)($apiResult['uuid'] ?? ''));
|
||||||
|
if ($resourceUuid === '') {
|
||||||
|
throw new RuntimeException('Coolify did not return a GitHub runner service UUID.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$apiResult = $client->updateService($resourceUuid, $this->githubRunnerServicePayload($instance, $input, $serviceName, $template, true));
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
if (!str_contains(strtolower($throwable->getMessage()), '404')
|
||||||
|
&& !str_contains(strtolower($throwable->getMessage()), 'not found')) {
|
||||||
|
throw $throwable;
|
||||||
|
}
|
||||||
|
$apiResult = $client->createService($this->githubRunnerServicePayload($instance, $input, $serviceName, $template, false));
|
||||||
|
$resourceUuid = trim((string)($apiResult['uuid'] ?? ''));
|
||||||
|
if ($resourceUuid === '') {
|
||||||
|
throw new RuntimeException('Coolify did not return a GitHub runner service UUID.');
|
||||||
|
}
|
||||||
|
$action = 'created';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$client->updateServiceEnvsBulk($resourceUuid, ['GITHUB_RUNNER_TOKEN' => $token]);
|
||||||
|
$start = $this->startOrRestartService($client, $resourceUuid, $action === 'updated');
|
||||||
|
$deployment = $client->deployResource($resourceUuid, false);
|
||||||
|
|
||||||
|
$this->setModuleConfigValue('Coolify', 'github_runner_service_uuid', $resourceUuid, 'string');
|
||||||
|
$this->setModuleConfigValue('Coolify', 'github_runner_frontend_repository', $repositories['frontend'], 'string');
|
||||||
|
$this->setModuleConfigValue('Coolify', 'github_runner_backend_repository', $repositories['backend'], 'string');
|
||||||
|
$this->setModuleConfigValue('Coolify', 'github_runner_labels', implode(',', $labels), 'string');
|
||||||
|
$this->setModuleConfigValue('Coolify', 'github_runner_count_per_repo', (string)$countPerRepo, 'int');
|
||||||
|
if ($this->toBool($input['persist_token'] ?? null, false)) {
|
||||||
|
$this->setModuleConfigValue('Coolify', 'github_runner_token', replication_secret_box::encrypt($token), 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
$applied = array_replace($plan, [
|
||||||
|
'action' => $action,
|
||||||
|
'service_uuid' => $resourceUuid,
|
||||||
|
'coolify' => self::redactCoolifyResponse($apiResult),
|
||||||
|
'start' => self::redactCoolifyResponse(is_array($start) ? $start : []),
|
||||||
|
'deployment' => self::redactCoolifyResponse($deployment),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->audit(null, $instanceId, null, 'github_runners_deployed', $actorUserId, 'info', $applied);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'dry_run' => false,
|
||||||
|
'mutated' => true,
|
||||||
|
'planned' => [$plan],
|
||||||
|
'applied' => [$applied],
|
||||||
|
'errors' => [],
|
||||||
|
'action' => $action,
|
||||||
|
'service_uuid' => $resourceUuid,
|
||||||
|
'service_name' => $serviceName,
|
||||||
|
'compose_hash' => $hash,
|
||||||
|
'repositories' => $repositories,
|
||||||
|
'labels' => $labels,
|
||||||
|
'count_per_repo' => $countPerRepo,
|
||||||
|
'deployment' => self::redactCoolifyResponse($deployment),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function gatewayApiCodeVersionLabel(array $target): string
|
private function gatewayApiCodeVersionLabel(array $target): string
|
||||||
{
|
{
|
||||||
$channelSlug = trim((string)($target['channel_slug'] ?? 'gateway'));
|
$channelSlug = trim((string)($target['channel_slug'] ?? 'gateway'));
|
||||||
@@ -3266,6 +3389,141 @@ class coolify_manager
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function githubRunnerRepositories(array $input): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'frontend' => $this->normalizeGithubRepository(
|
||||||
|
$input['frontend_repository'] ?? $input['frontend_repo'] ?? $this->coolifyConfigValue('github_runner_frontend_repository', 'copenhagentruckwash/pleno-vue'),
|
||||||
|
'frontend'
|
||||||
|
),
|
||||||
|
'backend' => $this->normalizeGithubRepository(
|
||||||
|
$input['backend_repository'] ?? $input['backend_repo'] ?? $this->coolifyConfigValue('github_runner_backend_repository', 'copenhagentruckwash/api'),
|
||||||
|
'backend'
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeGithubRepository(mixed $value, string $label): string
|
||||||
|
{
|
||||||
|
$repository = trim((string)$value);
|
||||||
|
$repository = preg_replace('#^https://github\.com/#i', '', $repository) ?? $repository;
|
||||||
|
$repository = preg_replace('#^git@github\.com:#i', '', $repository) ?? $repository;
|
||||||
|
$repository = preg_replace('#\.git$#i', '', $repository) ?? $repository;
|
||||||
|
$repository = trim($repository, " \t\n\r\0\x0B/");
|
||||||
|
if (!preg_match('#^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$#', $repository)) {
|
||||||
|
throw new RuntimeException('GitHub ' . $label . ' repository must be in owner/repo format.');
|
||||||
|
}
|
||||||
|
return $repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function githubRunnerLabels(mixed $value): array
|
||||||
|
{
|
||||||
|
$raw = trim((string)($value ?? ''));
|
||||||
|
if ($raw === '') {
|
||||||
|
$raw = $this->coolifyConfigValue('github_runner_labels', 'self-hosted,Linux,X64,default');
|
||||||
|
}
|
||||||
|
|
||||||
|
$labels = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn(string $label): string => trim($label),
|
||||||
|
preg_split('/[,\s]+/', $raw) ?: []
|
||||||
|
))));
|
||||||
|
|
||||||
|
return $labels !== [] ? $labels : ['self-hosted', 'Linux', 'X64', 'default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function githubRunnerCount(mixed $value): int
|
||||||
|
{
|
||||||
|
$count = (int)($value ?? 0);
|
||||||
|
if ($count <= 0) {
|
||||||
|
$count = (int)$this->coolifyConfigValue('github_runner_count_per_repo', '1');
|
||||||
|
}
|
||||||
|
return max(1, min(10, $count));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function githubRunnerServiceName(mixed $value): string
|
||||||
|
{
|
||||||
|
$name = strtolower(trim((string)($value ?? 'truckwash-github-runners')));
|
||||||
|
$name = preg_replace('/[^a-z0-9-]+/', '-', $name) ?: '';
|
||||||
|
$name = trim($name, '-') ?: 'truckwash-github-runners';
|
||||||
|
return substr($name, 0, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function githubRunnerToken(array $input): string
|
||||||
|
{
|
||||||
|
$token = trim((string)($input['github_token'] ?? $input['token'] ?? ''));
|
||||||
|
if ($token !== '') {
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
$envToken = trim((string)(getenv('GITHUB_RUNNER_TOKEN') ?: getenv('GITHUB_TOKEN') ?: ''));
|
||||||
|
if ($envToken !== '') {
|
||||||
|
return $envToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return replication_secret_box::decrypt($this->coolifyConfigValue('github_runner_token', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function githubRunnerComposeTemplate(array $repositories, array $labels, int $countPerRepo): array
|
||||||
|
{
|
||||||
|
$lines = ['services:'];
|
||||||
|
foreach ($repositories as $key => $repository) {
|
||||||
|
for ($index = 1; $index <= $countPerRepo; $index++) {
|
||||||
|
$service = 'github-runner-' . $key . '-' . $index;
|
||||||
|
$runnerName = 'truckwash-' . $key . '-' . $index;
|
||||||
|
$runnerLabels = array_values(array_unique(array_merge($labels, [$key])));
|
||||||
|
$lines = array_merge($lines, [
|
||||||
|
' ' . $service . ':',
|
||||||
|
' image: myoung34/github-runner:latest',
|
||||||
|
' restart: unless-stopped',
|
||||||
|
' environment:',
|
||||||
|
' REPO_URL: ' . self::yamlScalar('https://github.com/' . $repository),
|
||||||
|
' RUNNER_NAME: ' . self::yamlScalar($runnerName),
|
||||||
|
' RUNNER_SCOPE: repo',
|
||||||
|
' RUNNER_WORKDIR: /tmp/runner/work',
|
||||||
|
' LABELS: ' . self::yamlScalar(implode(',', $runnerLabels)),
|
||||||
|
' EPHEMERAL: "false"',
|
||||||
|
' RUN_AS_ROOT: "true"',
|
||||||
|
' ACCESS_TOKEN: ${GITHUB_RUNNER_TOKEN}',
|
||||||
|
' volumes:',
|
||||||
|
' - /var/run/docker.sock:/var/run/docker.sock',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'compose' => implode("\n", $lines) . "\n",
|
||||||
|
'env' => 'GITHUB_RUNNER_TOKEN=${GITHUB_RUNNER_TOKEN}',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function githubRunnerServicePayload(array $instance, array $input, string $serviceName, array $template, bool $update): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'name' => $serviceName,
|
||||||
|
'description' => 'Truckwash GitHub self-hosted runners for frontend and backend workflows.',
|
||||||
|
'instant_deploy' => false,
|
||||||
|
'docker_compose_raw' => $this->encodedDockerCompose($template),
|
||||||
|
'force_domain_override' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$update) {
|
||||||
|
$payload = array_replace($payload, [
|
||||||
|
'project_uuid' => $this->targetMapping($input, $instance, 'project_uuid'),
|
||||||
|
'environment_name' => $this->targetMapping($input, $instance, 'environment_name') ?: 'production',
|
||||||
|
'environment_uuid' => $this->targetMapping($input, $instance, 'environment_uuid'),
|
||||||
|
'server_uuid' => $this->targetMapping($input, $instance, 'server_uuid'),
|
||||||
|
'destination_uuid' => $this->targetMapping($input, $instance, 'destination_uuid'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter($payload, static fn($value): bool => $value !== null && $value !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function yamlScalar(string $value): string
|
||||||
|
{
|
||||||
|
return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $value) . '"';
|
||||||
|
}
|
||||||
|
|
||||||
private function publicLoadBalancerConfig(array $config): array
|
private function publicLoadBalancerConfig(array $config): array
|
||||||
{
|
{
|
||||||
unset($config['token']);
|
unset($config['token']);
|
||||||
|
|||||||
@@ -154,6 +154,12 @@ class coolify_schema_bootstrap
|
|||||||
self::ensureModuleConfigDefault('Coolify', 'hetzner_cloud_api_token', '', 'string');
|
self::ensureModuleConfigDefault('Coolify', 'hetzner_cloud_api_token', '', 'string');
|
||||||
self::ensureModuleConfigDefault('Coolify', 'public_gateway_host', 'api-v2.truckwash.io', 'string');
|
self::ensureModuleConfigDefault('Coolify', 'public_gateway_host', 'api-v2.truckwash.io', 'string');
|
||||||
self::ensureModuleConfigDefault('Coolify', 'public_gateway_probe_path', '', 'string');
|
self::ensureModuleConfigDefault('Coolify', 'public_gateway_probe_path', '', 'string');
|
||||||
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_token', '', 'string');
|
||||||
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_service_uuid', '', 'string');
|
||||||
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_frontend_repository', 'copenhagentruckwash/pleno-vue', 'string');
|
||||||
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_backend_repository', 'copenhagentruckwash/api', 'string');
|
||||||
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_labels', 'self-hosted,Linux,X64,default', 'string');
|
||||||
|
self::ensureModuleConfigDefault('Coolify', 'github_runner_count_per_repo', '1', 'int');
|
||||||
|
|
||||||
self::ensureDefaultGateway('node1.truckwash.io', '94.130.142.41', 10);
|
self::ensureDefaultGateway('node1.truckwash.io', '94.130.142.41', 10);
|
||||||
self::ensureDefaultGateway('node2.truckwash.io', '65.21.214.30', 20);
|
self::ensureDefaultGateway('node2.truckwash.io', '65.21.214.30', 20);
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace modules\coolify\config;
|
||||||
|
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class coolify_github_runner_backend_repository_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setupConfigVariable(
|
||||||
|
'Coolify',
|
||||||
|
'github_runner_backend_repository',
|
||||||
|
'string',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'GitHub backend repository that receives Coolify-managed self-hosted runners.',
|
||||||
|
'copenhagentruckwash/api',
|
||||||
|
false,
|
||||||
|
'copenhagentruckwash/api'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace modules\coolify\config;
|
||||||
|
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class coolify_github_runner_count_per_repo_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setupConfigVariable(
|
||||||
|
'Coolify',
|
||||||
|
'github_runner_count_per_repo',
|
||||||
|
'int',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'Number of self-hosted GitHub runner containers to deploy per repository.',
|
||||||
|
'1',
|
||||||
|
false,
|
||||||
|
'1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace modules\coolify\config;
|
||||||
|
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class coolify_github_runner_frontend_repository_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setupConfigVariable(
|
||||||
|
'Coolify',
|
||||||
|
'github_runner_frontend_repository',
|
||||||
|
'string',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'GitHub frontend repository that receives Coolify-managed self-hosted runners.',
|
||||||
|
'copenhagentruckwash/pleno-vue',
|
||||||
|
false,
|
||||||
|
'copenhagentruckwash/pleno-vue'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace modules\coolify\config;
|
||||||
|
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class coolify_github_runner_labels_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setupConfigVariable(
|
||||||
|
'Coolify',
|
||||||
|
'github_runner_labels',
|
||||||
|
'string',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'Comma-separated GitHub Actions runner labels registered on each Coolify-managed runner.',
|
||||||
|
'self-hosted,Linux,X64,default',
|
||||||
|
false,
|
||||||
|
'self-hosted,Linux,X64,default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace modules\coolify\config;
|
||||||
|
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class coolify_github_runner_service_uuid_c
|
||||||
|
{
|
||||||
|
use module_config_variable;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setupConfigVariable(
|
||||||
|
'Coolify',
|
||||||
|
'github_runner_service_uuid',
|
||||||
|
'string',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'Coolify service UUID for the managed GitHub self-hosted runner stack.',
|
||||||
|
'abc123...',
|
||||||
|
false,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace modules\coolify\config;
|
||||||
|
|
||||||
|
use classes\replication_secret_box;
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
class coolify_github_runner_token_c
|
||||||
|
{
|
||||||
|
use module_config_variable {
|
||||||
|
setVariableValue as private traitSetVariableValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setupConfigVariable(
|
||||||
|
'Coolify',
|
||||||
|
'github_runner_token',
|
||||||
|
'string',
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'GitHub PAT used to register Coolify-managed self-hosted repository runners.',
|
||||||
|
'github_pat_...',
|
||||||
|
true,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVariableValue(mixed $value): void
|
||||||
|
{
|
||||||
|
$value = trim((string)($value ?? ''));
|
||||||
|
if ($value !== '' && !str_starts_with($value, 'twsec:v1:')) {
|
||||||
|
$value = replication_secret_box::encrypt($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->traitSetVariableValue($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2962,7 +2962,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
|||||||
|
|
||||||
return array_values(array_filter($questions, static function (array $question) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
return array_values(array_filter($questions, static function (array $question) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
||||||
return (int)($question['department'] ?? 0) === $departmentId
|
return (int)($question['department'] ?? 0) === $departmentId
|
||||||
&& (int)($question['lane'] ?? 0) === $laneId
|
&& ((int)($question['lane'] ?? 0) === 0 || (int)($question['lane'] ?? 0) === $laneId)
|
||||||
&& (int)($question['product'] ?? 0) === $vehicleTypeId;
|
&& (int)($question['product'] ?? 0) === $vehicleTypeId;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -3005,7 +3005,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
|||||||
return array_values(array_filter($conditions, static function (array $condition) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
return array_values(array_filter($conditions, static function (array $condition) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
||||||
return (int)($condition['machine_type_id'] ?? 0) === 0
|
return (int)($condition['machine_type_id'] ?? 0) === 0
|
||||||
&& (int)($condition['department'] ?? 0) === $departmentId
|
&& (int)($condition['department'] ?? 0) === $departmentId
|
||||||
&& (int)($condition['lane'] ?? 0) === $laneId
|
&& ((int)($condition['lane'] ?? 0) === 0 || (int)($condition['lane'] ?? 0) === $laneId)
|
||||||
&& (int)($condition['product'] ?? 0) === $vehicleTypeId;
|
&& (int)($condition['product'] ?? 0) === $vehicleTypeId;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -3049,7 +3049,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
|||||||
return array_values(array_filter($tasks, static function (array $task) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
return array_values(array_filter($tasks, static function (array $task) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
||||||
return (int)($task['machine_type_id'] ?? 0) === 0
|
return (int)($task['machine_type_id'] ?? 0) === 0
|
||||||
&& (int)($task['department'] ?? 0) === $departmentId
|
&& (int)($task['department'] ?? 0) === $departmentId
|
||||||
&& (int)($task['lane'] ?? 0) === $laneId
|
&& ((int)($task['lane'] ?? 0) === 0 || (int)($task['lane'] ?? 0) === $laneId)
|
||||||
&& (int)($task['product'] ?? 0) === $vehicleTypeId;
|
&& (int)($task['product'] ?? 0) === $vehicleTypeId;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,8 +176,7 @@ trait selfserve_lane_command_t
|
|||||||
if (!$this->isRelayConfigured(selfserve_lane_relay::MACHINE)) {
|
if (!$this->isRelayConfigured(selfserve_lane_relay::MACHINE)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$active_wash = new selfserve_wash_flow();
|
if ($this->isMachineWashSelectedAndAvailableForStart()) {
|
||||||
if ($active_wash->isMachineAllowedToStartWash($this->id)) {
|
|
||||||
try {
|
try {
|
||||||
$this->setMachineRelayStatusHard(true);
|
$this->setMachineRelayStatusHard(true);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
@@ -193,6 +192,57 @@ trait selfserve_lane_command_t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep the program picker relay aligned with the selected wash mode at START.
|
||||||
|
* It is ON only when the active self-serve session is allowed to start machine wash.
|
||||||
|
*/
|
||||||
|
protected function setProgramPickerRelayStatusForWashStart(): void
|
||||||
|
{
|
||||||
|
if (!$this->isRelayConfigured(selfserve_lane_relay::MACHINE_PROGRAM_PICKER)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$shouldEnable = $this->isMachineWashSelectedAndAvailableForStart();
|
||||||
|
$this->setMachineProgramPickerRelayStatusHard($shouldEnable);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Best effort only; wash start must continue.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isMachineWashSelectedAndAvailableForStart(): bool
|
||||||
|
{
|
||||||
|
if (!$this->isMachineServiceSelectedForWashStart()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (new selfserve_wash_flow())->isMachineAllowedToStartWash((int)$this->id);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isMachineServiceSelectedForWashStart(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (method_exists($this, 'getLaneCache') && defined(self::class . '::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES')) {
|
||||||
|
$services = $this->getLaneCache((int)$this->id, self::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES);
|
||||||
|
if (is_array($services)) {
|
||||||
|
foreach ($services as $service) {
|
||||||
|
if (strtoupper((string)$service) === 'MACHINE') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Fall through to fail-closed when the selected service cache is unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected function openEntrancePortForWashStart(): void
|
protected function openEntrancePortForWashStart(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -277,27 +327,15 @@ trait selfserve_lane_command_t
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure cleaner relay is enabled whenever wash starts.
|
|
||||||
$this->turnOnCleanerRelayForWashStart();
|
$this->turnOnCleanerRelayForWashStart();
|
||||||
// Ensure the machine relay is ON when a wash starts, when it is allowed.
|
$this->setProgramPickerRelayStatusForWashStart();
|
||||||
$this->setMachineRelayStatusForWashStart();
|
$this->setMachineRelayStatusForWashStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resolveSelfServeActionWashModeForStart(): string
|
protected function resolveSelfServeActionWashModeForStart(): string
|
||||||
{
|
{
|
||||||
try {
|
if ($this->isMachineServiceSelectedForWashStart()) {
|
||||||
if (method_exists($this, 'getLaneCache') && defined(self::class . '::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES')) {
|
return selfserve_studio_actions::MODE_MACHINE;
|
||||||
$services = $this->getLaneCache((int)$this->id, self::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES);
|
|
||||||
if (is_array($services)) {
|
|
||||||
foreach ($services as $service) {
|
|
||||||
if (strtoupper((string)$service) === 'MACHINE') {
|
|
||||||
return selfserve_studio_actions::MODE_MACHINE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable) {
|
|
||||||
// Fall through to manual mode when the cached service set is unavailable.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return selfserve_studio_actions::MODE_MANUAL;
|
return selfserve_studio_actions::MODE_MANUAL;
|
||||||
@@ -382,14 +420,16 @@ trait selfserve_lane_command_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable relays after STOP in deterministic order:
|
* Disable relays before opening exit gates in deterministic order:
|
||||||
* 1. Cleaner relay
|
* 1. Cleaner relay
|
||||||
* 2. Machine relay
|
* 2. Program picker relay
|
||||||
|
* 3. Machine relay
|
||||||
*/
|
*/
|
||||||
protected function turnOffRelaysAfterStop(): void
|
protected function turnOffRelaysAfterStop(): void
|
||||||
{
|
{
|
||||||
$relays = [
|
$relays = [
|
||||||
selfserve_lane_relay::MACHINE_CLEANER,
|
selfserve_lane_relay::MACHINE_CLEANER,
|
||||||
|
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||||
selfserve_lane_relay::MACHINE,
|
selfserve_lane_relay::MACHINE,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -563,8 +603,17 @@ trait selfserve_lane_command_t
|
|||||||
// Set the customer number and license plate
|
// Set the customer number and license plate
|
||||||
$this->setCustomerNumber($customer_number);
|
$this->setCustomerNumber($customer_number);
|
||||||
$this->setLicensePlate($license_plate);
|
$this->setLicensePlate($license_plate);
|
||||||
// Mark the lane occupied before any physical entrance relay side effects.
|
// Mark the lane occupied before any physical relay or gate side effects.
|
||||||
$this->setLaneStatus(selfserve_lane_status::OCCUPIED);
|
$this->setLaneStatus(selfserve_lane_status::OCCUPIED);
|
||||||
|
$this->runRelaySideEffectsForWashStart($arguments);
|
||||||
|
$this->runPublishedStudioActions(
|
||||||
|
selfserve_studio_actions::EVENT_WASH_START_COMMAND,
|
||||||
|
$this->resolveSelfServeActionWashModeForStart(),
|
||||||
|
[
|
||||||
|
'customer_number' => (int)$customer_number,
|
||||||
|
'reg' => $license_plate,
|
||||||
|
]
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
// Gateway timeouts are ambiguous because the relay may already have received
|
// Gateway timeouts are ambiguous because the relay may already have received
|
||||||
// the pulse, so openEntrancePortForWashStart() reports them and continues.
|
// the pulse, so openEntrancePortForWashStart() reports them and continues.
|
||||||
@@ -580,15 +629,6 @@ trait selfserve_lane_command_t
|
|||||||
$this->setLaneState(selfserve_lane_state::IN_WASH);
|
$this->setLaneState(selfserve_lane_state::IN_WASH);
|
||||||
// Start the wash timer
|
// Start the wash timer
|
||||||
$this->setWashStartTime(time());
|
$this->setWashStartTime(time());
|
||||||
$this->runPublishedStudioActions(
|
|
||||||
selfserve_studio_actions::EVENT_WASH_START_COMMAND,
|
|
||||||
$this->resolveSelfServeActionWashModeForStart(),
|
|
||||||
[
|
|
||||||
'customer_number' => (int)$customer_number,
|
|
||||||
'reg' => $license_plate,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
$this->runRelaySideEffectsForWashStart($arguments);
|
|
||||||
// Log the lane start event
|
// Log the lane start event
|
||||||
$this->logLaneAction(selfserve_lane_log_action::START_WASH);
|
$this->logLaneAction(selfserve_lane_log_action::START_WASH);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -604,6 +644,8 @@ trait selfserve_lane_command_t
|
|||||||
}
|
}
|
||||||
// Snapshot the physical machine ON signal before session completion/reset.
|
// Snapshot the physical machine ON signal before session completion/reset.
|
||||||
$machine_start_triggered = $this->hasMachineStartSignalForStop();
|
$machine_start_triggered = $this->hasMachineStartSignalForStop();
|
||||||
|
// Turn off relays before any configured or default exit gate opens.
|
||||||
|
$this->turnOffRelaysAfterStop();
|
||||||
$this->runPublishedStudioActions(
|
$this->runPublishedStudioActions(
|
||||||
selfserve_studio_actions::EVENT_WASH_STOP_COMMAND,
|
selfserve_studio_actions::EVENT_WASH_STOP_COMMAND,
|
||||||
$machine_start_triggered ? selfserve_studio_actions::MODE_MACHINE : selfserve_studio_actions::MODE_MANUAL,
|
$machine_start_triggered ? selfserve_studio_actions::MODE_MACHINE : selfserve_studio_actions::MODE_MANUAL,
|
||||||
@@ -616,8 +658,6 @@ trait selfserve_lane_command_t
|
|||||||
// Open the exit port. Gateway timeouts are ambiguous because
|
// Open the exit port. Gateway timeouts are ambiguous because
|
||||||
// the relay may already have received the pulse.
|
// the relay may already have received the pulse.
|
||||||
$this->openExitPortForWashStop();
|
$this->openExitPortForWashStop();
|
||||||
// Turn off relays in deterministic order after STOP
|
|
||||||
$this->turnOffRelaysAfterStop();
|
|
||||||
// Log the lane stop event
|
// Log the lane stop event
|
||||||
$this->logLaneAction(selfserve_lane_log_action::STOP_WASH);
|
$this->logLaneAction(selfserve_lane_log_action::STOP_WASH);
|
||||||
// Invoice the customer
|
// Invoice the customer
|
||||||
|
|||||||
@@ -9234,6 +9234,47 @@ paths:
|
|||||||
'403':
|
'403':
|
||||||
$ref: '#/components/responses/Forbidden'
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
|
||||||
|
/modules/self-serve/lane/wash/my-active-wash:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Modules
|
||||||
|
summary: Get the authenticated customer's active self-serve wash
|
||||||
|
description: |
|
||||||
|
Returns the latest active self-serve wash for the authenticated customer,
|
||||||
|
without requiring the frontend to know or poll a lane id.
|
||||||
|
operationId: getMyActiveSelfServeWash
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Active self-serve wash details resolved
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
lane_id:
|
||||||
|
type: integer
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
in_progress:
|
||||||
|
type: boolean
|
||||||
|
elapsed_minutes:
|
||||||
|
type: integer
|
||||||
|
session:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
customer:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
vehicle:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/NotFound'
|
||||||
|
|
||||||
/modules/self-serve/sessions:
|
/modules/self-serve/sessions:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use objects\customer_vehicles_o;
|
|||||||
use objects\department_lanes_o;
|
use objects\department_lanes_o;
|
||||||
use objects\department_selfserve_tasks_o;
|
use objects\department_selfserve_tasks_o;
|
||||||
use objects\department_selfserve_vehicle_conditions_o;
|
use objects\department_selfserve_vehicle_conditions_o;
|
||||||
|
use objects\departments_o;
|
||||||
use objects\logs_o;
|
use objects\logs_o;
|
||||||
use traits\route_t;
|
use traits\route_t;
|
||||||
|
|
||||||
@@ -131,9 +132,9 @@ class departmentSelfserveVehicleConditionsRoute
|
|||||||
$lane_id = (int)self::getParameter('lane_id');
|
$lane_id = (int)self::getParameter('lane_id');
|
||||||
$reg = selfserve::standardize_registration((string)self::getParameter('reg'));
|
$reg = selfserve::standardize_registration((string)self::getParameter('reg'));
|
||||||
|
|
||||||
$lane = $this->assertLaneAccess($user, $lane_id);
|
$lane = $this->assertLaneAccess($user, $lane_id, $has_global, $has_own);
|
||||||
$customer_number = null;
|
$customer_number = null;
|
||||||
if (!$has_global && $has_own) {
|
if ($has_own && (!$has_global || !$this->userHasLaneDepartmentAccess($user, $lane))) {
|
||||||
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
|
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
|
||||||
}
|
}
|
||||||
$vehicle_type_id = $this->resolveVehicleTypeIdFromQuery();
|
$vehicle_type_id = $this->resolveVehicleTypeIdFromQuery();
|
||||||
@@ -171,7 +172,7 @@ class departmentSelfserveVehicleConditionsRoute
|
|||||||
try {
|
try {
|
||||||
if (self::isParametersSet(['session_id'])) {
|
if (self::isParametersSet(['session_id'])) {
|
||||||
$summary = $flow->getSessionSummary((int)self::getParameter('session_id'));
|
$summary = $flow->getSessionSummary((int)self::getParameter('session_id'));
|
||||||
$this->assertSummaryAccess($user, $summary, $has_global, 'list_department_selfserve_vehicle_conditions');
|
$this->assertSummaryAccess($user, $summary, $has_global, $has_own, 'list_department_selfserve_vehicle_conditions');
|
||||||
|
|
||||||
if ($this->shouldRefreshSummaryForVehicleType($summary, $vehicle_type_id)) {
|
if ($this->shouldRefreshSummaryForVehicleType($summary, $vehicle_type_id)) {
|
||||||
$summary = $flow->synchronizeSession(
|
$summary = $flow->synchronizeSession(
|
||||||
@@ -193,9 +194,9 @@ class departmentSelfserveVehicleConditionsRoute
|
|||||||
$lane_id = (int)self::getParameter('lane_id');
|
$lane_id = (int)self::getParameter('lane_id');
|
||||||
$reg = selfserve::standardize_registration((string)self::getParameter('reg'));
|
$reg = selfserve::standardize_registration((string)self::getParameter('reg'));
|
||||||
|
|
||||||
$this->assertLaneAccess($user, $lane_id);
|
$lane = $this->assertLaneAccess($user, $lane_id, $has_global, $has_own);
|
||||||
$customer_number = null;
|
$customer_number = null;
|
||||||
if (!$has_global && $has_own) {
|
if ($has_own && (!$has_global || !$this->userHasLaneDepartmentAccess($user, $lane))) {
|
||||||
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
|
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +205,7 @@ class departmentSelfserveVehicleConditionsRoute
|
|||||||
} else {
|
} else {
|
||||||
$summary = $flow->getLatestSessionSummary($lane_id, $reg);
|
$summary = $flow->getLatestSessionSummary($lane_id, $reg);
|
||||||
}
|
}
|
||||||
$this->assertSummaryAccess($user, $summary, $has_global, 'list_department_selfserve_vehicle_conditions');
|
$this->assertSummaryAccess($user, $summary, $has_global, $has_own, 'list_department_selfserve_vehicle_conditions');
|
||||||
$response->success($summary);
|
$response->success($summary);
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
$response->error($e->getMessage(), 404);
|
$response->error($e->getMessage(), 404);
|
||||||
@@ -527,7 +528,13 @@ class departmentSelfserveVehicleConditionsRoute
|
|||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertLaneAccess(object $user, int $laneId): department_lanes_o
|
private function assertLaneAccess(
|
||||||
|
object $user,
|
||||||
|
int $laneId,
|
||||||
|
bool $hasGlobalPermission = true,
|
||||||
|
bool $hasOwnPermission = false,
|
||||||
|
string $elevatedPermission = 'list_department_selfserve_vehicle_conditions'
|
||||||
|
): department_lanes_o
|
||||||
{
|
{
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
@@ -536,17 +543,77 @@ class departmentSelfserveVehicleConditionsRoute
|
|||||||
$response->error('Department lane not found', 404);
|
$response->error('Department lane not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($hasGlobalPermission && $this->userHasLaneDepartmentAccess($user, $lane)) {
|
||||||
|
return $lane;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasOwnPermission && $this->isCustomerSelfServeLaneEnabled($lane)) {
|
||||||
|
return $lane;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasGlobalPermission) {
|
||||||
|
$lane_department_id = (int)$lane->department->value();
|
||||||
|
$this->forbidDepartmentAccess($lane_department_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->forbidden([$elevatedPermission]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function userHasLaneDepartmentAccess(object $user, department_lanes_o $lane): bool
|
||||||
|
{
|
||||||
$lane_department_id = (int)$lane->department->value();
|
$lane_department_id = (int)$lane->department->value();
|
||||||
$authorized_department_ids = array_values(array_filter(
|
$authorized_department_ids = array_values(array_filter(
|
||||||
array_map('intval', (array)$user->getGroup()->getDepartments()),
|
array_map('intval', (array)$user->getGroup()->getDepartments()),
|
||||||
static fn(int $department_id): bool => $department_id > 0
|
static fn(int $department_id): bool => $department_id > 0
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!in_array($lane_department_id, $authorized_department_ids, true)) {
|
return in_array($lane_department_id, $authorized_department_ids, true);
|
||||||
$this->forbidDepartmentAccess($lane_department_id);
|
}
|
||||||
|
|
||||||
|
private function isCustomerSelfServeLaneEnabled(department_lanes_o $lane): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (!$lane->isSelfServeEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$department = (new departments_o())->select((int)$lane->department->value());
|
||||||
|
return $department->exists() && $department->getSelfServeEnabled();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function summaryBelongsToCustomer(object $user, array $summary): bool
|
||||||
|
{
|
||||||
|
$session_customer_number = $summary['session']['customer_number'] ?? null;
|
||||||
|
if ($session_customer_number !== null && (int)$session_customer_number === (int)$user->customer_number->value()) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $lane;
|
$reg = (string)($summary['session']['reg'] ?? '');
|
||||||
|
$vehicle_o = (new customer_vehicles_o())->selectByPlate($reg);
|
||||||
|
return $vehicle_o->exists() && (int)$vehicle_o->customer_id->value() === (int)$user->customer_number->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function summaryDepartmentId(array $summary): int
|
||||||
|
{
|
||||||
|
return (int)($summary['lane']['department'] ?? $summary['session']['department_id'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function userHasSummaryDepartmentAccess(object $user, array $summary): bool
|
||||||
|
{
|
||||||
|
$lane_department = $this->summaryDepartmentId($summary);
|
||||||
|
if ($lane_department <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authorized_department_ids = array_values(array_filter(
|
||||||
|
array_map('intval', (array)$user->getGroup()->getDepartments()),
|
||||||
|
static fn(int $department_id): bool => $department_id > 0
|
||||||
|
));
|
||||||
|
|
||||||
|
return in_array($lane_department, $authorized_department_ids, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function requireAuthenticatedCustomerNumber(object $user, string $elevatedPermission): int
|
private function requireAuthenticatedCustomerNumber(object $user, string $elevatedPermission): int
|
||||||
@@ -561,28 +628,26 @@ class departmentSelfserveVehicleConditionsRoute
|
|||||||
return $customer_number;
|
return $customer_number;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertSummaryAccess(object $user, array $summary, bool $hasGlobalPermission, string $elevatedPermission): void
|
private function assertSummaryAccess(
|
||||||
|
object $user,
|
||||||
|
array $summary,
|
||||||
|
bool $hasGlobalPermission,
|
||||||
|
bool $hasOwnPermission,
|
||||||
|
string $elevatedPermission
|
||||||
|
): void
|
||||||
{
|
{
|
||||||
global $response;
|
global $response;
|
||||||
|
|
||||||
|
if ($hasGlobalPermission && $this->userHasSummaryDepartmentAccess($user, $summary)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hasOwnPermission && $this->summaryBelongsToCustomer($user, $summary)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($hasGlobalPermission) {
|
if ($hasGlobalPermission) {
|
||||||
$authorized_department_ids = $user->getGroup()->getDepartments();
|
$this->forbidDepartmentAccess($this->summaryDepartmentId($summary));
|
||||||
$lane_department = (int)($summary['lane']['department'] ?? 0);
|
|
||||||
if (!in_array($lane_department, $authorized_department_ids, true)) {
|
|
||||||
$this->forbidDepartmentAccess($lane_department);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$session_customer_number = $summary['session']['customer_number'] ?? null;
|
|
||||||
if ($session_customer_number !== null && (int)$session_customer_number === (int)$user->customer_number->value()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$reg = (string)($summary['session']['reg'] ?? '');
|
|
||||||
$vehicle_o = (new customer_vehicles_o())->selectByPlate($reg);
|
|
||||||
if ($vehicle_o->exists() && (int)$vehicle_o->customer_id->value() === (int)$user->customer_number->value()) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->forbidden([$elevatedPermission]);
|
$response->forbidden([$elevatedPermission]);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use objects\logs_o;
|
|||||||
use objects\orders_o;
|
use objects\orders_o;
|
||||||
use objects\customer_vehicles_o;
|
use objects\customer_vehicles_o;
|
||||||
use objects\selfserve_wash_sessions_o;
|
use objects\selfserve_wash_sessions_o;
|
||||||
|
use objects\selfserve_wash_session_tasks_o;
|
||||||
use objects\stripe_module_customers_o;
|
use objects\stripe_module_customers_o;
|
||||||
use objects\users_o;
|
use objects\users_o;
|
||||||
use traits\route_t;
|
use traits\route_t;
|
||||||
@@ -334,6 +335,23 @@ class moduleSelfServeRoute
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Modules > Self Serve > Lane > Wash > My active wash */
|
||||||
|
$this->get('/modules/self-serve/lane/wash/my-active-wash', function () {
|
||||||
|
global $response;
|
||||||
|
|
||||||
|
$customer_number = $this->requireMyActiveWashCustomerNumber();
|
||||||
|
$session = $this->findLatestActiveSelfServeSessionForCustomer($customer_number);
|
||||||
|
if (!$session->exists()) {
|
||||||
|
$response->error('No active self-serve wash found.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->success($this->buildActiveSelfServeSessionResponse($session));
|
||||||
|
},
|
||||||
|
[
|
||||||
|
'list_own_department_selfserve_vehicle_conditions' => 'View the authenticated customer\'s active self-serve wash',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
/** Modules > Self Serve > Sessions */
|
/** Modules > Self Serve > Sessions */
|
||||||
$this->get('/modules/self-serve/sessions', function () {
|
$this->get('/modules/self-serve/sessions', function () {
|
||||||
global $response;
|
global $response;
|
||||||
@@ -613,23 +631,59 @@ class moduleSelfServeRoute
|
|||||||
// Build allowed services from provided tasks
|
// Build allowed services from provided tasks
|
||||||
$lane = $selfserve->lane($lane_id);
|
$lane = $selfserve->lane($lane_id);
|
||||||
$customer_number = $this->resolveEffectiveCustomerNumber();
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||||
self::requirePermission('modules_selfserve_lane_services_set_allowed');
|
$this->requireSelfServeLaneAccess(
|
||||||
|
$lane,
|
||||||
|
$customer_number === null ? 0 : (int)$customer_number,
|
||||||
|
['modules_selfserve_lane_services_set_allowed'],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
$session_task_services = [];
|
||||||
|
$latest_session = (new selfserve_wash_sessions_o())->selectLatestOpenByLane(
|
||||||
|
$lane_id,
|
||||||
|
$customer_number === null || (int)$customer_number <= 0 ? null : (int)$customer_number
|
||||||
|
);
|
||||||
|
if (!$latest_session->exists() && $customer_number !== null && (int)$customer_number > 0) {
|
||||||
|
$latest_session = (new selfserve_wash_sessions_o())->selectLatestOpenByLane($lane_id);
|
||||||
|
}
|
||||||
|
if ($latest_session->exists()) {
|
||||||
|
foreach ((new selfserve_wash_session_tasks_o())->listBySession((int)$latest_session->id) as $session_task) {
|
||||||
|
$task_id = (int)($session_task['task_id'] ?? 0);
|
||||||
|
if ($task_id > 0) {
|
||||||
|
$services = $session_task['services'] ?? [];
|
||||||
|
if (is_string($services)) {
|
||||||
|
$decoded_services = json_decode($services, true);
|
||||||
|
$services = json_last_error() === JSON_ERROR_NONE && is_array($decoded_services)
|
||||||
|
? $decoded_services
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
$session_task_services[$task_id] = is_array($services) ? $services : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$allowed_services = [];
|
$allowed_services = [];
|
||||||
foreach ($task_ids as $tid) {
|
$merge_services = static function (array $services) use (&$allowed_services): void {
|
||||||
if ($tid <= 0) continue;
|
|
||||||
$t = new \objects\department_selfserve_tasks_o();
|
|
||||||
$t->select($tid);
|
|
||||||
if (!$t->exists()) continue; // ignore unknown ids
|
|
||||||
// Validate the task belongs to the same lane
|
|
||||||
if ((int)$t->lane->value() !== $lane_id) continue;
|
|
||||||
// Merge services (if any)
|
|
||||||
$services = (array)$t->services->value();
|
|
||||||
foreach ($services as $srv) {
|
foreach ($services as $srv) {
|
||||||
$name = strtoupper((string)$srv);
|
$name = strtoupper((string)$srv);
|
||||||
if (!in_array($name, $allowed_services, true)) {
|
if (!in_array($name, $allowed_services, true)) {
|
||||||
$allowed_services[] = $name;
|
$allowed_services[] = $name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
foreach ($task_ids as $tid) {
|
||||||
|
if ($tid <= 0) continue;
|
||||||
|
if (array_key_exists($tid, $session_task_services)) {
|
||||||
|
$merge_services($session_task_services[$tid]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$t = new \objects\department_selfserve_tasks_o();
|
||||||
|
$t->select($tid);
|
||||||
|
if (!$t->exists()) continue; // ignore unknown ids
|
||||||
|
// Validate the task belongs to the same lane
|
||||||
|
if ((int)$t->lane->value() !== $lane_id) continue;
|
||||||
|
// Merge services (if any)
|
||||||
|
$merge_services((array)$t->services->value());
|
||||||
}
|
}
|
||||||
// Persist on lane cache (overwrites previous allowed services)
|
// Persist on lane cache (overwrites previous allowed services)
|
||||||
try {
|
try {
|
||||||
@@ -1324,6 +1378,188 @@ class moduleSelfServeRoute
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function requireMyActiveWashCustomerNumber(): int
|
||||||
|
{
|
||||||
|
global $response;
|
||||||
|
|
||||||
|
$user = (new authentication())->get_user();
|
||||||
|
if (!$user) {
|
||||||
|
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)) {
|
||||||
|
$this->emitForbidden([self::CUSTOMER_SELFSERVE_PERMISSION]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||||
|
if ($customer_number === null || $customer_number <= 0) {
|
||||||
|
$response->error('No customer number found for authenticated user.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)$customer_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findLatestActiveSelfServeSessionForCustomer(int $customer_number): selfserve_wash_sessions_o
|
||||||
|
{
|
||||||
|
if ($customer_number <= 0) {
|
||||||
|
return new selfserve_wash_sessions_o();
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
||||||
|
'customer_number' => $customer_number,
|
||||||
|
'completed_at' => null,
|
||||||
|
'deleted_at' => null,
|
||||||
|
], ['id', 'status']);
|
||||||
|
|
||||||
|
$active_statuses = $this->activeSelfServeWashSessionStatusValues();
|
||||||
|
$rows = array_values(array_filter(
|
||||||
|
$rows,
|
||||||
|
static fn(array $row): bool => in_array((string)($row['status'] ?? ''), $active_statuses, true)
|
||||||
|
));
|
||||||
|
if ($rows === []) {
|
||||||
|
return new selfserve_wash_sessions_o();
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
|
||||||
|
return (new selfserve_wash_sessions_o())->select((int)$rows[0]['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
private function activeSelfServeWashSessionStatusValues(): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
static fn(selfserve_wash_session_status $status): string => $status->value,
|
||||||
|
[
|
||||||
|
selfserve_wash_session_status::MACHINE_RELAY_ENABLED,
|
||||||
|
selfserve_wash_session_status::READY_FOR_MACHINE_START,
|
||||||
|
selfserve_wash_session_status::MACHINE_STARTED,
|
||||||
|
selfserve_wash_session_status::PENDING_QUESTIONS,
|
||||||
|
selfserve_wash_session_status::MACHINE_NOT_ALLOWED,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function buildActiveSelfServeSessionResponse(selfserve_wash_sessions_o $session): array
|
||||||
|
{
|
||||||
|
$lane_id = (int)$session->lane_id->value();
|
||||||
|
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
|
||||||
|
$vehicle_id = $session->vehicle_id->value() === null ? null : (int)$session->vehicle_id->value();
|
||||||
|
$session_reg = trim((string)$session->reg->value());
|
||||||
|
if ($session_reg === '') {
|
||||||
|
$session_reg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machine_start_triggered_at = $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value();
|
||||||
|
$wash_started_at = $session->wash_started_at->value() === null ? null : (string)$session->wash_started_at->value();
|
||||||
|
if ($wash_started_at === null) {
|
||||||
|
$wash_started_at = $machine_start_triggered_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machine_relay_enabled = (bool)$session->machine_relay_enabled->value();
|
||||||
|
$included_minutes = null;
|
||||||
|
if ($machine_relay_enabled) {
|
||||||
|
$included_minutes = (int)(new selfserve())->config->machine_wash_minutes_included->getVariableValue();
|
||||||
|
if ($included_minutes < 0) {
|
||||||
|
$included_minutes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'lane_id' => $lane_id,
|
||||||
|
'status' => (string)$session->status->value(),
|
||||||
|
'in_progress' => true,
|
||||||
|
'elapsed_minutes' => $session->getElapsedMinutes(),
|
||||||
|
'session' => [
|
||||||
|
'id' => (int)$session->id,
|
||||||
|
'status' => (string)$session->status->value(),
|
||||||
|
'department_id' => $session->department_id->value() === null ? null : (int)$session->department_id->value(),
|
||||||
|
'lane_id' => $lane_id,
|
||||||
|
'reg' => $session_reg,
|
||||||
|
'customer_number' => $customer_number,
|
||||||
|
'vehicle_id' => $vehicle_id,
|
||||||
|
'vehicle_type_id' => $session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
|
||||||
|
'included_minutes' => $included_minutes ?? 0,
|
||||||
|
'machine_type_id' => $session->machine_type_id->value() === null ? null : (int)$session->machine_type_id->value(),
|
||||||
|
'machine_relay_enabled' => $machine_relay_enabled,
|
||||||
|
'machine_relay_enabled_at' => $session->machine_relay_enabled_at->value() === null ? null : (string)$session->machine_relay_enabled_at->value(),
|
||||||
|
'machine_start_triggered' => (bool)$session->machine_start_triggered->value(),
|
||||||
|
'machine_start_triggered_at' => $machine_start_triggered_at,
|
||||||
|
'wash_started_at' => $wash_started_at,
|
||||||
|
'created_at' => (string)$session->created_at->value(),
|
||||||
|
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
||||||
|
],
|
||||||
|
'customer' => $this->buildInProgressWashCustomerPayload($customer_number),
|
||||||
|
'vehicle' => $this->buildInProgressWashVehiclePayload($vehicle_id, $session_reg),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
private function buildInProgressWashCustomerPayload(?int $customer_number): ?array
|
||||||
|
{
|
||||||
|
if ($customer_number === null || $customer_number <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer_obj = (new users_o())->getUserByCustomerNumber($customer_number);
|
||||||
|
if ($customer_obj->exists()) {
|
||||||
|
return [
|
||||||
|
'id' => (int)$customer_obj->id,
|
||||||
|
'customer_number' => $customer_number,
|
||||||
|
'display_name' => $customer_obj->display_name->value() === null ? null : (string)$customer_obj->display_name->value(),
|
||||||
|
'email' => $customer_obj->email->value() === null ? null : (string)$customer_obj->email->value(),
|
||||||
|
'phone_country_code' => $customer_obj->phone_country_code->value() === null ? null : (int)$customer_obj->phone_country_code->value(),
|
||||||
|
'phone' => $customer_obj->phone->value() === null ? null : (string)$customer_obj->phone->value(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => null,
|
||||||
|
'customer_number' => $customer_number,
|
||||||
|
'display_name' => null,
|
||||||
|
'email' => null,
|
||||||
|
'phone_country_code' => null,
|
||||||
|
'phone' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
private function buildInProgressWashVehiclePayload(?int $vehicle_id, ?string $reg): ?array
|
||||||
|
{
|
||||||
|
$vehicle_obj = null;
|
||||||
|
if ($vehicle_id !== null && $vehicle_id > 0) {
|
||||||
|
$tmp_vehicle = (new customer_vehicles_o())->select($vehicle_id);
|
||||||
|
if ($tmp_vehicle->exists()) {
|
||||||
|
$vehicle_obj = $tmp_vehicle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($vehicle_obj === null && $reg !== null && trim($reg) !== '') {
|
||||||
|
$tmp_vehicle = (new customer_vehicles_o())->selectByPlate(trim($reg));
|
||||||
|
if ($tmp_vehicle->exists()) {
|
||||||
|
$vehicle_obj = $tmp_vehicle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($vehicle_obj === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int)$vehicle_obj->id,
|
||||||
|
'customer_id' => (int)$vehicle_obj->customer_id->value(),
|
||||||
|
'type' => (int)$vehicle_obj->type->value(),
|
||||||
|
'reg' => (string)$vehicle_obj->reg->value(),
|
||||||
|
'reference' => $vehicle_obj->reference->value() === null ? null : (string)$vehicle_obj->reference->value(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $status
|
* @param array<string,mixed> $status
|
||||||
* @param array<string,mixed> $extra
|
* @param array<string,mixed> $extra
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
usesApiSuite();
|
||||||
|
|
||||||
|
function selfserve_customer_start_ensure_legacy_redis_constant(): void
|
||||||
|
{
|
||||||
|
if (defined('redis')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $REDIS_CONFIG;
|
||||||
|
|
||||||
|
$REDIS_CONFIG = [
|
||||||
|
'host' => getenv('REDIS_CONFIG_HOST') ?: getenv('REDIS_CONFIG_DEBUG_HOST') ?: 'redis',
|
||||||
|
'user' => getenv('REDIS_CONFIG_USER') ?: getenv('REDIS_CONFIG_DEBUG_USER') ?: 'default',
|
||||||
|
'database' => getenv('REDIS_CONFIG_DATABASE') ?: getenv('REDIS_CONFIG_DEBUG_DATABASE') ?: '0',
|
||||||
|
'password' => getenv('REDIS_CONFIG_PASSWORD') ?: getenv('REDIS_CONFIG_DEBUG_PASSWORD') ?: '',
|
||||||
|
'port' => getenv('REDIS_CONFIG_PORT') ?: getenv('REDIS_CONFIG_DEBUG_PORT') ?: '6379',
|
||||||
|
];
|
||||||
|
|
||||||
|
define('redis', (new \classes\redis())->connect());
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfserve_customer_start_make_available(int $laneId): void
|
||||||
|
{
|
||||||
|
selfserve_customer_start_ensure_legacy_redis_constant();
|
||||||
|
|
||||||
|
$lane = (new \classes\selfserve())->lane($laneId);
|
||||||
|
$lane->setLaneStatus(\modules\selfserve\helpers\selfserve_lane_status::AVAILABLE);
|
||||||
|
$lane->setLaneState(\modules\selfserve\helpers\selfserve_lane_state::IDLE);
|
||||||
|
$lane->setCustomerNumber(0);
|
||||||
|
$lane->setLicensePlate('');
|
||||||
|
$lane->setWashStartTime(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('allows the customer self-serve start sequence without department access', function (): void {
|
||||||
|
$group = api_fixtures()->createGroup([], [
|
||||||
|
'list_own_department_selfserve_vehicle_conditions',
|
||||||
|
]);
|
||||||
|
$scenario = api_fixtures()->createSelfServeScenario([
|
||||||
|
'customer' => ['group_id' => $group['id']],
|
||||||
|
'department_selfserve_enabled' => true,
|
||||||
|
'lane_selfserve_enabled' => true,
|
||||||
|
]);
|
||||||
|
$headers = api_fixtures()->bearerHeaders(
|
||||||
|
api_fixtures()->createAuthToken((int)$scenario['customer']['id'])
|
||||||
|
);
|
||||||
|
$laneId = (int)$scenario['lane']['id'];
|
||||||
|
$reg = (string)$scenario['vehicle']['reg'];
|
||||||
|
|
||||||
|
selfserve_customer_start_make_available($laneId);
|
||||||
|
|
||||||
|
api_client()
|
||||||
|
->get('/department/selfserve/vehicle/allowed?lane_id=' . $laneId . '®=' . urlencode($reg), $headers)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSuccess(true);
|
||||||
|
|
||||||
|
$summaryResponse = api_client()
|
||||||
|
->get('/department/selfserve/washes/summary?lane_id=' . $laneId . '®=' . urlencode($reg), $headers)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSuccess(true);
|
||||||
|
|
||||||
|
expect($summaryResponse->data()['session']['customer_number'] ?? null)
|
||||||
|
->toBe((int)$scenario['customer']['customer_number']);
|
||||||
|
|
||||||
|
api_client()
|
||||||
|
->post('/modules/self-serve/lane/services/allowed', [
|
||||||
|
'lane_id' => $laneId,
|
||||||
|
'task_ids' => [(int)$scenario['tasks'][1]['id']],
|
||||||
|
], $headers)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSuccess(true);
|
||||||
|
|
||||||
|
$startResponse = api_client()
|
||||||
|
->post('/modules/self-serve/lane/command', [
|
||||||
|
'lane_id' => $laneId,
|
||||||
|
'command' => 'START',
|
||||||
|
'license_plate' => $reg,
|
||||||
|
'defer_relay_side_effects' => true,
|
||||||
|
], $headers)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSuccess(true);
|
||||||
|
|
||||||
|
expect($startResponse->data()['status'] ?? null)->toBe('OCCUPIED')
|
||||||
|
->and($startResponse->data()['customer_number'] ?? null)
|
||||||
|
->toBe((int)$scenario['customer']['customer_number']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives allowed services from v2 session task snapshots when task rows are not legacy records', function (): void {
|
||||||
|
$group = api_fixtures()->createGroup([], [
|
||||||
|
'list_own_department_selfserve_vehicle_conditions',
|
||||||
|
]);
|
||||||
|
$scenario = api_fixtures()->createSelfServeScenario([
|
||||||
|
'customer' => ['group_id' => $group['id']],
|
||||||
|
'department_selfserve_enabled' => true,
|
||||||
|
'lane_selfserve_enabled' => true,
|
||||||
|
]);
|
||||||
|
$headers = api_fixtures()->bearerHeaders(
|
||||||
|
api_fixtures()->createAuthToken((int)$scenario['customer']['id'])
|
||||||
|
);
|
||||||
|
$laneId = (int)$scenario['lane']['id'];
|
||||||
|
$v2TaskId = 900000 + $laneId;
|
||||||
|
|
||||||
|
$sessionTask = (new \objects\selfserve_wash_session_tasks_o())->addSnapshot(
|
||||||
|
(int)$scenario['session']['id'],
|
||||||
|
$v2TaskId,
|
||||||
|
'V2 path editor machine task',
|
||||||
|
'Task exists only in the session snapshot.',
|
||||||
|
['MACHINE', 'PROGRAM_PICKER'],
|
||||||
|
['program_picker', 6, 'start'],
|
||||||
|
(int)$scenario['product']['id'],
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = api_client()
|
||||||
|
->post('/modules/self-serve/lane/services/allowed', [
|
||||||
|
'lane_id' => $laneId,
|
||||||
|
'task_ids' => [$v2TaskId],
|
||||||
|
], $headers)
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSuccess(true);
|
||||||
|
|
||||||
|
expect($response->data()['allowed_services'] ?? [])->toBe([
|
||||||
|
'MACHINE',
|
||||||
|
'PROGRAM_PICKER',
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
$sessionTask->delete();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -10,6 +10,14 @@ it('requires authentication before checking in-progress wash permissions', funct
|
|||||||
->assertMessage('Authentication failed. Invalid or missing token.');
|
->assertMessage('Authentication failed. Invalid or missing token.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires authentication before checking the current customers active self-serve wash', function (): void {
|
||||||
|
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash');
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(401)
|
||||||
|
->assertMessage('Authentication failed. Invalid or missing token.');
|
||||||
|
});
|
||||||
|
|
||||||
it('reports both elevated and customer self-serve permissions when lane polling is not allowed', function (): void {
|
it('reports both elevated and customer self-serve permissions when lane polling is not allowed', function (): void {
|
||||||
$session = api_fixtures()->createUserSession([]);
|
$session = api_fixtures()->createUserSession([]);
|
||||||
|
|
||||||
@@ -26,6 +34,18 @@ it('reports both elevated and customer self-serve permissions when lane polling
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('requires customer self-serve permission before checking my active wash', function (): void {
|
||||||
|
$session = api_fixtures()->createUserSession([]);
|
||||||
|
|
||||||
|
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $session['headers']);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(403)
|
||||||
|
->assertMissingPermissions([
|
||||||
|
'list_own_department_selfserve_vehicle_conditions',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('allows customer self-serve permission to view their own in-progress wash details', function (): void {
|
it('allows customer self-serve permission to view their own in-progress wash details', function (): void {
|
||||||
$group = api_fixtures()->createGroup([], [
|
$group = api_fixtures()->createGroup([], [
|
||||||
'list_own_department_selfserve_vehicle_conditions',
|
'list_own_department_selfserve_vehicle_conditions',
|
||||||
@@ -51,6 +71,46 @@ it('allows customer self-serve permission to view their own in-progress wash det
|
|||||||
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns the authenticated customers active self-serve wash without requiring a lane id', function (): void {
|
||||||
|
$group = api_fixtures()->createGroup([], [
|
||||||
|
'list_own_department_selfserve_vehicle_conditions',
|
||||||
|
]);
|
||||||
|
$scenario = api_fixtures()->createSelfServeScenario([
|
||||||
|
'customer' => [
|
||||||
|
'group_id' => $group['id'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$token = api_fixtures()->createAuthToken((int)$scenario['customer']['id']);
|
||||||
|
|
||||||
|
$response = api_client()->get(
|
||||||
|
'/modules/self-serve/lane/wash/my-active-wash',
|
||||||
|
api_fixtures()->bearerHeaders($token)
|
||||||
|
);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(200)
|
||||||
|
->assertSuccess(true);
|
||||||
|
|
||||||
|
expect($response->data()['in_progress'] ?? null)->toBeTrue()
|
||||||
|
->and($response->data()['lane_id'] ?? null)->toBe((int)$scenario['lane']['id'])
|
||||||
|
->and($response->data()['session']['id'] ?? null)->toBe((int)$scenario['session']['id'])
|
||||||
|
->and($response->data()['session']['lane_id'] ?? null)->toBe((int)$scenario['lane']['id'])
|
||||||
|
->and($response->data()['session']['customer_number'] ?? null)->toBe((int)$scenario['customer']['customer_number'])
|
||||||
|
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when the authenticated customer has no active self-serve wash', function (): void {
|
||||||
|
$session = api_fixtures()->createUserSession([
|
||||||
|
'list_own_department_selfserve_vehicle_conditions',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $session['headers']);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertStatus(404)
|
||||||
|
->assertMessage('No active self-serve wash found.');
|
||||||
|
});
|
||||||
|
|
||||||
it('redacts another customers in-progress wash from customer self-serve lane polling', function (): void {
|
it('redacts another customers in-progress wash from customer self-serve lane polling', function (): void {
|
||||||
$scenario = api_fixtures()->createSelfServeScenario();
|
$scenario = api_fixtures()->createSelfServeScenario();
|
||||||
$otherSession = api_fixtures()->createUserSession([
|
$otherSession = api_fixtures()->createUserSession([
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
app_require('traits/module_config_variable_t.php');
|
||||||
|
|
||||||
|
use traits\module_config_variable;
|
||||||
|
|
||||||
|
function selfserve_module_config_database_config(): array
|
||||||
|
{
|
||||||
|
$subject = new class {
|
||||||
|
use module_config_variable;
|
||||||
|
};
|
||||||
|
|
||||||
|
$method = new ReflectionMethod($subject::class, 'readDatabaseConfigFromEnvironment');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
return $method->invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfserve_module_config_with_env(array $values, callable $callback): mixed
|
||||||
|
{
|
||||||
|
$keys = [
|
||||||
|
'CONFIG_DB_TARGET',
|
||||||
|
'CONFIG_DB_HOST',
|
||||||
|
'CONFIG_DB_USER',
|
||||||
|
'CONFIG_DB_PASSWORD',
|
||||||
|
'CONFIG_DB_DATABASE',
|
||||||
|
'CONFIG_DB_PORT',
|
||||||
|
'CONFIG_DB_SSL_MODE',
|
||||||
|
'CONFIG_DB_DEBUG_HOST',
|
||||||
|
'CONFIG_DB_DEBUG_USER',
|
||||||
|
'CONFIG_DB_DEBUG_PASSWORD',
|
||||||
|
'CONFIG_DB_DEBUG_DATABASE',
|
||||||
|
'CONFIG_DB_DEBUG_PORT',
|
||||||
|
'CONFIG_DB_DEBUG_SSL_MODE',
|
||||||
|
];
|
||||||
|
$previous = [];
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$previous[$key] = [
|
||||||
|
'env' => getenv($key),
|
||||||
|
'has_env' => array_key_exists($key, $_ENV),
|
||||||
|
'super_env' => $_ENV[$key] ?? null,
|
||||||
|
'has_server' => array_key_exists($key, $_SERVER),
|
||||||
|
'server' => $_SERVER[$key] ?? null,
|
||||||
|
];
|
||||||
|
putenv($key);
|
||||||
|
unset($_ENV[$key], $_SERVER[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
putenv($key . '=' . (string)$value);
|
||||||
|
$_ENV[$key] = (string)$value;
|
||||||
|
$_SERVER[$key] = (string)$value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $callback();
|
||||||
|
} finally {
|
||||||
|
foreach ($previous as $key => $state) {
|
||||||
|
$state['env'] === false
|
||||||
|
? putenv($key)
|
||||||
|
: putenv($key . '=' . $state['env']);
|
||||||
|
|
||||||
|
if ($state['has_env']) {
|
||||||
|
$_ENV[$key] = $state['super_env'];
|
||||||
|
} else {
|
||||||
|
unset($_ENV[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($state['has_server']) {
|
||||||
|
$_SERVER[$key] = $state['server'];
|
||||||
|
} else {
|
||||||
|
unset($_SERVER[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('reads debug database configuration when the debug target is selected', function (): void {
|
||||||
|
$config = selfserve_module_config_with_env([
|
||||||
|
'CONFIG_DB_TARGET' => 'debug',
|
||||||
|
'CONFIG_DB_HOST' => '',
|
||||||
|
'CONFIG_DB_USER' => '',
|
||||||
|
'CONFIG_DB_PASSWORD' => '',
|
||||||
|
'CONFIG_DB_DATABASE' => '',
|
||||||
|
'CONFIG_DB_PORT' => '',
|
||||||
|
'CONFIG_DB_SSL_MODE' => '',
|
||||||
|
'CONFIG_DB_DEBUG_HOST' => 'mysql-debug',
|
||||||
|
'CONFIG_DB_DEBUG_USER' => 'root',
|
||||||
|
'CONFIG_DB_DEBUG_PASSWORD' => 'debug_root_password',
|
||||||
|
'CONFIG_DB_DEBUG_DATABASE' => 'nnks_db_debug',
|
||||||
|
'CONFIG_DB_DEBUG_PORT' => '3306',
|
||||||
|
'CONFIG_DB_DEBUG_SSL_MODE' => 'DISABLED',
|
||||||
|
], static fn(): array => selfserve_module_config_database_config());
|
||||||
|
|
||||||
|
expect($config)->toMatchArray([
|
||||||
|
'host' => 'mysql-debug',
|
||||||
|
'user' => 'root',
|
||||||
|
'password' => 'debug_root_password',
|
||||||
|
'database' => 'nnks_db_debug',
|
||||||
|
'port' => 3306,
|
||||||
|
'ssl_mode' => 'DISABLED',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps live database configuration as the default target', function (): void {
|
||||||
|
$config = selfserve_module_config_with_env([
|
||||||
|
'CONFIG_DB_HOST' => 'live-db',
|
||||||
|
'CONFIG_DB_USER' => 'live-user',
|
||||||
|
'CONFIG_DB_PASSWORD' => 'live-password',
|
||||||
|
'CONFIG_DB_DATABASE' => 'live-database',
|
||||||
|
'CONFIG_DB_PORT' => '3307',
|
||||||
|
'CONFIG_DB_SSL_MODE' => 'REQUIRED',
|
||||||
|
'CONFIG_DB_DEBUG_HOST' => 'mysql-debug',
|
||||||
|
'CONFIG_DB_DEBUG_USER' => 'root',
|
||||||
|
'CONFIG_DB_DEBUG_PASSWORD' => 'debug_root_password',
|
||||||
|
'CONFIG_DB_DEBUG_DATABASE' => 'nnks_db_debug',
|
||||||
|
'CONFIG_DB_DEBUG_PORT' => '3306',
|
||||||
|
'CONFIG_DB_DEBUG_SSL_MODE' => 'DISABLED',
|
||||||
|
], static fn(): array => selfserve_module_config_database_config());
|
||||||
|
|
||||||
|
expect($config)->toMatchArray([
|
||||||
|
'host' => 'live-db',
|
||||||
|
'user' => 'live-user',
|
||||||
|
'password' => 'live-password',
|
||||||
|
'database' => 'live-database',
|
||||||
|
'port' => 3307,
|
||||||
|
'ssl_mode' => 'REQUIRED',
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -43,16 +43,17 @@ function selfserve_eligibility_method_block(string $route, string $signature): s
|
|||||||
it('authorizes customer eligibility preview lanes before returning task attachments', function (): void {
|
it('authorizes customer eligibility preview lanes before returning task attachments', function (): void {
|
||||||
$route = selfserve_eligibility_route_source();
|
$route = selfserve_eligibility_route_source();
|
||||||
$allowedBlock = selfserve_eligibility_route_block($route, 'get', '/department/selfserve/vehicle/allowed');
|
$allowedBlock = selfserve_eligibility_route_block($route, 'get', '/department/selfserve/vehicle/allowed');
|
||||||
$assertBlock = selfserve_eligibility_method_block($route, 'private function assertLaneAccess(object $user, int $laneId): department_lanes_o');
|
$assertBlock = selfserve_eligibility_method_block($route, 'private function assertLaneAccess(');
|
||||||
|
|
||||||
expect($allowedBlock)->toContain('$lane = $this->assertLaneAccess($user, $lane_id);')
|
expect($allowedBlock)->toContain('$lane = $this->assertLaneAccess($user, $lane_id, $has_global, $has_own);')
|
||||||
->and($allowedBlock)->toContain('previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id)')
|
->and($allowedBlock)->toContain('previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id)')
|
||||||
->and(strpos($allowedBlock, '$lane = $this->assertLaneAccess($user, $lane_id);'))
|
->and(strpos($allowedBlock, '$lane = $this->assertLaneAccess($user, $lane_id, $has_global, $has_own);'))
|
||||||
->toBeLessThan(strpos($allowedBlock, 'previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id)'));
|
->toBeLessThan(strpos($allowedBlock, 'previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id)'));
|
||||||
|
|
||||||
expect($assertBlock)->toContain('$authorized_department_ids = array_values(array_filter(')
|
expect($assertBlock)->toContain('bool $hasGlobalPermission = true')
|
||||||
->and($assertBlock)->toContain('array_map(\'intval\', (array)$user->getGroup()->getDepartments())')
|
->and($assertBlock)->toContain('bool $hasOwnPermission = false')
|
||||||
->and($assertBlock)->toContain('if (!in_array($lane_department_id, $authorized_department_ids, true))')
|
->and($assertBlock)->toContain('if ($hasGlobalPermission && $this->userHasLaneDepartmentAccess($user, $lane))')
|
||||||
|
->and($assertBlock)->toContain('if ($hasOwnPermission && $this->isCustomerSelfServeLaneEnabled($lane))')
|
||||||
->and($assertBlock)->toContain('$this->forbidDepartmentAccess($lane_department_id);')
|
->and($assertBlock)->toContain('$this->forbidDepartmentAccess($lane_department_id);')
|
||||||
->and($assertBlock)->not->toContain('if ($hasGlobalPermission)');
|
->and($assertBlock)->toContain('$response->forbidden([$elevatedPermission]);');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ class SelfserveLaneStartEntranceTimeoutHarness
|
|||||||
public ?\Throwable $reportedTimeout = null;
|
public ?\Throwable $reportedTimeout = null;
|
||||||
public ?selfserve_lane_state $laneState = null;
|
public ?selfserve_lane_state $laneState = null;
|
||||||
public int $cleanerRelayCalls = 0;
|
public int $cleanerRelayCalls = 0;
|
||||||
|
public int $programPickerRelayCalls = 0;
|
||||||
public int $machineRelayCalls = 0;
|
public int $machineRelayCalls = 0;
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $relayEvents = [];
|
||||||
|
|
||||||
public function open(selfserve_lane_port $port, ?int $toggle_after_seconds = null): bool
|
public function open(selfserve_lane_port $port, ?int $toggle_after_seconds = null): bool
|
||||||
{
|
{
|
||||||
@@ -47,11 +50,19 @@ class SelfserveLaneStartEntranceTimeoutHarness
|
|||||||
protected function turnOnCleanerRelayForWashStart(): void
|
protected function turnOnCleanerRelayForWashStart(): void
|
||||||
{
|
{
|
||||||
$this->cleanerRelayCalls++;
|
$this->cleanerRelayCalls++;
|
||||||
|
$this->relayEvents[] = 'cleaner:on';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setProgramPickerRelayStatusForWashStart(): void
|
||||||
|
{
|
||||||
|
$this->programPickerRelayCalls++;
|
||||||
|
$this->relayEvents[] = 'program_picker:sync';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setMachineRelayStatusForWashStart(): void
|
protected function setMachineRelayStatusForWashStart(): void
|
||||||
{
|
{
|
||||||
$this->machineRelayCalls++;
|
$this->machineRelayCalls++;
|
||||||
|
$this->relayEvents[] = 'machine:sync';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function runEntranceOpenForStart(): void
|
public function runEntranceOpenForStart(): void
|
||||||
@@ -98,20 +109,28 @@ it('parses deferred relay side effects on start command arguments', function ():
|
|||||||
expect($arguments->defer_relay_side_effects)->toBeTrue();
|
expect($arguments->defer_relay_side_effects)->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips cleaner and machine relay side effects when start asks to defer them', function (): void {
|
it('skips cleaner, program picker and machine relay side effects when start asks to defer them', function (): void {
|
||||||
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
|
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
|
||||||
|
|
||||||
$lane->runStartRelaySideEffects(true);
|
$lane->runStartRelaySideEffects(true);
|
||||||
|
|
||||||
expect($lane->cleanerRelayCalls)->toBe(0);
|
expect($lane->cleanerRelayCalls)->toBe(0);
|
||||||
|
expect($lane->programPickerRelayCalls)->toBe(0);
|
||||||
expect($lane->machineRelayCalls)->toBe(0);
|
expect($lane->machineRelayCalls)->toBe(0);
|
||||||
|
expect($lane->relayEvents)->toBe([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps cleaner and machine relay side effects for normal start commands', function (): void {
|
it('keeps cleaner, program picker and machine relay side effects in order for normal start commands', function (): void {
|
||||||
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
|
$lane = new SelfserveLaneStartEntranceTimeoutHarness();
|
||||||
|
|
||||||
$lane->runStartRelaySideEffects(false);
|
$lane->runStartRelaySideEffects(false);
|
||||||
|
|
||||||
expect($lane->cleanerRelayCalls)->toBe(1);
|
expect($lane->cleanerRelayCalls)->toBe(1);
|
||||||
|
expect($lane->programPickerRelayCalls)->toBe(1);
|
||||||
expect($lane->machineRelayCalls)->toBe(1);
|
expect($lane->machineRelayCalls)->toBe(1);
|
||||||
|
expect($lane->relayEvents)->toBe([
|
||||||
|
'cleaner:on',
|
||||||
|
'program_picker:sync',
|
||||||
|
'machine:sync',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class SelfserveLaneStopFlowHarness
|
|||||||
public array $openedPorts = [];
|
public array $openedPorts = [];
|
||||||
/** @var selfserve_lane_relay[] */
|
/** @var selfserve_lane_relay[] */
|
||||||
public array $turnedOffRelays = [];
|
public array $turnedOffRelays = [];
|
||||||
|
/** @var array<int,string> */
|
||||||
|
public array $events = [];
|
||||||
|
|
||||||
private selfserve_lane_status $laneStatus;
|
private selfserve_lane_status $laneStatus;
|
||||||
private selfserve_lane_mode $laneMode;
|
private selfserve_lane_mode $laneMode;
|
||||||
@@ -182,6 +184,7 @@ class SelfserveLaneStopFlowHarness
|
|||||||
public function open(selfserve_lane_port $port): bool
|
public function open(selfserve_lane_port $port): bool
|
||||||
{
|
{
|
||||||
$this->openedPorts[] = $port;
|
$this->openedPorts[] = $port;
|
||||||
|
$this->events[] = 'open:' . $port->name;
|
||||||
if ($this->openThrowable !== null) {
|
if ($this->openThrowable !== null) {
|
||||||
throw $this->openThrowable;
|
throw $this->openThrowable;
|
||||||
}
|
}
|
||||||
@@ -192,6 +195,7 @@ class SelfserveLaneStopFlowHarness
|
|||||||
public function turnOffRelay(selfserve_lane_relay $relay): bool
|
public function turnOffRelay(selfserve_lane_relay $relay): bool
|
||||||
{
|
{
|
||||||
$this->turnedOffRelays[] = $relay;
|
$this->turnedOffRelays[] = $relay;
|
||||||
|
$this->events[] = 'relay:' . $relay->name . ':off';
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +203,7 @@ class SelfserveLaneStopFlowHarness
|
|||||||
{
|
{
|
||||||
if ($on === false) {
|
if ($on === false) {
|
||||||
$this->turnedOffRelays[] = $relay;
|
$this->turnedOffRelays[] = $relay;
|
||||||
|
$this->events[] = 'relay:' . $relay->name . ':off';
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -229,7 +234,7 @@ class SelfserveLaneStopFlowHarness
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
it('adds vehicle type product on STOP when the physical machine ON signal was recorded, then turns off cleaner and machine relays', function (): void {
|
it('adds vehicle type product on STOP when the physical machine ON signal was recorded, after turning relays off before exit', function (): void {
|
||||||
$lane = new SelfserveLaneStopFlowHarness(machineStartTriggered: true);
|
$lane = new SelfserveLaneStopFlowHarness(machineStartTriggered: true);
|
||||||
$args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);
|
$args = (new selfserve_lane_command_arguments())->setCustomerNumber(1234);
|
||||||
|
|
||||||
@@ -243,8 +248,15 @@ it('adds vehicle type product on STOP when the physical machine ON signal was re
|
|||||||
expect($lane->openedPorts)->toBe([selfserve_lane_port::EXIT]);
|
expect($lane->openedPorts)->toBe([selfserve_lane_port::EXIT]);
|
||||||
expect($lane->turnedOffRelays)->toBe([
|
expect($lane->turnedOffRelays)->toBe([
|
||||||
selfserve_lane_relay::MACHINE_CLEANER,
|
selfserve_lane_relay::MACHINE_CLEANER,
|
||||||
|
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||||
selfserve_lane_relay::MACHINE,
|
selfserve_lane_relay::MACHINE,
|
||||||
]);
|
]);
|
||||||
|
expect($lane->events)->toBe([
|
||||||
|
'relay:MACHINE_CLEANER:off',
|
||||||
|
'relay:MACHINE_PROGRAM_PICKER:off',
|
||||||
|
'relay:MACHINE:off',
|
||||||
|
'open:EXIT',
|
||||||
|
]);
|
||||||
expect($lane->getLaneStatus())->toBe(selfserve_lane_status::AVAILABLE);
|
expect($lane->getLaneStatus())->toBe(selfserve_lane_status::AVAILABLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,8 +278,14 @@ it('skips vehicle type product add when no physical machine ON signal was record
|
|||||||
expect($lane->lastVehicleTypeProductDecision)->toBeFalse();
|
expect($lane->lastVehicleTypeProductDecision)->toBeFalse();
|
||||||
expect($lane->programSelectorStatusReads)->toBe(0);
|
expect($lane->programSelectorStatusReads)->toBe(0);
|
||||||
expect($lane->turnedOffRelays)->toBe([
|
expect($lane->turnedOffRelays)->toBe([
|
||||||
|
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||||
selfserve_lane_relay::MACHINE,
|
selfserve_lane_relay::MACHINE,
|
||||||
]);
|
]);
|
||||||
|
expect($lane->events)->toBe([
|
||||||
|
'relay:MACHINE_PROGRAM_PICKER:off',
|
||||||
|
'relay:MACHINE:off',
|
||||||
|
'open:EXIT',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not use selector relay online status as machine-wash billing evidence', function (): void {
|
it('does not use selector relay online status as machine-wash billing evidence', function (): void {
|
||||||
@@ -301,8 +319,15 @@ it('continues STOP when exit relay dispatch times out ambiguously', function ():
|
|||||||
expect($lane->vehicleTypeProductAddCalls)->toBe(1);
|
expect($lane->vehicleTypeProductAddCalls)->toBe(1);
|
||||||
expect($lane->turnedOffRelays)->toBe([
|
expect($lane->turnedOffRelays)->toBe([
|
||||||
selfserve_lane_relay::MACHINE_CLEANER,
|
selfserve_lane_relay::MACHINE_CLEANER,
|
||||||
|
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
|
||||||
selfserve_lane_relay::MACHINE,
|
selfserve_lane_relay::MACHINE,
|
||||||
]);
|
]);
|
||||||
|
expect($lane->events)->toBe([
|
||||||
|
'relay:MACHINE_CLEANER:off',
|
||||||
|
'relay:MACHINE_PROGRAM_PICKER:off',
|
||||||
|
'relay:MACHINE:off',
|
||||||
|
'open:EXIT',
|
||||||
|
]);
|
||||||
expect($lane->getLaneStatus())->toBe(selfserve_lane_status::AVAILABLE);
|
expect($lane->getLaneStatus())->toBe(selfserve_lane_status::AVAILABLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ it('documents self-serve machine type, eligibility, summary, and webhook endpoin
|
|||||||
expect($content)->toContain('/relay/button/press/post:');
|
expect($content)->toContain('/relay/button/press/post:');
|
||||||
expect($content)->toContain('/relay/machine/on/post:');
|
expect($content)->toContain('/relay/machine/on/post:');
|
||||||
expect($content)->toContain('/modules/self-serve/lane/wash/in-progress:');
|
expect($content)->toContain('/modules/self-serve/lane/wash/in-progress:');
|
||||||
|
expect($content)->toContain('/modules/self-serve/lane/wash/my-active-wash:');
|
||||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine/status:');
|
expect($content)->toContain('/modules/self-serve/lane/relay/machine/status:');
|
||||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine/set:');
|
expect($content)->toContain('/modules/self-serve/lane/relay/machine/set:');
|
||||||
expect($content)->toContain('/modules/self-serve/lane/gate/open:');
|
expect($content)->toContain('/modules/self-serve/lane/gate/open:');
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
app_require('modules/selfserve/classes/selfserve_wash_flow.php');
|
||||||
|
|
||||||
|
use modules\selfserve\classes\selfserve_wash_flow;
|
||||||
|
|
||||||
|
class SelfservePublishedConfigLaneWildcardHarness extends selfserve_wash_flow
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function questionsFor(int $departmentId, int $laneId, int $vehicleTypeId, array $config): array
|
||||||
|
{
|
||||||
|
return $this->loadQuestions($departmentId, $laneId, $vehicleTypeId, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function conditionsFor(int $departmentId, int $laneId, int $vehicleTypeId, array $config): array
|
||||||
|
{
|
||||||
|
return $this->loadConditions($departmentId, $laneId, $vehicleTypeId, null, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tasksFor(int $departmentId, int $laneId, int $vehicleTypeId, array $config): array
|
||||||
|
{
|
||||||
|
return $this->loadTasks($departmentId, $laneId, $vehicleTypeId, null, $config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('applies department product rows with lane zero to all lanes in published v2 configs', function (): void {
|
||||||
|
$flow = new SelfservePublishedConfigLaneWildcardHarness();
|
||||||
|
$config = [
|
||||||
|
'schema_version' => 2,
|
||||||
|
'questions' => [
|
||||||
|
['id' => 1, 'department' => 6, 'lane' => 0, 'product' => 1, 'question' => 'Wildcard question'],
|
||||||
|
['id' => 2, 'department' => 6, 'lane' => 99, 'product' => 1, 'question' => 'Other lane question'],
|
||||||
|
],
|
||||||
|
'conditions' => [
|
||||||
|
['id' => 11, 'department' => 6, 'lane' => 0, 'product' => 1, 'machine_type_id' => null, 'expression' => ['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 1, 'operator' => 'IS_TRUE']],
|
||||||
|
['id' => 12, 'department' => 6, 'lane' => 99, 'product' => 1, 'machine_type_id' => null, 'expression' => ['type' => 'predicate', 'subject_type' => 'question', 'subject_id' => 2, 'operator' => 'IS_TRUE']],
|
||||||
|
],
|
||||||
|
'tasks' => [
|
||||||
|
['id' => 21, 'department' => 6, 'lane' => 0, 'product' => 1, 'machine_type_id' => null, 'task' => 'Wildcard task', 'services' => ['MACHINE'], 'buttons' => [], 'gate_type' => 'ALWAYS', 'gate_ref_id' => null],
|
||||||
|
['id' => 22, 'department' => 6, 'lane' => 99, 'product' => 1, 'machine_type_id' => null, 'task' => 'Other lane task', 'services' => ['MACHINE'], 'buttons' => [], 'gate_type' => 'ALWAYS', 'gate_ref_id' => null],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(array_column($flow->questionsFor(6, 12, 1, $config), 'id'))->toBe([1]);
|
||||||
|
expect(array_column($flow->conditionsFor(6, 12, 1, $config), 'id'))->toBe([11]);
|
||||||
|
expect(array_column($flow->tasksFor(6, 12, 1, $config), 'id'))->toBe([21]);
|
||||||
|
});
|
||||||
@@ -240,6 +240,9 @@ it('wires allowed services route through machine relay visibility sync', functio
|
|||||||
|
|
||||||
expect($moduleSelfServeRoute)->not->toBeFalse();
|
expect($moduleSelfServeRoute)->not->toBeFalse();
|
||||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/services/allowed');
|
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/services/allowed');
|
||||||
|
expect($moduleSelfServeRoute)->toContain('selfserve_wash_session_tasks_o');
|
||||||
|
expect($moduleSelfServeRoute)->toContain('selectLatestOpenByLane');
|
||||||
|
expect($moduleSelfServeRoute)->toContain('$session_task_services[$task_id]');
|
||||||
expect($moduleSelfServeRoute)->toContain('setAllowedServicesFromVisibleTasks($allowed_services)');
|
expect($moduleSelfServeRoute)->toContain('setAllowedServicesFromVisibleTasks($allowed_services)');
|
||||||
expect($moduleSelfServeRoute)->not->toContain('syncMachineRelayFromVisibleServices($allowed_services, true)');
|
expect($moduleSelfServeRoute)->not->toContain('syncMachineRelayFromVisibleServices($allowed_services, true)');
|
||||||
expect($moduleSelfServeRoute)->toContain("'relay_sync' => \$relay_sync");
|
expect($moduleSelfServeRoute)->toContain("'relay_sync' => \$relay_sync");
|
||||||
@@ -286,9 +289,12 @@ it('wires in-progress self-serve wash details endpoint', function (): void {
|
|||||||
|
|
||||||
expect($moduleSelfServeRoute)->not->toBeFalse();
|
expect($moduleSelfServeRoute)->not->toBeFalse();
|
||||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/in-progress');
|
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/in-progress');
|
||||||
|
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/my-active-wash');
|
||||||
expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_wash_in_progress_view');
|
expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_wash_in_progress_view');
|
||||||
expect($moduleSelfServeRoute)->toContain('list_own_department_selfserve_vehicle_conditions');
|
expect($moduleSelfServeRoute)->toContain('list_own_department_selfserve_vehicle_conditions');
|
||||||
expect($moduleSelfServeRoute)->toContain('requireInProgressWashDetailsAccess($lane_id)');
|
expect($moduleSelfServeRoute)->toContain('requireInProgressWashDetailsAccess($lane_id)');
|
||||||
|
expect($moduleSelfServeRoute)->toContain('findLatestActiveSelfServeSessionForCustomer($customer_number)');
|
||||||
|
expect($moduleSelfServeRoute)->toContain('buildActiveSelfServeSessionResponse($session)');
|
||||||
expect($moduleSelfServeRoute)->toContain('(new department_lanes_o())->select($lane_id)');
|
expect($moduleSelfServeRoute)->toContain('(new department_lanes_o())->select($lane_id)');
|
||||||
expect($moduleSelfServeRoute)->toContain('self::requireDepartmentAccess((string)$department_lane->department->value())');
|
expect($moduleSelfServeRoute)->toContain('self::requireDepartmentAccess((string)$department_lane->department->value())');
|
||||||
expect($moduleSelfServeRoute)->toContain('scopeInProgressWashResponseForCustomer');
|
expect($moduleSelfServeRoute)->toContain('scopeInProgressWashResponseForCustomer');
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ trait module_config_variable
|
|||||||
private static function readDatabaseConfigFromEnvironment(): array
|
private static function readDatabaseConfigFromEnvironment(): array
|
||||||
{
|
{
|
||||||
$readEnv = static function (string $key, string $default = ''): string {
|
$readEnv = static function (string $key, string $default = ''): string {
|
||||||
$value = $_ENV[$key] ?? getenv($key);
|
$value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
|
||||||
if ($value === false || $value === null) {
|
if ($value === false || $value === null) {
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
@@ -284,9 +284,25 @@ trait module_config_variable
|
|||||||
return trim((string)$value);
|
return trim((string)$value);
|
||||||
};
|
};
|
||||||
|
|
||||||
$host = $readEnv('CONFIG_DB_HOST');
|
$dbTarget = strtolower(trim($readEnv('CONFIG_DB_TARGET', 'live')));
|
||||||
$user = $readEnv('CONFIG_DB_USER');
|
if ($dbTarget !== 'live' && $dbTarget !== 'debug') {
|
||||||
$database = $readEnv('CONFIG_DB_DATABASE');
|
$dbTarget = 'live';
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolveDbValue = static function (string $key) use ($dbTarget, $readEnv): string {
|
||||||
|
$liveValue = $readEnv('CONFIG_DB_' . $key);
|
||||||
|
$debugValue = $readEnv('CONFIG_DB_DEBUG_' . $key);
|
||||||
|
|
||||||
|
if ($dbTarget === 'debug' && $debugValue !== '') {
|
||||||
|
return $debugValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $liveValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
$host = $resolveDbValue('HOST');
|
||||||
|
$user = $resolveDbValue('USER');
|
||||||
|
$database = $resolveDbValue('DATABASE');
|
||||||
|
|
||||||
if ($host === '' || $user === '' || $database === '') {
|
if ($host === '' || $user === '' || $database === '') {
|
||||||
return [];
|
return [];
|
||||||
@@ -295,10 +311,10 @@ trait module_config_variable
|
|||||||
return [
|
return [
|
||||||
'host' => $host,
|
'host' => $host,
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'password' => $readEnv('CONFIG_DB_PASSWORD'),
|
'password' => $resolveDbValue('PASSWORD'),
|
||||||
'database' => $database,
|
'database' => $database,
|
||||||
'port' => (int)($readEnv('CONFIG_DB_PORT', '3306') ?: '3306'),
|
'port' => (int)($resolveDbValue('PORT') ?: '3306'),
|
||||||
'ssl_mode' => $readEnv('CONFIG_DB_SSL_MODE', 'DISABLED') ?: 'DISABLED',
|
'ssl_mode' => $resolveDbValue('SSL_MODE') ?: 'DISABLED',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ http:
|
|||||||
certResolver: le
|
certResolver: le
|
||||||
|
|
||||||
# Dashboard routers are declared via labels; middlewares are defined below.
|
# Dashboard routers are declared via labels; middlewares are defined below.
|
||||||
|
local:
|
||||||
|
rule: Host(`localhost`)
|
||||||
|
entryPoints: [web]
|
||||||
|
middlewares: [secure-headers]
|
||||||
|
service: caddy-local
|
||||||
|
|
||||||
|
local-api:
|
||||||
|
rule: Host(`localhost`) && PathPrefix(`/api`)
|
||||||
|
entryPoints: [web]
|
||||||
|
middlewares: [strip-api-prefix, secure-headers]
|
||||||
|
service: caddy-local
|
||||||
|
priority: 100
|
||||||
|
|
||||||
middlewares:
|
middlewares:
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
@@ -125,6 +137,10 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://94.130.142.41:11000"
|
- url: "http://94.130.142.41:11000"
|
||||||
|
caddy-local:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://caddy"
|
||||||
|
|
||||||
tls:
|
tls:
|
||||||
options:
|
options:
|
||||||
|
|||||||
Reference in New Issue
Block a user