Enhance self-serve lane functionality with new relay management and configuration updates

This commit is contained in:
Jeppe Bundgaard
2026-06-02 17:27:22 +02:00
parent 6d739cfebc
commit 1b99523366
18 changed files with 133418 additions and 90 deletions
BIN
View File
Binary file not shown.
+3
View File
@@ -3,6 +3,8 @@ services:
traefik:
image: traefik:2.11
container_name: traefik
group_add:
- "${DOCKER_SOCKET_GID:-65534}"
ports:
- "${TRAEFIK_WEB_PORT:-80}:80"
- "${TRAEFIK_WEBSECURE_PORT:-443}:443"
@@ -102,6 +104,7 @@ services:
image: mysql:8.4
container_name: mysql-debug
profiles: [dev]
command: ["mysqld", "--innodb-use-native-aio=0"]
environment:
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}
+132545
View File
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:08 2026] 127.0.0.1:38290 Accepted
[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
@@ -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 (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;
}));
}
@@ -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 (int)($condition['machine_type_id'] ?? 0) === 0
&& (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;
}));
}
@@ -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 (int)($task['machine_type_id'] ?? 0) === 0
&& (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;
}));
}
@@ -176,8 +176,7 @@ trait selfserve_lane_command_t
if (!$this->isRelayConfigured(selfserve_lane_relay::MACHINE)) {
return;
}
$active_wash = new selfserve_wash_flow();
if ($active_wash->isMachineAllowedToStartWash($this->id)) {
if ($this->isMachineWashSelectedAndAvailableForStart()) {
try {
$this->setMachineRelayStatusHard(true);
} 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
{
try {
@@ -277,28 +327,16 @@ trait selfserve_lane_command_t
return;
}
// Ensure cleaner relay is enabled whenever wash starts.
$this->turnOnCleanerRelayForWashStart();
// Ensure the machine relay is ON when a wash starts, when it is allowed.
$this->setProgramPickerRelayStatusForWashStart();
$this->setMachineRelayStatusForWashStart();
}
protected function resolveSelfServeActionWashModeForStart(): string
{
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') {
if ($this->isMachineServiceSelectedForWashStart()) {
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;
}
@@ -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
* 2. Machine relay
* 2. Program picker relay
* 3. Machine relay
*/
protected function turnOffRelaysAfterStop(): void
{
$relays = [
selfserve_lane_relay::MACHINE_CLEANER,
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
selfserve_lane_relay::MACHINE,
];
@@ -563,8 +603,17 @@ trait selfserve_lane_command_t
// Set the customer number and license plate
$this->setCustomerNumber($customer_number);
$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->runRelaySideEffectsForWashStart($arguments);
$this->runPublishedStudioActions(
selfserve_studio_actions::EVENT_WASH_START_COMMAND,
$this->resolveSelfServeActionWashModeForStart(),
[
'customer_number' => (int)$customer_number,
'reg' => $license_plate,
]
);
try {
// Gateway timeouts are ambiguous because the relay may already have received
// the pulse, so openEntrancePortForWashStart() reports them and continues.
@@ -580,15 +629,6 @@ trait selfserve_lane_command_t
$this->setLaneState(selfserve_lane_state::IN_WASH);
// Start the wash timer
$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
$this->logLaneAction(selfserve_lane_log_action::START_WASH);
} finally {
@@ -604,6 +644,8 @@ trait selfserve_lane_command_t
}
// Snapshot the physical machine ON signal before session completion/reset.
$machine_start_triggered = $this->hasMachineStartSignalForStop();
// Turn off relays before any configured or default exit gate opens.
$this->turnOffRelaysAfterStop();
$this->runPublishedStudioActions(
selfserve_studio_actions::EVENT_WASH_STOP_COMMAND,
$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
// the relay may already have received the pulse.
$this->openExitPortForWashStop();
// Turn off relays in deterministic order after STOP
$this->turnOffRelaysAfterStop();
// Log the lane stop event
$this->logLaneAction(selfserve_lane_log_action::STOP_WASH);
// Invoice the customer
@@ -13,6 +13,7 @@ use objects\customer_vehicles_o;
use objects\department_lanes_o;
use objects\department_selfserve_tasks_o;
use objects\department_selfserve_vehicle_conditions_o;
use objects\departments_o;
use objects\logs_o;
use traits\route_t;
@@ -131,9 +132,9 @@ class departmentSelfserveVehicleConditionsRoute
$lane_id = (int)self::getParameter('lane_id');
$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;
if (!$has_global && $has_own) {
if ($has_own && (!$has_global || !$this->userHasLaneDepartmentAccess($user, $lane))) {
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
}
$vehicle_type_id = $this->resolveVehicleTypeIdFromQuery();
@@ -171,7 +172,7 @@ class departmentSelfserveVehicleConditionsRoute
try {
if (self::isParametersSet(['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)) {
$summary = $flow->synchronizeSession(
@@ -193,9 +194,9 @@ class departmentSelfserveVehicleConditionsRoute
$lane_id = (int)self::getParameter('lane_id');
$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;
if (!$has_global && $has_own) {
if ($has_own && (!$has_global || !$this->userHasLaneDepartmentAccess($user, $lane))) {
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
}
@@ -204,7 +205,7 @@ class departmentSelfserveVehicleConditionsRoute
} else {
$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);
} catch (\RuntimeException $e) {
$response->error($e->getMessage(), 404);
@@ -527,7 +528,13 @@ class departmentSelfserveVehicleConditionsRoute
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;
@@ -536,17 +543,77 @@ class departmentSelfserveVehicleConditionsRoute
$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();
$authorized_department_ids = array_values(array_filter(
array_map('intval', (array)$user->getGroup()->getDepartments()),
static fn(int $department_id): bool => $department_id > 0
));
if (!in_array($lane_department_id, $authorized_department_ids, true)) {
$this->forbidDepartmentAccess($lane_department_id);
return in_array($lane_department_id, $authorized_department_ids, true);
}
return $lane;
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;
}
$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
@@ -561,28 +628,26 @@ class departmentSelfserveVehicleConditionsRoute
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;
if ($hasGlobalPermission && $this->userHasSummaryDepartmentAccess($user, $summary)) {
return;
}
if ($hasOwnPermission && $this->summaryBelongsToCustomer($user, $summary)) {
return;
}
if ($hasGlobalPermission) {
$authorized_department_ids = $user->getGroup()->getDepartments();
$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;
$this->forbidDepartmentAccess($this->summaryDepartmentId($summary));
}
$response->forbidden([$elevatedPermission]);
@@ -22,6 +22,7 @@ use objects\logs_o;
use objects\orders_o;
use objects\customer_vehicles_o;
use objects\selfserve_wash_sessions_o;
use objects\selfserve_wash_session_tasks_o;
use objects\stripe_module_customers_o;
use objects\users_o;
use traits\route_t;
@@ -630,23 +631,59 @@ class moduleSelfServeRoute
// Build allowed services from provided tasks
$lane = $selfserve->lane($lane_id);
$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 = [];
foreach ($task_ids as $tid) {
if ($tid <= 0) continue;
$t = new \objects\department_selfserve_tasks_o();
$t->select($tid);
if (!$t->exists()) continue; // ignore unknown ids
// Validate the task belongs to the same lane
if ((int)$t->lane->value() !== $lane_id) continue;
// Merge services (if any)
$services = (array)$t->services->value();
$merge_services = static function (array $services) use (&$allowed_services): void {
foreach ($services as $srv) {
$name = strtoupper((string)$srv);
if (!in_array($name, $allowed_services, true)) {
$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)
try {
@@ -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 . '&reg=' . urlencode($reg), $headers)
->assertStatus(200)
->assertSuccess(true);
$summaryResponse = api_client()
->get('/department/selfserve/washes/summary?lane_id=' . $laneId . '&reg=' . 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();
}
});
@@ -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 {
$route = selfserve_eligibility_route_source();
$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(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)'));
expect($assertBlock)->toContain('$authorized_department_ids = array_values(array_filter(')
->and($assertBlock)->toContain('array_map(\'intval\', (array)$user->getGroup()->getDepartments())')
->and($assertBlock)->toContain('if (!in_array($lane_department_id, $authorized_department_ids, true))')
expect($assertBlock)->toContain('bool $hasGlobalPermission = true')
->and($assertBlock)->toContain('bool $hasOwnPermission = false')
->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)->not->toContain('if ($hasGlobalPermission)');
->and($assertBlock)->toContain('$response->forbidden([$elevatedPermission]);');
});
@@ -21,7 +21,10 @@ class SelfserveLaneStartEntranceTimeoutHarness
public ?\Throwable $reportedTimeout = null;
public ?selfserve_lane_state $laneState = null;
public int $cleanerRelayCalls = 0;
public int $programPickerRelayCalls = 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
{
@@ -47,11 +50,19 @@ class SelfserveLaneStartEntranceTimeoutHarness
protected function turnOnCleanerRelayForWashStart(): void
{
$this->cleanerRelayCalls++;
$this->relayEvents[] = 'cleaner:on';
}
protected function setProgramPickerRelayStatusForWashStart(): void
{
$this->programPickerRelayCalls++;
$this->relayEvents[] = 'program_picker:sync';
}
protected function setMachineRelayStatusForWashStart(): void
{
$this->machineRelayCalls++;
$this->relayEvents[] = 'machine:sync';
}
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();
});
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->runStartRelaySideEffects(true);
expect($lane->cleanerRelayCalls)->toBe(0);
expect($lane->programPickerRelayCalls)->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->runStartRelaySideEffects(false);
expect($lane->cleanerRelayCalls)->toBe(1);
expect($lane->programPickerRelayCalls)->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 = [];
/** @var selfserve_lane_relay[] */
public array $turnedOffRelays = [];
/** @var array<int,string> */
public array $events = [];
private selfserve_lane_status $laneStatus;
private selfserve_lane_mode $laneMode;
@@ -182,6 +184,7 @@ class SelfserveLaneStopFlowHarness
public function open(selfserve_lane_port $port): bool
{
$this->openedPorts[] = $port;
$this->events[] = 'open:' . $port->name;
if ($this->openThrowable !== null) {
throw $this->openThrowable;
}
@@ -192,6 +195,7 @@ class SelfserveLaneStopFlowHarness
public function turnOffRelay(selfserve_lane_relay $relay): bool
{
$this->turnedOffRelays[] = $relay;
$this->events[] = 'relay:' . $relay->name . ':off';
return true;
}
@@ -199,6 +203,7 @@ class SelfserveLaneStopFlowHarness
{
if ($on === false) {
$this->turnedOffRelays[] = $relay;
$this->events[] = 'relay:' . $relay->name . ':off';
}
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);
$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->turnedOffRelays)->toBe([
selfserve_lane_relay::MACHINE_CLEANER,
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
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);
});
@@ -266,8 +278,14 @@ it('skips vehicle type product add when no physical machine ON signal was record
expect($lane->lastVehicleTypeProductDecision)->toBeFalse();
expect($lane->programSelectorStatusReads)->toBe(0);
expect($lane->turnedOffRelays)->toBe([
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
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 {
@@ -301,8 +319,15 @@ it('continues STOP when exit relay dispatch times out ambiguously', function ():
expect($lane->vehicleTypeProductAddCalls)->toBe(1);
expect($lane->turnedOffRelays)->toBe([
selfserve_lane_relay::MACHINE_CLEANER,
selfserve_lane_relay::MACHINE_PROGRAM_PICKER,
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);
});
@@ -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)->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)->not->toContain('syncMachineRelayFromVisibleServices($allowed_services, true)');
expect($moduleSelfServeRoute)->toContain("'relay_sync' => \$relay_sync");
@@ -276,7 +276,7 @@ trait module_config_variable
private static function readDatabaseConfigFromEnvironment(): array
{
$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) {
return $default;
}
@@ -284,9 +284,25 @@ trait module_config_variable
return trim((string)$value);
};
$host = $readEnv('CONFIG_DB_HOST');
$user = $readEnv('CONFIG_DB_USER');
$database = $readEnv('CONFIG_DB_DATABASE');
$dbTarget = strtolower(trim($readEnv('CONFIG_DB_TARGET', 'live')));
if ($dbTarget !== 'live' && $dbTarget !== 'debug') {
$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 === '') {
return [];
@@ -295,10 +311,10 @@ trait module_config_variable
return [
'host' => $host,
'user' => $user,
'password' => $readEnv('CONFIG_DB_PASSWORD'),
'password' => $resolveDbValue('PASSWORD'),
'database' => $database,
'port' => (int)($readEnv('CONFIG_DB_PORT', '3306') ?: '3306'),
'ssl_mode' => $readEnv('CONFIG_DB_SSL_MODE', 'DISABLED') ?: 'DISABLED',
'port' => (int)($resolveDbValue('PORT') ?: '3306'),
'ssl_mode' => $resolveDbValue('SSL_MODE') ?: 'DISABLED',
];
}
+16
View File
@@ -49,6 +49,18 @@ http:
certResolver: le
# 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:
redirect-to-https:
@@ -125,6 +137,10 @@ http:
loadBalancer:
servers:
- url: "http://94.130.142.41:11000"
caddy-local:
loadBalancer:
servers:
- url: "http://caddy"
tls:
options: