Compare commits
497
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03cb74370b | ||
|
|
d05524222b | ||
|
|
c02b1b57c7 | ||
|
|
efff024196 | ||
|
|
021671a45f | ||
|
|
744cf55a7e | ||
|
|
f2c12a9f03 | ||
|
|
5cf4b0f9ea | ||
|
|
370bc9fc5f | ||
|
|
a1d43b9c5a | ||
|
|
90fc66277e | ||
|
|
cd5d353158 | ||
|
|
590146be9e | ||
|
|
8e2c688641 | ||
|
|
f705a851c2 | ||
|
|
d33ff9779c | ||
|
|
876cd5a8fa | ||
|
|
5a64c85fb6 | ||
|
|
31153e90b4 | ||
|
|
949bea723d | ||
|
|
0365daa57b | ||
|
|
3996e02dd9 | ||
|
|
bd488211bd | ||
|
|
ca34d4a4f3 | ||
|
|
a93a8469c4 | ||
|
|
cec481741b | ||
|
|
39bacd2927 | ||
|
|
39972c1418 | ||
|
|
45cfc459aa | ||
|
|
21356f00d5 | ||
|
|
4529fe5472 | ||
|
|
08aaff1e2d | ||
|
|
0884a39f3d | ||
|
|
41b0f56b5b | ||
|
|
b034b8c7f8 | ||
|
|
a4cae4f321 | ||
|
|
ab67f9d7b5 | ||
|
|
a22688c87e | ||
|
|
cfc52a9cf3 | ||
|
|
33e09bbe89 | ||
|
|
a339f8a517 | ||
|
|
abeb91b59f | ||
|
|
350a9e6742 | ||
|
|
c373a98953 | ||
|
|
3863999f2a | ||
|
|
54410628b4 | ||
|
|
2c3ce32282 | ||
|
|
dd03cdd77f | ||
|
|
bc8d4f74c1 | ||
|
|
6f869bdb1a | ||
|
|
6adc0769c7 | ||
|
|
2f9930a240 | ||
|
|
3036a7773c | ||
|
|
e668c2d882 | ||
|
|
bb4768c491 | ||
|
|
9140e4f3fc | ||
|
|
6b205c7a86 | ||
|
|
0aa4bb5055 | ||
|
|
ee3b1f47a7 | ||
|
|
40da2ecaf3 | ||
|
|
5d0ba0ef9a | ||
|
|
786a4306dd | ||
|
|
677ffc7368 | ||
|
|
93eb08c610 | ||
|
|
6365e4b24c | ||
|
|
e138fbeca8 | ||
|
|
d29a6a1835 | ||
|
|
dc91660488 | ||
|
|
e9548d9f38 | ||
|
|
d5944bf717 | ||
|
|
2355442ac0 | ||
|
|
789555078d | ||
|
|
34f190f469 | ||
|
|
8646c4ffdd | ||
|
|
419d3b78c6 | ||
|
|
e8df7d193e | ||
|
|
aa08307500 | ||
|
|
c41524c1fd | ||
|
|
8dec0be375 | ||
|
|
9471abce94 | ||
|
|
2aeed762fd | ||
|
|
512b63b88b | ||
|
|
92d7eac3a1 | ||
|
|
548b8a8ef0 | ||
|
|
fb8aabe34c | ||
|
|
0c1f143b12 | ||
|
|
24ac2efc53 | ||
|
|
e7a139b025 | ||
|
|
cfe19b7a74 | ||
|
|
f725a6b72f | ||
|
|
a097159897 | ||
|
|
da33620ea9 | ||
|
|
0b44394b11 | ||
|
|
2949870acc | ||
|
|
35c289240d | ||
|
|
3b165bf292 | ||
|
|
f0c43a17d8 | ||
|
|
2776429639 | ||
|
|
5c9f8c2a2e | ||
|
|
3e6533829c | ||
|
|
054505d853 | ||
|
|
c793550af5 | ||
|
|
b18fccff9d | ||
|
|
398eb448cb | ||
|
|
6cdede138e | ||
|
|
3046aad94d | ||
|
|
ec2ce6ec62 | ||
|
|
3c61e7eaf3 | ||
|
|
f942600dd8 | ||
|
|
efacbf64cb | ||
|
|
e38445f184 | ||
|
|
00803b2e73 | ||
|
|
a52f65919f | ||
|
|
3d4d88e986 | ||
|
|
5221cea53b | ||
|
|
7636ee41af | ||
|
|
830fd2464c | ||
|
|
d922fd2b9c | ||
|
|
d22078b26e | ||
|
|
195af95995 | ||
|
|
d95d0a8930 | ||
|
|
6fca40963d | ||
|
|
ace251c1dd | ||
|
|
db2e92c058 | ||
|
|
28b62f5071 | ||
|
|
168c128b1d | ||
|
|
90af012e56 | ||
|
|
d6c36b4171 | ||
|
|
9575aad10c | ||
|
|
7b98cccbe9 | ||
|
|
fe2268c897 | ||
|
|
f2c1988d75 | ||
|
|
5b9f35eddc | ||
|
|
c8ed5b2cc7 | ||
|
|
da517b37b6 | ||
|
|
0992c05539 | ||
|
|
ed8a8a2f8b | ||
|
|
5516ef45f6 | ||
|
|
6beb965c85 | ||
|
|
479a88e131 | ||
|
|
e4a1a992be | ||
|
|
97a5d7f9f1 | ||
|
|
776b9d0912 | ||
|
|
df35ed3032 | ||
|
|
59b08ed098 | ||
|
|
936004bc2d | ||
|
|
365658d727 | ||
|
|
d603e57c62 | ||
|
|
58ec94eaeb | ||
|
|
2f4184f60f | ||
|
|
b0b608f12e | ||
|
|
97be776b10 | ||
|
|
ddb876bc1c | ||
|
|
6fee5c048a | ||
|
|
80f22b7cd0 | ||
|
|
e1123577f4 | ||
|
|
5f33f82b8b | ||
|
|
5285ded46f | ||
|
|
69df93b773 | ||
|
|
1e3389e21a | ||
|
|
192b1fdcf4 | ||
|
|
6008a1b2d9 | ||
|
|
62fc734f82 | ||
|
|
7fb5466eb4 | ||
|
|
aafac9d43b | ||
|
|
585f87e1f9 | ||
|
|
eb1d05aa10 | ||
|
|
db1de2653c | ||
|
|
8bdc33b4f0 | ||
|
|
1c1766bbd4 | ||
|
|
96205c14db | ||
|
|
7ec4562064 | ||
|
|
1eb1e9f02f | ||
|
|
0b35b3494b | ||
|
|
e021fc7570 | ||
|
|
2a13a62291 | ||
|
|
60c1459902 | ||
|
|
97f8baba06 | ||
|
|
3ffa80e2bc | ||
|
|
ab1a21dffc | ||
|
|
bccd8c6fc3 | ||
|
|
2ea298854d | ||
|
|
b7e4f29813 | ||
|
|
7780646601 | ||
|
|
0f58f07757 | ||
|
|
ef263e064a | ||
|
|
7127b4d050 | ||
|
|
42744fa30f | ||
|
|
be3f6e6288 | ||
|
|
9291adfdff | ||
|
|
c9426eff3b | ||
|
|
cd350c212e | ||
|
|
1f1ac1a0cc | ||
|
|
2dd5b7ad64 | ||
|
|
62ac8038dc | ||
|
|
5f94f0487b | ||
|
|
64880355e8 | ||
|
|
1dc8489239 | ||
|
|
5831294d33 | ||
|
|
073bbdc0f8 | ||
|
|
ef75383885 | ||
|
|
bfb25a4051 | ||
|
|
f55febd545 | ||
|
|
d27b78cec1 | ||
|
|
1bb145e3be | ||
|
|
59b9a42009 | ||
|
|
a576fa2d23 | ||
|
|
14fe9bd44f | ||
|
|
cce59b8ed6 | ||
|
|
8904e1c7fe | ||
|
|
a0f763af63 | ||
|
|
667c9d4677 | ||
|
|
1622f2be6a | ||
|
|
28914d6586 | ||
|
|
cfc3957fec | ||
|
|
aee2eb848d | ||
|
|
42f17db542 | ||
|
|
6315fbd0f7 | ||
|
|
79e5a3c12d | ||
|
|
e9c1dd8b9d | ||
|
|
b1ff6989ec | ||
|
|
1d97a19982 | ||
|
|
c3519124b7 | ||
|
|
05c769bab2 | ||
|
|
c1539a47f9 | ||
|
|
f54f66cd32 | ||
|
|
c8a33cdf43 | ||
|
|
b4c03160ca | ||
|
|
72cd36a573 | ||
|
|
4d3a4b7c04 | ||
|
|
b9dfeb2d3e | ||
|
|
e42ef2d3c9 | ||
|
|
bb69a28ec2 | ||
|
|
e8c2261b7e | ||
|
|
7665385cbf | ||
|
|
346dee23e4 | ||
|
|
109f36e7f5 | ||
|
|
0e9c5e6e1a | ||
|
|
145a08e511 | ||
|
|
2b8046b416 | ||
|
|
e27b5da3c8 | ||
|
|
613e0b9a5c | ||
|
|
0564e3c424 | ||
|
|
32c73ab7b1 | ||
|
|
420efb25ad | ||
|
|
150998e120 | ||
|
|
31f414b4d8 | ||
|
|
9c6860ecc6 | ||
|
|
6b32b6baa4 | ||
|
|
3527be9c29 | ||
|
|
0239aee89a | ||
|
|
f21f4a0385 | ||
|
|
fa2b5e0d7f | ||
|
|
1ac7720765 | ||
|
|
4182c30b72 | ||
|
|
e4aad8a456 | ||
|
|
d78638e6e5 | ||
|
|
1726523eaf | ||
|
|
1aa564960d | ||
|
|
cd2765bab5 | ||
|
|
390ee81675 | ||
|
|
6dbbfd87f8 | ||
|
|
118f86b2a0 | ||
|
|
27d77624da | ||
|
|
20a67e9b96 | ||
|
|
233748a6c5 | ||
|
|
67491a7f68 | ||
|
|
c7129d5791 | ||
|
|
4abee677f6 | ||
|
|
21600dc2d6 | ||
|
|
f0b84c9943 | ||
|
|
59418adc02 | ||
|
|
de52907eec | ||
|
|
bfb1a04ea0 | ||
|
|
b206c3990b | ||
|
|
54a37c5bf7 | ||
|
|
fa98ceac29 | ||
|
|
1bab230f10 | ||
|
|
5b55d0984a | ||
|
|
2b3a8f1c69 | ||
|
|
696800aafd | ||
|
|
e7997f84eb | ||
|
|
8d3a7b99b4 | ||
|
|
61a8afdde7 | ||
|
|
1f3e79d57c | ||
|
|
f4691ec67e | ||
|
|
32bef9a4e9 | ||
|
|
30704f936a | ||
|
|
9167c84b4c | ||
|
|
f5f5d43ab5 | ||
|
|
9b2ad3845d | ||
|
|
e2513d6245 | ||
|
|
126fce9687 | ||
|
|
c1364a458b | ||
|
|
ecc4741f56 | ||
|
|
f33e55f201 | ||
|
|
ce1b9ece09 | ||
|
|
9d28d2f102 | ||
|
|
aaa7e9f4c3 | ||
|
|
26681e5c62 | ||
|
|
33c3dc6022 | ||
|
|
a6b0051f1b | ||
|
|
d0d1edb3ae | ||
|
|
6e96f6fb44 | ||
|
|
c6b28e7f4e | ||
|
|
3ce4d81a4c | ||
|
|
d5c572c25b | ||
|
|
bcded403be | ||
|
|
aeb25f3b7e | ||
|
|
421851cc4c | ||
|
|
66eb392151 | ||
|
|
beaccd93e0 | ||
|
|
111cda95f3 | ||
|
|
84ed41f325 | ||
|
|
e7cf172b9f | ||
|
|
e52cb98163 | ||
|
|
d85e81e8b9 | ||
|
|
1e5fe23159 | ||
|
|
2f78ddb8ad | ||
|
|
f19762d686 | ||
|
|
eba2f8e503 | ||
|
|
0a0bef9e82 | ||
|
|
29f415a91f | ||
|
|
a619add4e3 | ||
|
|
34a8d42fc3 | ||
|
|
cfd346d5df | ||
|
|
3d694ed7eb | ||
|
|
5021f414d1 | ||
|
|
51dbf20118 | ||
|
|
a50bd205cd | ||
|
|
da7b4dfb48 | ||
|
|
eee7be0c1e | ||
|
|
d55a201681 | ||
|
|
ba0eab9e96 | ||
|
|
b63804df88 | ||
|
|
647a25a78f | ||
|
|
6b0dfb3426 | ||
|
|
c8bb051ae6 | ||
|
|
b326510b04 | ||
|
|
293574187f | ||
|
|
b74f84e8ad | ||
|
|
19a272db96 | ||
|
|
67843c2f2e | ||
|
|
a01e966d6d | ||
|
|
9140b97a71 | ||
|
|
405f03ee7a | ||
|
|
b88ad24c3d | ||
|
|
bb630da5ed | ||
|
|
39d944351c | ||
|
|
2b272ef789 | ||
|
|
3e54a58b9b | ||
|
|
60176d19f4 | ||
|
|
9c716477aa | ||
|
|
be2fffad23 | ||
|
|
34230e5520 | ||
|
|
1949bf9b97 | ||
|
|
7ab60b1048 | ||
|
|
0e6c31b8ce | ||
|
|
a2cfb693e0 | ||
|
|
10df3ebb35 | ||
|
|
ab91c138d5 | ||
|
|
cc1d1e5746 | ||
|
|
8c75d0070d | ||
|
|
4df04d0504 | ||
|
|
38bc594ca4 | ||
|
|
fb9c9d91ee | ||
|
|
ee69346eb4 | ||
|
|
e6c47019d5 | ||
|
|
b894bbf5e6 | ||
|
|
0f8fed605c | ||
|
|
479b177e90 | ||
|
|
08210b8e52 | ||
|
|
b5dfe17b73 | ||
|
|
49b5ccf308 | ||
|
|
434c4d0a11 | ||
|
|
dc7ed0728a | ||
|
|
b1dbb15ad7 | ||
|
|
e76170b6dd | ||
|
|
8225d4e92d | ||
|
|
a5addf8d33 | ||
|
|
0542a669bd | ||
|
|
49b7a3fe34 | ||
|
|
ead2c092da | ||
|
|
6e818c3480 | ||
|
|
a02490ea36 | ||
|
|
9eda12981a | ||
|
|
4939a1a9ad | ||
|
|
989f599b54 | ||
|
|
67c642bc2f | ||
|
|
cba0f747cd | ||
|
|
c3008d7f7a | ||
|
|
7304fb50c6 | ||
|
|
ec1bba7c46 | ||
|
|
b66d453881 | ||
|
|
b7b80296e9 | ||
|
|
2f582f0548 | ||
|
|
fad28c2442 | ||
|
|
7f1ef4059d | ||
|
|
139ec3c1db | ||
|
|
798beae597 | ||
|
|
3edf5a80aa | ||
|
|
fc93b28af0 | ||
|
|
cd82b70139 | ||
|
|
fba9edc931 | ||
|
|
8e43edcbaf | ||
|
|
8984b2edd5 | ||
|
|
32ec2c1deb | ||
|
|
25029dff5d | ||
|
|
875b181aff | ||
|
|
0f3d9b87fa | ||
|
|
d7929dd855 | ||
|
|
8a495346d6 | ||
|
|
fef4997132 | ||
|
|
b4eaf695c7 | ||
|
|
372490ed5a | ||
|
|
e37b6234e8 | ||
|
|
71b42786f7 | ||
|
|
009eabd09c | ||
|
|
49a4393980 | ||
|
|
c0e20a15fe | ||
|
|
061317ed4c | ||
|
|
a5dce776de | ||
|
|
90ae78262a | ||
|
|
0809f07d46 | ||
|
|
199ef08e35 | ||
|
|
4930a2ba42 | ||
|
|
0ceabe465b | ||
|
|
bd7b70e465 | ||
|
|
55a24b99f0 | ||
|
|
122df1a3f5 | ||
|
|
391dd63ebf | ||
|
|
9d44197246 | ||
|
|
582fb9b52d | ||
|
|
eed7ba4d7b | ||
|
|
5642193610 | ||
|
|
1a1e120cf6 | ||
|
|
cf66b85993 | ||
|
|
8677e9a8a5 | ||
|
|
906f591979 | ||
|
|
97cc0ac422 | ||
|
|
bdc7100f74 | ||
|
|
29a51ed66d | ||
|
|
c9a258a350 | ||
|
|
096dd96db0 | ||
|
|
ddd5971c06 | ||
|
|
b7638f5e9e | ||
|
|
b197152f12 | ||
|
|
297becf7a0 | ||
|
|
b6f991b291 | ||
|
|
5469c834a6 | ||
|
|
a3ea02a136 | ||
|
|
195246af89 | ||
|
|
2953ed0b2d | ||
|
|
ccb2fa75ac | ||
|
|
a33e6a0dd4 | ||
|
|
435e3a79e8 | ||
|
|
21054f0c45 | ||
|
|
605b6ca64c | ||
|
|
bc9616e7de | ||
|
|
491169b9d7 | ||
|
|
0e0ce254a9 | ||
|
|
7b25fbf28b | ||
|
|
2a5100e76d | ||
|
|
e98695ea06 | ||
|
|
1c2421fb7e | ||
|
|
76aeb101bd | ||
|
|
60b655a195 | ||
|
|
369d660e24 | ||
|
|
47703dc205 | ||
|
|
25bbf3d44f | ||
|
|
803b10f981 | ||
|
|
b992e4a5ea | ||
|
|
9c5597eb23 | ||
|
|
2ec61d5086 | ||
|
|
8bae958e24 | ||
|
|
e7f0c09b36 | ||
|
|
ae2ed306b9 | ||
|
|
2b122a7cc4 | ||
|
|
787e76c21c | ||
|
|
7b1f2e7468 | ||
|
|
3f94c51bc8 | ||
|
|
64d51c01b2 | ||
|
|
955dbd2f05 | ||
|
|
318a326fdb | ||
|
|
86223dc8a6 | ||
|
|
4bb62830a4 | ||
|
|
4ce731bfcb | ||
|
|
f225dac5fe | ||
|
|
5e1f2a8711 | ||
|
|
8746397d83 | ||
|
|
e8c27cb51b | ||
|
|
d2a80ea8fd | ||
|
|
c85c72863c | ||
|
|
77afc0fe7b | ||
|
|
7a53908b80 | ||
|
|
ca4495169f | ||
|
|
d1344bbfb4 |
No files matched your search
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
## Description: Run Claude Code
|
||||
## Usage: claude [args]
|
||||
## Example: ddev claude
|
||||
## Example: ddev claude --help
|
||||
## Example: ddev claude doctor
|
||||
|
||||
# Ensure ~/.local/bin is in PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Check if claude is installed
|
||||
if [ ! -f "$HOME/.local/bin/claude" ]; then
|
||||
echo "Claude Code is not installed. Installing now..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
fi
|
||||
|
||||
# Run claude with all arguments
|
||||
exec "$HOME/.local/bin/claude" "$@"
|
||||
@@ -40,12 +40,6 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
MAILER_DSN=null://null
|
||||
###< symfony/mailer ###
|
||||
|
||||
MAILJET_API_KEY=
|
||||
MAILJET_API_SECRET=
|
||||
MAILJET_API_BASE_URL=https://api.mailjet.com/v3/REST
|
||||
MAILJET_NEWSLETTER_LIST_ID=
|
||||
APP_NEWSLETTER_CONFIRMATION_TTL_HOURS=1
|
||||
|
||||
APP_BPN_USER=
|
||||
APP_BPN_PASSWORD=
|
||||
APP_BPN_IP=
|
||||
@@ -66,14 +60,6 @@ APP_CUSTOMER_SERVICE_EMAIL="[email protected]"
|
||||
# Travel Data Service Configuration
|
||||
APP_TRAVEL_PREFER_REMOTE=false
|
||||
APP_TRAVEL_ENABLE_FALLBACK=true
|
||||
APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS=14
|
||||
|
||||
# Feature Flags
|
||||
FEATURE_TRAVEL_SNAPSHOT=true
|
||||
|
||||
# Emails
|
||||
APP_DEFAULT_EMAIL_FROM=[email protected]
|
||||
APP_DEFAULT_EMAIL_TO=[email protected]
|
||||
|
||||
# Booking Configuration
|
||||
# Status for new bookings: 'F' (Fixed/Final), 'O' (Option - requires agency confirmation)
|
||||
|
||||
@@ -33,7 +33,3 @@ yarn-error.log
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
/ray.php
|
||||
|
||||
###> phpstan/phpstan ###
|
||||
phpstan.neon
|
||||
###< phpstan/phpstan ###
|
||||
@@ -60,7 +60,7 @@ Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
### API travel
|
||||
# @no-cookie-jar
|
||||
GET {{base_url}}/api/travels/DPWMP140326
|
||||
GET {{base_url}}/api/travels/SBW131225
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.3 KiB |
@@ -1,7 +1,7 @@
|
||||
.button {
|
||||
@apply inline-flex items-center justify-center space-x-2 h-8 leading-8 md:h-10 md:leading-10 px-4 md:px-8 cursor-pointer;
|
||||
@apply uppercase leading-none text-center text-sm md:text-base;
|
||||
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-dark;
|
||||
@apply inline-flex items-center justify-center space-x-2 h-10 leading-10 px-4 md:px-8 cursor-pointer;
|
||||
@apply uppercase leading-none text-center;
|
||||
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-light;
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
@@ -20,47 +20,42 @@
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
@apply bg-none bg-primary-light text-white;
|
||||
@apply hover:bg-primary-light/50;
|
||||
}
|
||||
|
||||
.button--neutral {
|
||||
@apply bg-none bg-gray-200;
|
||||
@apply hover:bg-gray-200/50;
|
||||
@apply bg-none bg-primary-dark text-white;
|
||||
@apply hover:bg-primary-light/80;
|
||||
}
|
||||
|
||||
.sbw {
|
||||
.button--primary {
|
||||
@apply bg-none bg-sbw-secondary text-white;
|
||||
@apply hover:bg-none hover:bg-sbw-primary/50;
|
||||
@apply hover:bg-none hover:bg-sbw-primary/80;
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
@apply bg-none bg-sbw-primary-dark text-white;
|
||||
@apply hover:bg-none hover:bg-sbw-primary/50;
|
||||
@apply hover:bg-none hover:bg-sbw-primary/80;
|
||||
}
|
||||
}
|
||||
|
||||
.snz {
|
||||
.button--primary {
|
||||
@apply bg-none bg-snz-secondary text-white;
|
||||
@apply hover:bg-none hover:bg-snz-primary/50;
|
||||
@apply hover:bg-none hover:bg-snz-primary/80;
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
@apply bg-none bg-snz-primary-dark text-white;
|
||||
@apply hover:bg-none hover:bg-snz-primary/50;
|
||||
@apply hover:bg-none hover:bg-snz-primary/80;
|
||||
}
|
||||
}
|
||||
|
||||
.ser {
|
||||
.button--primary {
|
||||
@apply bg-none bg-ser-secondary text-white;
|
||||
@apply hover:bg-none hover:bg-ser-primary/50;
|
||||
@apply hover:bg-none hover:bg-ser-primary/80;
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
@apply bg-none bg-ser-primary text-white;
|
||||
@apply hover:bg-none hover:bg-ser-primary/50;
|
||||
@apply hover:bg-none hover:bg-ser-primary/80;
|
||||
}
|
||||
}
|
||||
+36
-40
@@ -4,7 +4,7 @@
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"php": ">=8.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-iconv": "*",
|
||||
@@ -16,7 +16,6 @@
|
||||
"doctrine/orm": "^3.3",
|
||||
"easycorp/easyadmin-bundle": "^4.27",
|
||||
"fakerphp/faker": "^1.24",
|
||||
"flagception/flagception-bundle": "^6.1",
|
||||
"league/flysystem-bundle": "^3.4",
|
||||
"league/flysystem-sftp-v3": "^3.29",
|
||||
"league/oauth2-server-bundle": "^1.0",
|
||||
@@ -27,38 +26,38 @@
|
||||
"phpstan/phpdoc-parser": "^2.0",
|
||||
"spatie/crypto": "^2.1",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "7.4.*",
|
||||
"symfony/console": "7.4.*",
|
||||
"symfony/doctrine-messenger": "7.4.*",
|
||||
"symfony/dom-crawler": "7.4.*",
|
||||
"symfony/dotenv": "7.4.*",
|
||||
"symfony/expression-language": "7.4.*",
|
||||
"symfony/asset": "6.4.*",
|
||||
"symfony/console": "6.4.*",
|
||||
"symfony/doctrine-messenger": "6.4.*",
|
||||
"symfony/dom-crawler": "6.4.*",
|
||||
"symfony/dotenv": "6.4.*",
|
||||
"symfony/expression-language": "6.4.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/form": "7.4.*",
|
||||
"symfony/framework-bundle": "7.4.*",
|
||||
"symfony/html-sanitizer": "7.4.*",
|
||||
"symfony/http-client": "7.4.*",
|
||||
"symfony/intl": "7.4.*",
|
||||
"symfony/lock": "7.4.*",
|
||||
"symfony/mailer": "7.4.*",
|
||||
"symfony/mime": "7.4.*",
|
||||
"symfony/form": "6.4.*",
|
||||
"symfony/framework-bundle": "6.4.*",
|
||||
"symfony/html-sanitizer": "6.4.*",
|
||||
"symfony/http-client": "6.4.*",
|
||||
"symfony/intl": "6.4.*",
|
||||
"symfony/lock": "6.4.*",
|
||||
"symfony/mailer": "6.4.*",
|
||||
"symfony/mime": "6.4.*",
|
||||
"symfony/monolog-bundle": "^3.0",
|
||||
"symfony/notifier": "7.4.*",
|
||||
"symfony/process": "7.4.*",
|
||||
"symfony/property-access": "7.4.*",
|
||||
"symfony/property-info": "7.4.*",
|
||||
"symfony/runtime": "7.4.*",
|
||||
"symfony/security-bundle": "7.4.*",
|
||||
"symfony/serializer": "7.4.*",
|
||||
"symfony/notifier": "6.4.*",
|
||||
"symfony/process": "6.4.*",
|
||||
"symfony/property-access": "6.4.*",
|
||||
"symfony/property-info": "6.4.*",
|
||||
"symfony/runtime": "6.4.*",
|
||||
"symfony/security-bundle": "6.4.*",
|
||||
"symfony/serializer": "6.4.*",
|
||||
"symfony/stimulus-bundle": "^2.21",
|
||||
"symfony/string": "7.4.*",
|
||||
"symfony/translation": "7.4.*",
|
||||
"symfony/twig-bundle": "7.4.*",
|
||||
"symfony/uid": "7.4.*",
|
||||
"symfony/validator": "7.4.*",
|
||||
"symfony/web-link": "7.4.*",
|
||||
"symfony/string": "6.4.*",
|
||||
"symfony/translation": "6.4.*",
|
||||
"symfony/twig-bundle": "6.4.*",
|
||||
"symfony/uid": "6.4.*",
|
||||
"symfony/validator": "6.4.*",
|
||||
"symfony/web-link": "6.4.*",
|
||||
"symfony/webpack-encore-bundle": "^2.2",
|
||||
"symfony/yaml": "7.4.*",
|
||||
"symfony/yaml": "6.4.*",
|
||||
"twig/extra-bundle": "^2.12|^3.0",
|
||||
"twig/html-extra": "^3.15",
|
||||
"twig/intl-extra": "*",
|
||||
@@ -113,25 +112,22 @@
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": true,
|
||||
"require": "7.4.*",
|
||||
"require": "6.4.*",
|
||||
"docker": true
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"deployer/deployer": "^7.5",
|
||||
"friendsofphp/php-cs-fixer": "^3.75",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-doctrine": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"spatie/ray": "^1.43",
|
||||
"spatie/x-ray": "^1.2",
|
||||
"symfony/browser-kit": "7.4.*",
|
||||
"symfony/css-selector": "7.4.*",
|
||||
"symfony/debug-bundle": "7.4.*",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
"symfony/debug-bundle": "6.4.*",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/phpunit-bridge": "7.4.*",
|
||||
"symfony/stopwatch": "7.4.*",
|
||||
"symfony/web-profiler-bundle": "7.4.*"
|
||||
"symfony/phpunit-bridge": "^7.1",
|
||||
"symfony/stopwatch": "6.4.*",
|
||||
"symfony/web-profiler-bundle": "6.4.*"
|
||||
}
|
||||
}
|
||||
Generated
+1102
-1478
File diff suppressed because it is too large.
Load diff
@@ -18,5 +18,4 @@ return [
|
||||
Zenstruck\ScheduleBundle\ZenstruckScheduleBundle::class => ['all' => true],
|
||||
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
||||
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
|
||||
Flagception\Bundle\FlagceptionBundle\FlagceptionBundle::class => ['all' => true],
|
||||
];
|
||||
@@ -1,11 +0,0 @@
|
||||
# Enable stateless CSRF protection for forms and logins/logouts
|
||||
framework:
|
||||
form:
|
||||
csrf_protection:
|
||||
token_id: submit
|
||||
|
||||
csrf_protection:
|
||||
stateless_token_ids:
|
||||
- submit
|
||||
- authenticate
|
||||
- logout
|
||||
@@ -1,2 +0,0 @@
|
||||
flagception:
|
||||
features: {}
|
||||
@@ -13,7 +13,7 @@ framework:
|
||||
max_retries: 3
|
||||
multiplier: 2
|
||||
failed: 'doctrine://default?queue_name=failed'
|
||||
sync: 'sync://'
|
||||
# sync: 'sync://'
|
||||
|
||||
default_bus: messenger.bus.default
|
||||
|
||||
@@ -21,7 +21,7 @@ framework:
|
||||
messenger.bus.default: []
|
||||
|
||||
routing:
|
||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: sync
|
||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
|
||||
Symfony\Component\Notifier\Message\ChatMessage: async
|
||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ when@test:
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod: &prod_config
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
database:
|
||||
@@ -104,6 +104,4 @@ when@prod: &prod_config
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
||||
|
||||
when@staging: *prod_config
|
||||
formatter: monolog.formatter.json
|
||||
@@ -1,3 +0,0 @@
|
||||
framework:
|
||||
property_info:
|
||||
with_constructor_extractor: true
|
||||
@@ -1,8 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
form_themes: ['forms.html.twig']
|
||||
paths:
|
||||
'%kernel.project_dir%/assets/images': images
|
||||
globals:
|
||||
htmx_change_trigger: 'change delay:100ms'
|
||||
website_base_url: '%env(APP_WEBSITE_BASE_URL)%'
|
||||
|
||||
@@ -24,22 +24,6 @@ zenstruck_schedule:
|
||||
frequency: "30 1 * * *"
|
||||
description: "Removes expired drafts"
|
||||
|
||||
- task: app:cleanup:newsletter-opt-in-requests
|
||||
frequency: "45 1 * * *"
|
||||
description: "Removes expired pending newsletter double opt-in requests"
|
||||
|
||||
- task: league:oauth2-server:clear-expired-tokens
|
||||
frequency: "0 * * * *"
|
||||
description: "Removes expired oauth2 tokens hourly"
|
||||
|
||||
- task: app:bpn:refresh-travel-snapshot --refresh-after 60
|
||||
frequency: "10 8-19 * * *"
|
||||
description: "Syncs travel data snapshots with BusPro XML data hourly during peak hours"
|
||||
|
||||
- task: app:bpn:refresh-travel-snapshot --refresh-after 180
|
||||
frequency: "10 20-23,0-7 * * *"
|
||||
description: "Syncs travel data snapshots with BusPro XML data every 3 hours outside peak hours"
|
||||
|
||||
when@staging:
|
||||
zenstruck_schedule:
|
||||
mailer:
|
||||
@@ -53,8 +37,8 @@ when@prod:
|
||||
tasks:
|
||||
- task: app:bpn:xml-sync
|
||||
frequency: "*/15 8-19 * * *"
|
||||
description: "Syncs BusPro XML data every 15 min. during peak hours"
|
||||
description: "Sync BusPro XML data every 15 min. during peak hours"
|
||||
|
||||
- task: app:bpn:xml-sync
|
||||
frequency: "0 20-23,0-7 * * *"
|
||||
description: "Syncs BusPro XML data hourly outside peak hours"
|
||||
description: "Sync BusPro XML data hourly outside peak hours"
|
||||
File diff suppressed because it is too large.
Load diff
+8
-38
@@ -9,10 +9,6 @@ parameters:
|
||||
path_to_keys: '%kernel.project_dir%/config/secret'
|
||||
bpn_debug: '%env(APP_BPN_DEBUG)%'
|
||||
default_booking_status: '%env(DEFAULT_BOOKING_STATUS)%'
|
||||
newsletter_confirmation_ttl_hours: '%env(int:APP_NEWSLETTER_CONFIRMATION_TTL_HOURS)%'
|
||||
travel_snapshot_retention_buffer_days: '%env(int:APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS)%'
|
||||
default_email_from: '%env(APP_DEFAULT_EMAIL_FROM)%'
|
||||
default_email_to: '%env(APP_DEFAULT_EMAIL_TO)%'
|
||||
|
||||
# Body dimensions choices for BodyDimensionsType
|
||||
body_dimensions.height_choices:
|
||||
@@ -85,18 +81,12 @@ services:
|
||||
bpn_username: '%env(APP_BPN_USER)%'
|
||||
bpn_password: '%env(APP_BPN_PASSWORD)%'
|
||||
bpn_api_ip: '%env(APP_BPN_IP)%'
|
||||
bpn_api_ports: '%env(csv_int:APP_BPN_PORTS)%'
|
||||
bpn_api_ports: '%env(csv:APP_BPN_PORTS)%'
|
||||
debug: '%env(bool:APP_BPN_DEBUG)%'
|
||||
connection_timeout: '%env(int:APP_BPN_CONNECTION_TIMEOUT)%'
|
||||
stream_timeout: '%env(int:APP_BPN_STREAM_TIMEOUT)%'
|
||||
total_timeout: '%env(int:APP_BPN_TOTAL_TIMEOUT)%'
|
||||
|
||||
App\DependencyInjection\Env\CsvIntEnvVarProcessor: ~
|
||||
|
||||
App\BusProNet\XmlParser\DocumentsParser:
|
||||
arguments:
|
||||
$tempDir: '%kernel.project_dir%/temp'
|
||||
|
||||
App\Twig\AppRuntime:
|
||||
arguments:
|
||||
$intlExtension: '@twig.extension.intl'
|
||||
@@ -109,21 +99,17 @@ services:
|
||||
arguments:
|
||||
$path: '%path_to_keys%'
|
||||
|
||||
App\Service\TravelSnapshotManager:
|
||||
arguments:
|
||||
$retentionBufferDays: '%travel_snapshot_retention_buffer_days%'
|
||||
|
||||
App\Service\TravelDataProvider:
|
||||
App\Service\TravelDataService:
|
||||
arguments:
|
||||
$logger: '@monolog.logger.core'
|
||||
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
|
||||
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
|
||||
|
||||
App\Service\BookingEditDataLoader:
|
||||
App\Service\BookingEditDataLoaderService:
|
||||
arguments:
|
||||
$bpnCache: '@bpn.cache'
|
||||
|
||||
App\Service\VoucherValidator:
|
||||
App\Service\VoucherValidationService:
|
||||
arguments:
|
||||
$cache: '@cache.app'
|
||||
$logger: '@monolog.logger.bpn'
|
||||
@@ -162,16 +148,16 @@ services:
|
||||
- '@App\Form\Service\ParticipantPurchaseVoucherFieldHandler'
|
||||
- '@App\Form\Service\ParticipantPromoVoucherFieldHandler'
|
||||
|
||||
# Booking Status Rules
|
||||
# Participant Status Rules
|
||||
App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule: ~
|
||||
|
||||
# Booking Status Rule Registry
|
||||
App\BusProNet\Service\BookingStatusRuleRegistry:
|
||||
# Participant Status Rule Registry
|
||||
App\BusProNet\Service\ParticipantStatusRuleRegistry:
|
||||
arguments:
|
||||
$rules:
|
||||
- '@App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule'
|
||||
|
||||
App\Service\CmsDataProvider:
|
||||
App\Service\CmsDataService:
|
||||
arguments:
|
||||
$httpClient: '@typo3.client'
|
||||
$apiKey: 'AbcAbc123'# dummy key, not yet implemented
|
||||
@@ -180,23 +166,7 @@ services:
|
||||
tags:
|
||||
- { name: monolog.processor }
|
||||
|
||||
App\Service\NewsletterManager:
|
||||
arguments:
|
||||
$newsletterConfirmationTtlHours: '%newsletter_confirmation_ttl_hours%'
|
||||
|
||||
App\Service\DomainConfigProvider:
|
||||
arguments:
|
||||
$domainConfig: '%domain_config%'
|
||||
|
||||
App\Email\Mailer:
|
||||
arguments:
|
||||
$defaults:
|
||||
from: '%default_email_from%'
|
||||
to: '%default_email_to%'
|
||||
|
||||
App\Service\MailjetApiClient:
|
||||
arguments:
|
||||
$mailjetApiKey: '%env(default::MAILJET_API_KEY)%'
|
||||
$mailjetApiSecret: '%env(default::MAILJET_API_SECRET)%'
|
||||
$mailjetApiBaseUrl: '%env(default::MAILJET_API_BASE_URL)%'
|
||||
$mailjetNewsletterListId: '%env(default::MAILJET_NEWSLETTER_LIST_ID)%'
|
||||
@@ -0,0 +1,484 @@
|
||||
# Symfony 7.4 Upgrade Plan
|
||||
|
||||
**Project:** MyEP Next Booking
|
||||
**Current Version:** Symfony 6.4.x (6.4.33)
|
||||
**Target Version:** Symfony 7.4 (LTS)
|
||||
**Date:** 2026-02-23
|
||||
**Status:** Ready for Upgrade
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Estimated Effort: LOW**
|
||||
**Risk Level: LOW**
|
||||
**Status: READY FOR UPGRADE**
|
||||
|
||||
The MyEP Next Booking application is well-positioned for a Symfony 7.4 upgrade. The codebase follows modern Symfony best practices, uses PHP 8 features extensively, and avoids most deprecated patterns. All pre-upgrade fixes have been completed.
|
||||
|
||||
**Key Facts:**
|
||||
- Symfony 7.4 was released on November 27, 2025
|
||||
- Requires PHP 8.2.0+ (current: PHP 8.3.30)
|
||||
- LTS version with bug fixes until November 2028, security fixes until November 2029
|
||||
- Symfony 8.0 = Symfony 7.4 minus deprecations (same features, PHP 8.4 required)
|
||||
|
||||
**Pre-Upgrade Status:**
|
||||
- PHP version: 8.3.30 (compatible; Symfony 7.4 requires 8.2+)
|
||||
- OAuth2 Server Bundle: 1.1.0 (compatible)
|
||||
- Stimulus Bundle: 2.32.0 (compatible)
|
||||
- Doctrine ORM: 3.6.0 (compatible)
|
||||
- All 433 tests passing
|
||||
- Email validation mode: Configured (html5)
|
||||
- 18 test-only deprecation warnings (cosmetic, do not affect runtime)
|
||||
|
||||
---
|
||||
|
||||
## Previous Plan Status
|
||||
|
||||
The previous upgrade plan (dated 2025-12-06) has been updated. All pre-upgrade tasks are now complete.
|
||||
|
||||
### Completed Items
|
||||
| Item | Previous Status | Current Status |
|
||||
|------|----------------|----------------|
|
||||
| OAuth2 Server Bundle | 1.0.0 | **1.1.0 installed** |
|
||||
| Stimulus Bundle | 2.31.0 | **2.32.0 installed** |
|
||||
| PHP 8.2 deprecations (ParticipantDto::$touched) | Planned | **Completed** |
|
||||
| Doctrine config (controller_resolver) | Planned | **Completed** |
|
||||
| SecurityController null safety | Needs verification | **Completed** (uses `?->` and `??`) |
|
||||
| Email validator "loose" mode deprecation | Needs fix | **Completed** (validator.yaml configured) |
|
||||
| Failing test in BookingDataProcessorTest | Unrelated to upgrade | **Resolved** (test now passes) |
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### PHP Version Compatibility
|
||||
|
||||
- **Current Runtime:** PHP 8.3.30
|
||||
- **Composer Platform:** PHP 8.3.30
|
||||
- **Symfony 7.4 Required:** PHP 8.2.0+
|
||||
- **Status:** Compatible
|
||||
|
||||
### Installed Dependencies
|
||||
|
||||
#### Core Symfony Packages (6.4.x)
|
||||
All 33 Symfony packages currently at 6.4.x need version constraint updates to 7.4.*
|
||||
|
||||
#### Third-Party Bundles - Compatible
|
||||
| Package | Installed | Symfony 7.4 Compatible |
|
||||
|---------|-----------|------------------------|
|
||||
| `doctrine/orm` | 3.6.0 | Yes |
|
||||
| `doctrine/doctrine-bundle` | 2.18.1 | Yes |
|
||||
| `doctrine/doctrine-migrations-bundle` | 3.7.0 | Yes |
|
||||
| `league/oauth2-server-bundle` | 1.1.0 | Yes |
|
||||
| `symfony/stimulus-bundle` | 2.32.0 | Yes |
|
||||
| `symfony/webpack-encore-bundle` | 2.4.0 | Yes |
|
||||
| `symfony/monolog-bundle` | 3.11.0 | Yes |
|
||||
| `zenstruck/schedule-bundle` | 1.9.0 | Yes |
|
||||
| `league/flysystem-bundle` | 3.6.1 | Yes |
|
||||
| `league/flysystem-sftp-v3` | 3.30.0 | Yes |
|
||||
| `nelexa/zip` | 4.0.2 | Yes (verify post-upgrade) |
|
||||
| `spatie/crypto` | 2.1.1 | Yes |
|
||||
| `easycorp/easyadmin-bundle` | 4.29.1 (^4.27) | Yes |
|
||||
|
||||
### Code Quality Assessment
|
||||
|
||||
The codebase implements modern Symfony/PHP patterns:
|
||||
- All controllers use PHP 8 attributes (`#[Route]`)
|
||||
- Constructor property promotion used throughout
|
||||
- Strict types enforced on all files
|
||||
- Typed properties and return type declarations
|
||||
- Entities use PHP 8 attributes for ORM mapping
|
||||
- Modern security system (AbstractLoginFormAuthenticator)
|
||||
- No deprecated annotation usage
|
||||
|
||||
---
|
||||
|
||||
## Pre-Upgrade Fixes - COMPLETED
|
||||
|
||||
### 1. Email Validator "loose" Mode Deprecation - DONE
|
||||
|
||||
**Issue:** The default mode for `#[Assert\Email]` is "loose", which is deprecated in Symfony 6.2+ and removed in Symfony 7.0.
|
||||
|
||||
**Resolution:** Framework default configured in `config/packages/validator.yaml`:
|
||||
|
||||
```yaml
|
||||
framework:
|
||||
validation:
|
||||
email_validation_mode: html5
|
||||
```
|
||||
|
||||
All Email constraints in the codebase also explicitly use `mode: 'strict'`:
|
||||
- `src/Form/Model/ParticipantDto.php` - Has `mode: 'strict'`
|
||||
- `src/Form/Model/RegistrationDto.php` - Has `mode: 'strict'`
|
||||
- `src/BusProNet/Model/Communication.php` - Has `mode: 'strict'`
|
||||
- `src/Controller/ResetPasswordController.php` - Has `mode: 'strict'`
|
||||
|
||||
### 2. Test Deprecation Warnings - COSMETIC ONLY
|
||||
|
||||
**Issue:** 18 deprecation warnings appear during test runs.
|
||||
|
||||
**Cause:** `ParticipantEditDtoTest` creates a standalone validator using `Validation::createValidatorBuilder()` which bypasses the framework's `validator.yaml` configuration.
|
||||
|
||||
**Impact:** These warnings are cosmetic and do not affect:
|
||||
- Runtime behavior (all Email constraints use explicit `mode: 'strict'`)
|
||||
- Symfony 7.4 compatibility (no code changes needed)
|
||||
- Test results (all 433 tests pass)
|
||||
|
||||
**Note:** Warnings can be silenced with `SYMFONY_DEPRECATIONS_HELPER=disabled` if desired.
|
||||
|
||||
---
|
||||
|
||||
## Required Changes
|
||||
|
||||
### 1. Update Composer Version Constraints
|
||||
|
||||
**File:** `composer.json`
|
||||
|
||||
#### Symfony Core Packages
|
||||
Update all packages from `6.4.*` to `7.4.*`:
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"symfony/asset": "7.4.*",
|
||||
"symfony/console": "7.4.*",
|
||||
"symfony/doctrine-messenger": "7.4.*",
|
||||
"symfony/dom-crawler": "7.4.*",
|
||||
"symfony/dotenv": "7.4.*",
|
||||
"symfony/expression-language": "7.4.*",
|
||||
"symfony/form": "7.4.*",
|
||||
"symfony/framework-bundle": "7.4.*",
|
||||
"symfony/html-sanitizer": "7.4.*",
|
||||
"symfony/http-client": "7.4.*",
|
||||
"symfony/intl": "7.4.*",
|
||||
"symfony/lock": "7.4.*",
|
||||
"symfony/mailer": "7.4.*",
|
||||
"symfony/mime": "7.4.*",
|
||||
"symfony/notifier": "7.4.*",
|
||||
"symfony/process": "7.4.*",
|
||||
"symfony/property-access": "7.4.*",
|
||||
"symfony/property-info": "7.4.*",
|
||||
"symfony/runtime": "7.4.*",
|
||||
"symfony/security-bundle": "7.4.*",
|
||||
"symfony/serializer": "7.4.*",
|
||||
"symfony/string": "7.4.*",
|
||||
"symfony/translation": "7.4.*",
|
||||
"symfony/twig-bundle": "7.4.*",
|
||||
"symfony/uid": "7.4.*",
|
||||
"symfony/validator": "7.4.*",
|
||||
"symfony/web-link": "7.4.*",
|
||||
"symfony/yaml": "7.4.*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Development Packages
|
||||
|
||||
```json
|
||||
{
|
||||
"require-dev": {
|
||||
"symfony/browser-kit": "7.4.*",
|
||||
"symfony/css-selector": "7.4.*",
|
||||
"symfony/debug-bundle": "7.4.*",
|
||||
"symfony/maker-bundle": "^1.0",
|
||||
"symfony/phpunit-bridge": "7.4.*",
|
||||
"symfony/stopwatch": "7.4.*",
|
||||
"symfony/web-profiler-bundle": "7.4.*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Keep `symfony/maker-bundle` at `^1.0` (compatible with Symfony 7.4).
|
||||
|
||||
### 2. Update Symfony Flex Configuration
|
||||
|
||||
**File:** `composer.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": true,
|
||||
"require": "7.4.*",
|
||||
"docker": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Potential Breaking Changes
|
||||
|
||||
### 1. Symfony Core Changes (Symfony 7.0 -> 7.4)
|
||||
|
||||
Symfony 7.0 removed all deprecated features from 6.4. Our codebase is fully compliant:
|
||||
|
||||
- **Native Return Types:** Already implemented
|
||||
- **Attribute-based Configuration:** Already implemented
|
||||
- **Security System:** Modern authenticator pattern in use
|
||||
- **Form System:** Modern approach in use
|
||||
- **No Annotation Usage:** All using PHP 8 attributes
|
||||
|
||||
### 2. Email Validation Mode
|
||||
|
||||
**Breaking Change:** The default `mode` for `Email` constraint changes from `loose` to `html5` in Symfony 7.0.
|
||||
|
||||
**Status:** Already handled - all constraints use explicit `mode: 'strict'` and framework default is set to `html5`.
|
||||
|
||||
### 3. Deprecation Cleanup
|
||||
|
||||
Deprecations accumulated in Symfony 7.1-7.4 will need review:
|
||||
- Check Symfony Profiler after upgrade for any new deprecation warnings
|
||||
- Review [UPGRADE-7.0.md](https://github.com/symfony/symfony/blob/7.0/UPGRADE-7.0.md)
|
||||
- Review [UPGRADE-7.4.md](https://github.com/symfony/symfony/blob/7.4/UPGRADE-7.4.md)
|
||||
|
||||
---
|
||||
|
||||
## Upgrade Procedure
|
||||
|
||||
### Phase 1: Preparation (15 minutes)
|
||||
|
||||
1. **Create Feature Branch**
|
||||
```bash
|
||||
git checkout -b feature/symfony-7.4-upgrade
|
||||
```
|
||||
|
||||
2. **Backup Current State**
|
||||
```bash
|
||||
git tag before-symfony-7.4-upgrade
|
||||
```
|
||||
|
||||
### Phase 2: Dependency Updates (30 minutes - 1 hour)
|
||||
|
||||
1. **Update composer.json**
|
||||
- Update all Symfony package constraints to `7.4.*`
|
||||
- Update `extra.symfony.require` to `"7.4.*"`
|
||||
- Consider tightening `php` to `>=8.2` (Symfony 7.4 requires 8.2+; optional if already on 8.3)
|
||||
|
||||
2. **Run Composer Update**
|
||||
```bash
|
||||
ddev composer update
|
||||
```
|
||||
|
||||
3. **Review Recipe Updates**
|
||||
```bash
|
||||
ddev composer recipes
|
||||
```
|
||||
Review any recipe updates from Symfony Flex and apply as needed.
|
||||
|
||||
4. **Clear All Caches**
|
||||
```bash
|
||||
ddev exec bin/console cache:clear
|
||||
ddev exec bin/console cache:warmup
|
||||
ddev exec "php -r 'opcache_reset();'"
|
||||
```
|
||||
|
||||
### Phase 3: Testing & Validation (1-2 hours)
|
||||
|
||||
1. **Run PHPUnit Test Suite**
|
||||
```bash
|
||||
ddev exec ./vendor/bin/phpunit
|
||||
```
|
||||
Check for:
|
||||
- Test failures
|
||||
- Deprecation warnings
|
||||
- Fatal errors
|
||||
|
||||
2. **Run PHP-CS-Fixer**
|
||||
```bash
|
||||
ddev php vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
```
|
||||
|
||||
3. **Manual Testing - Critical Paths**
|
||||
- [ ] User authentication (BPN API login)
|
||||
- [ ] OAuth2 token generation (`/token` endpoint)
|
||||
- [ ] OAuth2 authorization flow (`/authorize` endpoint)
|
||||
- [ ] API endpoints (all `/api/*` routes)
|
||||
- [ ] Booking creation flow (Steps 1-4)
|
||||
- [ ] Participant form rendering and submission
|
||||
- [ ] Dynamic field updates (HTMX)
|
||||
- [ ] Real-time pricing calculations
|
||||
- [ ] Insurance selection and filtering
|
||||
- [ ] Transportation service selection
|
||||
- [ ] Payment method selection
|
||||
- [ ] Final booking submission to BPN API
|
||||
- [ ] Booking edit functionality
|
||||
- [ ] PDF/XML download functionality
|
||||
- [ ] User registration
|
||||
- [ ] Password reset
|
||||
- [ ] Personal data management
|
||||
- [ ] EasyAdmin CRUD / admin panels
|
||||
|
||||
4. **Check Symfony Profiler**
|
||||
- Review deprecation warnings (should be minimal)
|
||||
- Check performance metrics
|
||||
- Verify no errors in logs
|
||||
|
||||
5. **Test Frontend Assets**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
- Verify Stimulus controllers load correctly
|
||||
- Test HTMX interactions
|
||||
- Verify Toastify notifications
|
||||
- Check TailwindCSS compilation
|
||||
|
||||
6. **Verify ZIP File Functionality**
|
||||
- Test any features using `nelexa/zip` (package last updated 2022)
|
||||
- Verify document generation and downloads work correctly
|
||||
|
||||
### Phase 4: Bug Fixes (0-2 hours)
|
||||
|
||||
Address any issues discovered during testing:
|
||||
|
||||
1. **Review Error Logs**
|
||||
```bash
|
||||
ddev logs
|
||||
```
|
||||
|
||||
2. **Fix Any Remaining Deprecations**
|
||||
- Use Symfony Profiler to identify deprecations
|
||||
- Update code to use modern alternatives
|
||||
|
||||
### Phase 5: Deployment (1 hour)
|
||||
|
||||
1. **Deploy to Staging**
|
||||
```bash
|
||||
ddev exec vendor/bin/dep deploy staging
|
||||
```
|
||||
|
||||
2. **Staging Validation**
|
||||
- Full regression testing
|
||||
- Performance testing
|
||||
|
||||
3. **Production Deployment**
|
||||
- Schedule maintenance window
|
||||
- Deploy during low-traffic period
|
||||
- Monitor error logs closely
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
### Immediate Rollback
|
||||
|
||||
If critical issues are discovered:
|
||||
|
||||
1. **Git Revert**
|
||||
```bash
|
||||
git checkout master
|
||||
git reset --hard before-symfony-7.4-upgrade
|
||||
```
|
||||
|
||||
2. **Restore Dependencies**
|
||||
```bash
|
||||
ddev composer install
|
||||
```
|
||||
|
||||
3. **Clear Caches**
|
||||
```bash
|
||||
ddev exec bin/console cache:clear
|
||||
```
|
||||
|
||||
### Production Rollback
|
||||
|
||||
If issues occur in production:
|
||||
|
||||
```bash
|
||||
ddev exec vendor/bin/dep rollback production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Unit Tests
|
||||
- [ ] All PHPUnit tests pass (433 tests)
|
||||
- [ ] No new deprecation warnings
|
||||
- [ ] Code coverage maintained
|
||||
|
||||
### Integration Tests
|
||||
- [ ] BusProNet API integration
|
||||
- [ ] XML parsing and data processing
|
||||
- [ ] Booking price calculations
|
||||
- [ ] Insurance assignment logic
|
||||
- [ ] Field handler system
|
||||
|
||||
### Functional Tests
|
||||
- [ ] User authentication flow
|
||||
- [ ] OAuth2 token generation
|
||||
- [ ] API endpoints respond correctly
|
||||
- [ ] Booking creation (all steps)
|
||||
- [ ] Form validation
|
||||
- [ ] HTMX partial updates
|
||||
- [ ] Dynamic field state changes
|
||||
- [ ] Real-time pricing updates
|
||||
- [ ] Toast notifications
|
||||
|
||||
### Browser Testing
|
||||
- [ ] Chrome (latest)
|
||||
- [ ] Firefox (latest)
|
||||
- [ ] Safari (latest)
|
||||
- [ ] Mobile browsers
|
||||
|
||||
---
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
| Phase | Estimated Time | Notes |
|
||||
|-------|---------------|-------|
|
||||
| **Phase 1:** Preparation | 15 min | Branch creation |
|
||||
| **Phase 2:** Dependency Updates | 30 min - 1 hour | composer.json updates, composer update |
|
||||
| **Phase 3:** Testing & Validation | 1-2 hours | Automated + manual testing |
|
||||
| **Phase 4:** Bug Fixes | 0-2 hours | Address any issues |
|
||||
| **Phase 5:** Deployment | 1 hour | Staging deployment |
|
||||
| **Total** | **2.5-6 hours** | Average: **3.5 hours** |
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations: Symfony 8.0
|
||||
|
||||
Symfony 8.0 was released alongside 7.4 (November 2025) with identical features but:
|
||||
- Requires PHP 8.4
|
||||
- No deprecation layers (all deprecated code removed)
|
||||
- Shorter support window (8 months)
|
||||
|
||||
**Recommendation:** Stay on Symfony 7.4 LTS for stability. Plan PHP 8.4 upgrade separately, then consider Symfony 8.x path.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] All PHPUnit tests pass (433 tests)
|
||||
- [ ] No deprecation warnings in Symfony Profiler
|
||||
- [ ] All critical user flows functional
|
||||
- [ ] OAuth2 authentication working
|
||||
- [ ] BPN API integration operational
|
||||
- [ ] Booking creation flow complete
|
||||
- [ ] Frontend assets compile and load
|
||||
- [ ] Performance metrics comparable or better
|
||||
- [ ] No errors in production logs after 24 hours
|
||||
|
||||
---
|
||||
|
||||
## Additional Resources
|
||||
|
||||
### Official Symfony Documentation
|
||||
- [Upgrade Major Version Guide](https://symfony.com/doc/current/setup/upgrade_major.html)
|
||||
- [UPGRADE-7.0.md](https://github.com/symfony/symfony/blob/7.0/UPGRADE-7.0.md)
|
||||
- [UPGRADE-7.4.md](https://github.com/symfony/symfony/blob/7.4/UPGRADE-7.4.md)
|
||||
- [Symfony 7.4 Release Notes](https://symfony.com/releases/7.4)
|
||||
- [Preparing for Symfony 7.4 and 8.0](https://symfony.com/blog/preparing-for-symfony-7-4-and-symfony-8-0)
|
||||
|
||||
### Bundle-Specific Documentation
|
||||
- [OAuth2 Server Bundle](https://github.com/thephpleague/oauth2-server-bundle)
|
||||
- [Doctrine ORM 3.x Documentation](https://www.doctrine-project.org/projects/orm.html)
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 7.0
|
||||
**Previous Version:** 6.0 (dated 2026-01-09)
|
||||
**Last Updated:** 2026-02-23
|
||||
**Status:** Ready for Symfony 7.4 Upgrade (updated for current project state)
|
||||
@@ -1,196 +0,0 @@
|
||||
# Booking Flow Reference
|
||||
|
||||
Last updated: 2026-04-16
|
||||
|
||||
This document traces the booking create and edit flows from controller entry point through to session/DTO state. It is intended as orientation for developers unfamiliar with the flow, not as a substitute for reading the code.
|
||||
|
||||
---
|
||||
|
||||
## Shared Concepts
|
||||
|
||||
### `BookingDto`
|
||||
|
||||
The central data object for both flows. Holds room selections, participants, payment method, and travel reference. Lives in the HTTP session between requests. Has two modes: `MODE_CREATE` and `MODE_EDIT`.
|
||||
|
||||
### `BookingSessionManager`
|
||||
|
||||
Owns all reads and writes to session-stored `BookingDto` instances. Also manages the baseline room snapshot used for change detection in the create flow.
|
||||
|
||||
### `BookingCreateContextFactory`
|
||||
|
||||
Assembles the view model (`BookingCreateContext`) passed to create-flow templates. Handles room grouping (by-pax vs. by-room) and wires summary sidebar data.
|
||||
|
||||
---
|
||||
|
||||
## Create Flow
|
||||
|
||||
```
|
||||
Step 1 (room selection)
|
||||
→ Step 2 (participants)
|
||||
→ Step 2 sub-form (per-participant, HTMX)
|
||||
→ Step 3 (payment + API pre-validation)
|
||||
→ Step 4 (confirmation + final submission)
|
||||
→ Success
|
||||
```
|
||||
|
||||
### Entry / Bootstrap
|
||||
|
||||
`BookingExceptionHandlerTrait::getOrCreateBookingCreateDto()` is called by every step controller. If no DTO exists in session, it calls `BookingConfigurator::startFreshBooking()` which loads travel data and initialises a fresh `BookingDto`.
|
||||
|
||||
### Step 1 — Room Selection
|
||||
|
||||
**Controller:** `Create/Step1Controller`
|
||||
**Route:** `GET|POST /bookings/create/rooms`
|
||||
|
||||
1. Load or create DTO via `getOrCreateBookingCreateDto()`
|
||||
2. `BookingSessionManager::getOrCreateBaselineSnapshot()` — saves the current room selection as a comparison point for change detection
|
||||
3. Render `BookingCreateStep1Type` form
|
||||
4. On submit:
|
||||
- If room selection changed from baseline: `BookingDto::resetParticipantAssignments()` — clears stale assignments
|
||||
- `BookingConfigurator::updateBookingStatusFromRoomSelection()` — may switch booking to inquiry mode if capacity constraints require it
|
||||
- `BookingSessionManager::saveBookingDto()`, advance `currentStep` to 2
|
||||
- `BookingSessionManager::clearBaselineSnapshot()`
|
||||
- Redirect to Step 2
|
||||
|
||||
HTMX refresh (`/bookings/create/refresh`): processes the form without validation and returns updated room selection + summary sidebar blocks.
|
||||
|
||||
### Step 2 — Participants (Card Overview)
|
||||
|
||||
**Controller:** `Create/Step2Controller`
|
||||
**Route:** `GET|POST /bookings/create/participants`
|
||||
|
||||
1. Load DTO from session (or redirect to Step 1 if missing)
|
||||
2. `TravelDataProvider::enrichWithFreshAvailabilities()` — refreshes room availability
|
||||
3. `ensureCorrectNumberOfParticipants()` — syncs the `BookingDto::participants` array length with the room selections (e.g., 2× double room → 4 participants). Preserves existing participant data for slots that still exist.
|
||||
4. `ParticipantDataPrefiller::shouldPrepopulateApplicant()` + `prepopulateApplicantFromUser()` — prefills participant[0] from user profile on first visit
|
||||
5. `RoomAssigner::validateAndResetInvalidAssignments()` — clears any room assignments that no longer match the current room selection (handles back-navigation from Step 2 → Step 1 → Step 2)
|
||||
6. `RoomAssigner::assignRoomsIfNeeded()` — auto-assigns participants to rooms
|
||||
7. `BookingConfigurator::preselectDefaultServices()` — selects mandatory and auto-book services for all participants
|
||||
8. `BookingConfigurator::applyCreateBookingStatusRules()` — re-evaluates booking status (book / inquiry / not-bookable)
|
||||
9. `BookingSessionManager::saveBookingDto()`
|
||||
10. On form submit (all participants valid): advance to Step 3, redirect
|
||||
|
||||
### Step 2 — Individual Participant Form (HTMX)
|
||||
|
||||
**Controller:** `Create/Step2ParticipantController`
|
||||
**Routes:** `GET|POST /bookings/create/participants/{index}` and `/participants/{index}/refresh`
|
||||
|
||||
Each participant card lazy-loads its form via HTMX. The controller:
|
||||
|
||||
1. Verifies the participant slot exists
|
||||
2. Refreshes availability
|
||||
3. Handles dummy data fill (`ParticipantDataPrefiller::isDummyDataFillRequested()` / `fillDummyParticipant()`)
|
||||
4. On submit: re-applies status rules, saves to session, redirects to card overview
|
||||
5. `ParticipantFormSupport` is used for form creation options and notification draining
|
||||
|
||||
### Step 3 — Payment + API Pre-Validation
|
||||
|
||||
**Controller:** `Create/Step3Controller`
|
||||
**Route:** `GET|POST /bookings/create/payment`
|
||||
|
||||
1. Load DTO from session
|
||||
2. `BookingConfigurator::applyCreateBookingStatusRules()`
|
||||
3. On submit:
|
||||
- `ApiClient::createBookingInquiry()` — submits booking to API for validation without committing it
|
||||
- If inquiry fails with "nicht möglich" + "Anfrage" in message: auto-switch to inquiry mode, advance to Step 4
|
||||
- If inquiry fails otherwise: show error, stay on step
|
||||
- Price verification: `BookingPriceCalculator::calculateGrandTotal()` is compared against the API response total (accounting for promotional/goodwill voucher discounts and API-applied group discounts). Mismatch triggers `BookingPriceMismatchAnalyzer` for detailed logging.
|
||||
- On success: advance `currentStep` to 4, redirect
|
||||
|
||||
### Step 4 — Confirmation + Final Submission
|
||||
|
||||
**Controller:** `Create/Step4Controller`
|
||||
**Route:** `GET|POST /bookings/create/confirmation`
|
||||
|
||||
1. Load DTO from session
|
||||
2. Newsletter subscription check for opt-in display
|
||||
3. On submit:
|
||||
- `BookingConfigurator::applyCreateBookingStatusRules()`
|
||||
- `ApiClient::createBooking()` — final booking submission (already validated in Step 3)
|
||||
- Newsletter double-opt-in if selected
|
||||
- On success: store booking number + total in flash (for analytics), clear travel data cache, `BookingSessionManager::clearBookingDto()`, redirect to success page
|
||||
|
||||
---
|
||||
|
||||
## Edit Flow
|
||||
|
||||
```
|
||||
Entry (start)
|
||||
→ Load (session or API + draft restore)
|
||||
→ Participant sub-forms (HTMX, per-participant)
|
||||
→ Submission
|
||||
```
|
||||
|
||||
### Entry
|
||||
|
||||
**Controller:** `Edit/IndexController::start()`
|
||||
**Route:** `GET /bookings/{id}/edit/start`
|
||||
|
||||
Clears any existing session DTO for edit mode and invalidates the API booking cache, then redirects to the edit page. This ensures a clean load from API on every intentional re-entry.
|
||||
|
||||
### Load
|
||||
|
||||
**Controller:** `Edit/IndexController::index()`
|
||||
**Route:** `GET|POST /bookings/{id}/edit`
|
||||
|
||||
Loading is delegated entirely to `BookingEditDataLoader::loadFormData()`:
|
||||
|
||||
**If session DTO exists and matches the requested booking ID:**
|
||||
- `TravelDataProvider::enrichWithFreshAvailabilities()` — refreshes room availability
|
||||
- `TravelDataProvider::getMutabilityData()` + `patchMutability()` — applies cached mutability state to DTO
|
||||
|
||||
**If no session DTO (first load or after `start`):**
|
||||
|
||||
`initializeFromApi()`:
|
||||
1. `fetchBookingData()` — `ApiClient::getBooking()` with 5-minute cache (tagged by user ID for bulk invalidation)
|
||||
2. `TravelDataProvider::getTravelData()` — loads travel data (XML or snapshot)
|
||||
3. `TravelDataProvider::getAvailabilityData(forceRefresh: true)` + `patchAvailabilities()` — force-refreshes availability at edit start, populates cache for subsequent loads within the session
|
||||
4. `TravelDataProvider::getMutabilityData(cached: true)` + `patchMutability()` — applies which service categories are currently editable
|
||||
5. `BookingDataProcessor::createBookingDtoFromBooking()` — hydrates `BookingDto` from API booking entity
|
||||
6. Agency code resolved via `AgencyLoader` (used by field state conditions)
|
||||
7. `BookingChangeTracker::generateFingerprint()` — sets `originalFingerprint` on DTO **before** draft is applied, so dirty detection compares against original API data
|
||||
8. `BookingEditDraftManager::findDraft()` + `applyDraftToDto()` — if draft exists, merges user edits on top of fresh API data. Sets `draftWasRestored = true` if successful.
|
||||
9. `BookingSessionManager::saveBookingDto()`
|
||||
|
||||
Back in the controller: shows "draft restored" flash if applicable, fetches booking data for surcharge/status display, renders the card overview with `BookingEditContextFactory::createOverviewContext()`.
|
||||
|
||||
### Individual Participant Form (HTMX)
|
||||
|
||||
**Controller:** `Edit/ParticipantController`
|
||||
**Routes:** `GET|POST /bookings/{id}/edit/participants/{index}` and `/{index}/refresh`
|
||||
|
||||
Per-participant forms are lazy-loaded, matching the create flow pattern.
|
||||
|
||||
1. `BookingEditContextFactory::prepareBookingDto()` — refreshes availabilities on the DTO travel object
|
||||
2. `ParticipantFormSupport` — form creation, notification draining
|
||||
3. On submit: `BookingSessionManager::saveBookingDto()` + `BookingEditDraftManager::saveDraft()` — persists participant edits to both session and draft
|
||||
|
||||
### Submission
|
||||
|
||||
**Service:** `BookingEditSubmitter::handleSubmission()`
|
||||
Called from `Edit/IndexController` on valid form submit.
|
||||
|
||||
1. Invalidate API cache, fetch fresh booking data
|
||||
2. Force-refresh mutability data (`TravelDataProvider::getMutabilityData(forceRefresh: true)`) and patch DTO travel
|
||||
3. `BookingEditSubmitGuard::reconcileImmutableCategories()` — reverts any DTO fields that have become immutable since the session was loaded. Shows flash if anything was reverted.
|
||||
4. `ApiClient::updateBooking()` — submit
|
||||
5. On success: invalidate cache again, `BookingSessionManager::clearBookingDto()`, `BookingEditDraftManager::deleteDraft()`
|
||||
|
||||
### Other Edit Routes
|
||||
|
||||
| Route | Action |
|
||||
|-------|--------|
|
||||
| `GET /bookings/{id}/edit/reload` | Shows confirmation modal |
|
||||
| `POST /bookings/{id}/edit/reload` | Clears session + **deletes draft**, reloads from API |
|
||||
| `GET /bookings/{id}/edit/cancel` | Clears session, **preserves draft**, redirects to booking list |
|
||||
|
||||
The distinction between reload and cancel is intentional: cancel keeps the draft so the user can return to their changes later; reload discards everything.
|
||||
|
||||
---
|
||||
|
||||
## Key Invariants
|
||||
|
||||
- `currentStep` on the DTO gates forward navigation in create mode; controllers redirect to the current step if a higher step is requested directly.
|
||||
- The original fingerprint is set before draft application in edit mode. Dirty detection (`BookingChangeTracker::isDirty()`) always compares the live session DTO against the original API state, not against the draft.
|
||||
- Mutability is always patched from fresh or cached data before rendering. The DTO travel object is the authoritative source of which fields are editable at render time.
|
||||
- Drafts survive cancel but not reload, and are deleted on successful submission.
|
||||
@@ -1,122 +0,0 @@
|
||||
# Service Simplification Plan
|
||||
|
||||
Status: active
|
||||
Last updated: 2026-04-13
|
||||
|
||||
## Purpose
|
||||
|
||||
This document tracks the next pass of service simplification work. The goal is to reduce orchestration density, make the booking flow easier to follow for a human reader, and keep responsibilities aligned with the actual boundaries in the code.
|
||||
|
||||
The emphasis is not on deleting services for its own sake. The emphasis is on:
|
||||
- keeping one clear owner for each meaningful boundary
|
||||
- removing thin wrappers and pass-through helpers
|
||||
- avoiding services that mostly shuffle data between layers
|
||||
- keeping presentation concerns out of calculation and orchestration code
|
||||
|
||||
## Current Read
|
||||
|
||||
The codebase is already in a better place than it was at the start of the refactor, but a few services still carry more than one responsibility:
|
||||
|
||||
- `BookingService` now mostly covers booking bootstrap, service preselection, and booking status rules.
|
||||
- `BookingParticipantCountService` was removed — its logic lives as a private method in the one controller that needed it (`Step2Controller`).
|
||||
- `BookingRoomSelectionService` was removed — its logic lives as a private method in `BookingCreateContextFactory`.
|
||||
- `BookingSummaryParticipantCountService` was removed — its logic lives as a private method in `BookingSummaryDataService`.
|
||||
- `ParticipantPrepopulationService` now owns applicant prefill plus the create-mode dummy-data shortcut.
|
||||
- `BookingEditParticipantContextFactory` now prepares the edit participant page context directly, replacing the older pass-through participant form service.
|
||||
- `BookingPriceCalculatorService` is focused on pricing, but it still sits close to display-oriented behavior in adjacent code paths.
|
||||
- `TravelDataService` remains broad and is likely the next larger boundary after booking orchestration is reduced.
|
||||
|
||||
One registry stands out as a real orchestration boundary and should be left alone for now:
|
||||
- `ParticipantFieldHandlerRegistry`
|
||||
|
||||
It is not just a lookup table. It owns execution order, edit-mode mutability gating, and synchronization of submitted form data back into the DTO state.
|
||||
|
||||
## Next Pass
|
||||
|
||||
### 1. Keep pricing calculation focused
|
||||
|
||||
Primary goal: keep pricing code about pricing, not rendering.
|
||||
|
||||
Concrete next steps:
|
||||
- keep `BookingPriceCalculatorService` as the pricing boundary
|
||||
- continue removing display formatting from pricing code paths
|
||||
- keep any remaining view-specific formatting in the presentation layer or a dedicated UI helper
|
||||
- avoid introducing another service that only formats values already known to the view
|
||||
|
||||
Decision rule:
|
||||
- if a value is only needed for display, prefer exposing the raw numeric/domain value and formatting it as close to the UI as possible
|
||||
|
||||
### 2. Leave the field-handler registry in place
|
||||
|
||||
Primary goal: avoid unnecessary churn in a class that is already a meaningful orchestration layer.
|
||||
|
||||
Concrete next steps:
|
||||
- do not refactor `ParticipantFieldHandlerRegistry` in this pass
|
||||
- revisit only if a later change can split ordering, mutability, and synchronization into clear collaborators without making the flow harder to trace
|
||||
|
||||
Decision rule:
|
||||
- if a registry owns actual workflow behavior, treat it as a boundary rather than a smell
|
||||
|
||||
## Follow-Up Queue
|
||||
|
||||
After the booking service pass, the next likely candidates are:
|
||||
|
||||
### `TravelDataService`
|
||||
|
||||
This is the largest broad service still in the codebase. It likely needs a later pass if the application should become easier to follow end to end.
|
||||
|
||||
Likely directions:
|
||||
- separate runtime travel loading from cache/snapshot maintenance if the public API still feels too wide
|
||||
- keep the read path explicit and avoid hiding maintenance work behind one large method surface
|
||||
|
||||
### `BookingPriceCalculatorService`
|
||||
|
||||
This service should stay focused on pricing logic, but it may still have room for further internal cleanup if more display or transport aggregation concerns surface.
|
||||
|
||||
Likely directions:
|
||||
- keep calculation responsibilities together
|
||||
- avoid dragging presentation behavior back into the calculator
|
||||
- split only if a sub-boundary becomes obvious and reusable
|
||||
|
||||
### `ParticipantFieldHandlerRegistry`
|
||||
|
||||
This stays on the list only as a future optional refactor, not as an immediate target.
|
||||
|
||||
Likely directions, only if justified later:
|
||||
- isolate ordering/sorting logic if it becomes independently meaningful
|
||||
- split synchronization code if a clearer DTO/form boundary emerges
|
||||
- otherwise leave it as the central orchestration point for participant field processing
|
||||
|
||||
## Current Notes
|
||||
|
||||
- `templates/booking/_summary_travel_info.html.twig` already renders the summary participant count directly, so the remaining work here is naming and contract clarity rather than Twig branching.
|
||||
- The summary count still needs a clear name if the code should distinguish the display-oriented count from the canonical participant total in the DTO/service layer.
|
||||
- `ParticipantFormSupportService` was reviewed and kept: it is shared between two controllers (`Step2ParticipantController` and `Edit/ParticipantController`) with real shared logic, not just delegation.
|
||||
|
||||
## Progress Tracker
|
||||
|
||||
| Item | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Participant card DTO cleanup | Done | Card data now uses typed DTOs instead of nested array payloads |
|
||||
| Room label formatting cleanup | Done | Pricing labels now have a dedicated presentation helper |
|
||||
| Booking service split | Done | Session lifecycle, hydration, baseline snapshot, return URL handling, room grouping, and participant count shaping moved out of `BookingService` |
|
||||
| Dummy prefill extraction | Done | Moved into `ParticipantPrepopulationService` |
|
||||
| Micro-service consolidation | Done | `BookingRoomSelectionService`, `BookingParticipantCountService`, `BookingSummaryParticipantCountService` inlined into their single callers |
|
||||
| Pricing service review | Pending | Keep focused on calculation, not rendering |
|
||||
| Travel data service review | Pending | Broad boundary, likely later pass |
|
||||
| Participant field registry review | Deferred | Real orchestration boundary, intentionally left alone for now |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
The next booking-service pass is only worth keeping if it:
|
||||
- reduces the number of unrelated responsibilities in `BookingService`
|
||||
- makes the booking flow easier to trace from controller to session/DTO state
|
||||
- preserves existing booking behavior and test coverage
|
||||
- does not replace one large service with several generic "manager" classes
|
||||
|
||||
## Working Agreement
|
||||
|
||||
- Update this document as decisions are made.
|
||||
- Record rejected simplification ideas here with a short reason.
|
||||
- Keep the plan aligned with actual code, not with an abstract architecture ideal.
|
||||
- If a future simplification does not clearly reduce cognitive load, do not add it.
|
||||
@@ -886,9 +886,10 @@ Keys stored in `config/secret/` directory.
|
||||
|
||||
**Location:** `src/Security/Voter/BookingVoter.php`
|
||||
|
||||
This voter currently exists as legacy authorization logic and is not actively invoked by booking controllers.
|
||||
|
||||
Current booking routes (`/bookings/*`) are guarded with `ROLE_USER`, while final document access checks are enforced by BusProNet.
|
||||
| Operation | Requirements |
|
||||
|-----------|--------------|
|
||||
| `VIEW` | User owns booking (addressId match) |
|
||||
| `EDIT` | User owns booking AND booking is editable |
|
||||
|
||||
### 7.4 OAuth2
|
||||
|
||||
@@ -1153,19 +1154,6 @@ php bin/console app:cleanup:xml-dumps
|
||||
|
||||
Removes XML debug dumps older than 3 days.
|
||||
|
||||
#### app:cleanup:newsletter-opt-in-requests
|
||||
|
||||
```bash
|
||||
php bin/console app:cleanup:newsletter-opt-in-requests
|
||||
```
|
||||
|
||||
Removes expired pending newsletter double opt-in requests.
|
||||
|
||||
- Request TTL is controlled by `NEWSLETTER_CONFIRMATION_TTL_HOURS` (default: 1 hour).
|
||||
- Expired pending requests are removed immediately when they are encountered (new request / confirmation attempt).
|
||||
- Scheduled cleanup removes all currently expired pending requests (`expires_at <= now`) as a safety net.
|
||||
- Confirmed requests are retained for audit/legal traceability.
|
||||
|
||||
### 10.3 Setup Commands
|
||||
|
||||
#### app:crypto:generate-keys
|
||||
|
||||
+6
-41
@@ -34,41 +34,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"staging": {
|
||||
"Security": {
|
||||
"Auth": {
|
||||
"oauth2_email": {
|
||||
"Type": "OAuth2",
|
||||
"Grant Type": "Authorization Code",
|
||||
"Client ID": "{{oauth2_client_id}}",
|
||||
"Client Secret": "{{oauth2_client_secret}}",
|
||||
"Auth URL": "https://my.ep-reisen.net/authorize",
|
||||
"Token URL": "https://my.ep-reisen.net/token",
|
||||
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
||||
"Scope": "email"
|
||||
},
|
||||
"oauth2_profile": {
|
||||
"Type": "OAuth2",
|
||||
"Grant Type": "Authorization Code",
|
||||
"Client ID": "{{oauth2_client_id}}",
|
||||
"Client Secret": "{{oauth2_client_secret}}",
|
||||
"Auth URL": "https://my.ep-reisen.net/authorize",
|
||||
"Token URL": "https://my.ep-reisen.net/token",
|
||||
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
||||
"Scope": "email profile"
|
||||
},
|
||||
"oauth2_api": {
|
||||
"Type": "OAuth2",
|
||||
"Grant Type": "Client Credentials",
|
||||
"Client ID": "{{oauth2_api_client_id}}",
|
||||
"Client Secret": "{{oauth2_api_client_secret}}",
|
||||
"Auth URL": "https://my.ep-reisen.net/authorize",
|
||||
"Token URL": "https://my.ep-reisen.net/token",
|
||||
"Scope": "api"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"prod": {
|
||||
"Security": {
|
||||
"Auth": {
|
||||
@@ -77,8 +42,8 @@
|
||||
"Grant Type": "Authorization Code",
|
||||
"Client ID": "{{oauth2_client_id}}",
|
||||
"Client Secret": "{{oauth2_client_secret}}",
|
||||
"Auth URL": "https://my.ep-reisen.de/authorize",
|
||||
"Token URL": "https://my.ep-reisen.de/token",
|
||||
"Auth URL": "https://myep.ep-reisen.de/authorize",
|
||||
"Token URL": "https://myep.ep-reisen.de/token",
|
||||
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
||||
"Scope": "email"
|
||||
},
|
||||
@@ -87,8 +52,8 @@
|
||||
"Grant Type": "Authorization Code",
|
||||
"Client ID": "{{oauth2_client_id}}",
|
||||
"Client Secret": "{{oauth2_client_secret}}",
|
||||
"Auth URL": "https://my.ep-reisen.de/authorize",
|
||||
"Token URL": "https://my.ep-reisen.de/token",
|
||||
"Auth URL": "https://myep.ep-reisen.de/authorize",
|
||||
"Token URL": "https://myep.ep-reisen.de/token",
|
||||
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
||||
"Scope": "email profile"
|
||||
},
|
||||
@@ -97,8 +62,8 @@
|
||||
"Grant Type": "Client Credentials",
|
||||
"Client ID": "{{oauth2_api_client_id}}",
|
||||
"Client Secret": "{{oauth2_api_client_secret}}",
|
||||
"Auth URL": "https://my.ep-reisen.de/authorize",
|
||||
"Token URL": "https://my.ep-reisen.de/token",
|
||||
"Auth URL": "https://myep.ep-reisen.de/authorize",
|
||||
"Token URL": "https://myep.ep-reisen.de/token",
|
||||
"Scope": "api"
|
||||
}
|
||||
}
|
||||
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
###
|
||||
# Mailjet API test requests for IntelliJ HTTP Client
|
||||
#
|
||||
# 1) Fill in variables below (or move them to http-client.private.env.json).
|
||||
# 2) Set `mailjet_api_key` and `mailjet_api_secret` in your env file.
|
||||
# IntelliJ HTTP Client will inject them into the Authorization header below.
|
||||
#
|
||||
# Required variables:
|
||||
# - mailjet_base_url (example: https://api.mailjet.com/v3/REST)
|
||||
# - mailjet_api_key
|
||||
# - mailjet_api_secret
|
||||
# - mailjet_contact_email
|
||||
# - mailjet_list_id
|
||||
#
|
||||
# @no-cookie-jar
|
||||
|
||||
### Mailjet health check (authenticated)
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/apikey
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
|
||||
### Find contact by email
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/contact?Email={{mailjet_contact_email}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
> {%
|
||||
if (response.body && response.body.Data && response.body.Data.length > 0) {
|
||||
client.global.set("contact_id", String(response.body.Data[0].ID));
|
||||
}
|
||||
%}
|
||||
|
||||
|
||||
### Create contact (run if not found)
|
||||
# @no-cookie-jar
|
||||
POST {{mailjet_base_url}}/contact
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
{
|
||||
"Email": "{{mailjet_contact_email}}",
|
||||
"IsExcludedFromCampaigns": false
|
||||
}
|
||||
|
||||
> {%
|
||||
if (response.body && response.body.Data && response.body.Data.length > 0) {
|
||||
client.global.set("contact_id", String(response.body.Data[0].ID));
|
||||
}
|
||||
%}
|
||||
|
||||
|
||||
### Get contact by ID (after find/create)
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/contact/{{contact_id}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
|
||||
### Mark contact as subscribed (opt-in)
|
||||
# @no-cookie-jar
|
||||
PUT {{mailjet_base_url}}/contact/{{contact_id}}
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
{
|
||||
"IsExcludedFromCampaigns": false
|
||||
}
|
||||
|
||||
|
||||
### Mark contact as unsubscribed (opt-out)
|
||||
# @no-cookie-jar
|
||||
PUT {{mailjet_base_url}}/contact/{{contact_id}}
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
{
|
||||
"IsExcludedFromCampaigns": true
|
||||
}
|
||||
|
||||
|
||||
### Get newsletter list by ID
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/contactslist/{{mailjet_list_id}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
|
||||
### List all available contact lists
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/contactslist?Limit=1000
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
|
||||
### Check list recipient status (before actions)
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/listrecipient?ContactsList={{mailjet_list_id}}&Contact={{contact_id}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
> {%
|
||||
if (response.body && response.body.Data && response.body.Data.length > 0) {
|
||||
client.global.set("listrecipient_id", String(response.body.Data[0].ID));
|
||||
}
|
||||
%}
|
||||
|
||||
|
||||
### Subscribe to list (addnoforce)
|
||||
# @no-cookie-jar
|
||||
POST {{mailjet_base_url}}/contactslist/{{mailjet_list_id}}/managecontact
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
{
|
||||
"Action": "addnoforce",
|
||||
"Email": "{{mailjet_contact_email}}"
|
||||
}
|
||||
|
||||
|
||||
### Verify list recipient after addnoforce
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/listrecipient?ContactsList={{mailjet_list_id}}&Contact={{contact_id}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
|
||||
### Unsubscribe from list (unsub)
|
||||
# @no-cookie-jar
|
||||
POST {{mailjet_base_url}}/contactslist/{{mailjet_list_id}}/managecontact
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
{
|
||||
"Action": "unsub",
|
||||
"Email": "{{mailjet_contact_email}}"
|
||||
}
|
||||
|
||||
|
||||
### Verify list recipient after unsub
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/listrecipient?ContactsList={{mailjet_list_id}}&Contact={{contact_id}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
|
||||
### Re-subscribe attempt with addnoforce (expected to keep unsubscribed state)
|
||||
# @no-cookie-jar
|
||||
POST {{mailjet_base_url}}/contactslist/{{mailjet_list_id}}/managecontact
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
{
|
||||
"Action": "addnoforce",
|
||||
"Email": "{{mailjet_contact_email}}"
|
||||
}
|
||||
|
||||
|
||||
### Verify list recipient after addnoforce re-subscribe attempt
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/listrecipient?ContactsList={{mailjet_list_id}}&Contact={{contact_id}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
|
||||
### Re-subscribe with addforce (expected to re-subscribe)
|
||||
# @no-cookie-jar
|
||||
POST {{mailjet_base_url}}/contactslist/{{mailjet_list_id}}/managecontact
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
|
||||
{
|
||||
"Action": "addforce",
|
||||
"Email": "{{mailjet_contact_email}}"
|
||||
}
|
||||
|
||||
|
||||
### Verify list recipient after addforce
|
||||
# @no-cookie-jar
|
||||
GET {{mailjet_base_url}}/listrecipient?ContactsList={{mailjet_list_id}}&Contact={{contact_id}}
|
||||
Accept: application/json
|
||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260318113000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create newsletter_opt_in_confirmation table for double opt-in confirmations';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql("CREATE TABLE newsletter_opt_in_confirmation (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(255) NOT NULL, token_hash VARCHAR(64) NOT NULL, expires_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', confirmed_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', UNIQUE INDEX UNIQ_NEWSLETTER_OPT_IN_TOKEN_HASH (token_hash), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE newsletter_opt_in_confirmation');
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260320071724 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add error_code column and index to log_entry table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE log_entry ADD error_code VARCHAR(12) DEFAULT NULL');
|
||||
$this->addSql('CREATE INDEX IDX_LOG_ENTRY_ERROR_CODE ON log_entry (error_code)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP INDEX IDX_LOG_ENTRY_ERROR_CODE ON log_entry');
|
||||
$this->addSql('ALTER TABLE log_entry DROP error_code');
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260321120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create travel_snapshot table for DB-primary travel persistence';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE travel_snapshot (id INT AUTO_INCREMENT NOT NULL, date_id INT NOT NULL, hotel_id INT NOT NULL, date_code VARCHAR(64) DEFAULT NULL, label VARCHAR(255) DEFAULT NULL, product_id INT DEFAULT NULL, product_code VARCHAR(64) DEFAULT NULL, hotel_code VARCHAR(64) DEFAULT NULL, hotel_label VARCHAR(255) DEFAULT NULL, date_from DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', date_to DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', payload LONGTEXT NOT NULL, payload_hash VARCHAR(64) NOT NULL, captured_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', extended_refreshed_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX uniq_snapshot_date_hotel (date_id, hotel_id), INDEX idx_snapshot_date_code (date_code), INDEX idx_snapshot_product_id (product_id), INDEX idx_snapshot_date_to (date_to), INDEX idx_snapshot_extended_refreshed_at (extended_refreshed_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE travel_snapshot');
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260323182524 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE booking_edit_draft CHANGE form_data form_data JSON NOT NULL');
|
||||
$this->addSql('ALTER TABLE log_entry CHANGE context context JSON DEFAULT NULL, CHANGE extra extra JSON DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE newsletter_opt_in_confirmation RENAME INDEX uniq_newsletter_opt_in_token_hash TO UNIQ_70505214B3BC57DA');
|
||||
$this->addSql('ALTER TABLE user CHANGE roles roles JSON NOT NULL, CHANGE hotel_codes hotel_codes JSON NOT NULL');
|
||||
$this->addSql('DROP INDEX IDX_75EA56E0FB7336F0 ON messenger_messages');
|
||||
$this->addSql('DROP INDEX IDX_75EA56E0E3BD61CE ON messenger_messages');
|
||||
$this->addSql('DROP INDEX IDX_75EA56E016BA31DB ON messenger_messages');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages (queue_name, available_at, delivered_at, id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE newsletter_opt_in_confirmation RENAME INDEX uniq_70505214b3bc57da TO UNIQ_NEWSLETTER_OPT_IN_TOKEN_HASH');
|
||||
$this->addSql('ALTER TABLE user CHANGE roles roles JSON NOT NULL COLLATE `utf8mb4_bin`, CHANGE hotel_codes hotel_codes JSON NOT NULL COLLATE `utf8mb4_bin`');
|
||||
$this->addSql('DROP INDEX IDX_75EA56E0FB7336F0E3BD61CE16BA31DBBF396750 ON messenger_messages');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
|
||||
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
|
||||
$this->addSql('ALTER TABLE log_entry CHANGE context context JSON DEFAULT NULL COLLATE `utf8mb4_bin`, CHANGE extra extra JSON DEFAULT NULL COLLATE `utf8mb4_bin`');
|
||||
$this->addSql('ALTER TABLE booking_edit_draft CHANGE form_data form_data JSON NOT NULL COLLATE `utf8mb4_bin`');
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Changes travel_snapshot.payload from TEXT to LONGTEXT so large
|
||||
* plain JSON snapshot payloads can be stored safely.
|
||||
*/
|
||||
final class Version20260324120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Change travel_snapshot.payload from TEXT to LONGTEXT for plain JSON snapshot payloads';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE travel_snapshot CHANGE payload payload LONGTEXT NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE travel_snapshot CHANGE payload payload TEXT NOT NULL');
|
||||
}
|
||||
}
|
||||
Generated
+881
-428
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -5,7 +5,7 @@
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@hotwired/stimulus": "^3.0.0",
|
||||
"@symfony/stimulus-bridge": "^3.2.0",
|
||||
"@symfony/webpack-encore": "^6.0.0",
|
||||
"@symfony/webpack-encore": "^5.0.0",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"autoprefixer": "^10.4.20",
|
||||
@@ -20,7 +20,7 @@
|
||||
"tippy.js": "^6.3.7",
|
||||
"toastify-js": "^1.12.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-cli": "^5.1.0",
|
||||
"webpack-notifier": "^1.15.0"
|
||||
},
|
||||
"license": "WTFPL",
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
parameters:
|
||||
ignoreErrors: []
|
||||
@@ -1,13 +0,0 @@
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-symfony/extension.neon
|
||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- src
|
||||
tmpDir: var/phpstan
|
||||
symfony:
|
||||
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
|
||||
doctrine:
|
||||
objectManagerLoader: tests/doctrine-object-manager.php
|
||||
@@ -1,13 +0,0 @@
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-symfony/extension.neon
|
||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
||||
- vendor/phpstan/phpstan-strict-rules/rules.neon
|
||||
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- src
|
||||
- tests
|
||||
tmpDir: var/cache/phpstan
|
||||
symfony:
|
||||
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Admin\Field;
|
||||
|
||||
|
||||
@@ -7,13 +7,11 @@ use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
use App\BusProNet\Exception\TimeoutException;
|
||||
use App\BusProNet\Model\Agency;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\BookingResponse;
|
||||
use App\BusProNet\Model\BookingUpdate;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\ExtendedServiceAvailabilityResponse;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\PromoVoucher;
|
||||
@@ -40,7 +38,6 @@ class ApiClient
|
||||
public const TYPE_BASE_DATA_COUNTRIES = 'STAMMLAENDER';
|
||||
public const TYPE_MUTABLE_DATA = 'MOEGLICHEAENDERUNGEN';
|
||||
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
|
||||
public const TYPE_AVAILABILITY_EXTENDED = 'VERFUEGBARKEIT2';
|
||||
public const TYPE_AVAILABILITY_HOTEL = 'VERFUEGBARKEITHOTEL';
|
||||
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
|
||||
public const TYPE_BOOKING = 'BUCHUNG';
|
||||
@@ -50,13 +47,11 @@ class ApiClient
|
||||
public const TYPE_PURCHASE_VOUCHER = 'GUTSCHEINPRUEFUNGEINLOESUNG';
|
||||
public const TYPE_PROMO_VOUCHER = 'AKTIONSGUTSCHEIN';
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $config;
|
||||
private float $operationStartTime;
|
||||
private int $selectedPort;
|
||||
private int $requestCounter = 0;
|
||||
|
||||
/** @param array<string, mixed> $options */
|
||||
public function __construct(
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly ApiResponseParser $responseParser,
|
||||
@@ -320,21 +315,6 @@ class ApiClient
|
||||
return $this->sendRequest(static::TYPE_AVAILABILITY, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getAvailabilitiesExtended(int $dateId): Notification|ExtendedServiceAvailabilityResponse
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_AVAILABILITY_EXTENDED),
|
||||
'satz' => ['@typ' => static::TYPE_AVAILABILITY_EXTENDED],
|
||||
'idreise' => $dateId,
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_AVAILABILITY_EXTENDED, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
@@ -616,10 +596,6 @@ class ApiClient
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $additionalArgs
|
||||
*/
|
||||
private function sendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed
|
||||
{
|
||||
return $this->executeWithRetry(fn () => $this->doSendRequest($type, $data, $additionalArgs, $debug), $type);
|
||||
@@ -628,7 +604,6 @@ class ApiClient
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
/** @param array<string, mixed> $data */
|
||||
private function sendRequestRaw(array $data): string
|
||||
{
|
||||
return $this->executeWithRetry(fn () => $this->doSendRequestRaw($data));
|
||||
@@ -684,10 +659,6 @@ class ApiClient
|
||||
* @throws ApiClientException
|
||||
* @throws ImmediateConnectionCloseException
|
||||
*/
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $additionalArgs
|
||||
*/
|
||||
private function doSendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed
|
||||
{
|
||||
$requestId = $this->getRequestId();
|
||||
@@ -745,8 +716,6 @@ class ApiClient
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @throws ApiClientException
|
||||
* @throws ImmediateConnectionCloseException
|
||||
*/
|
||||
@@ -808,8 +777,6 @@ class ApiClient
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource
|
||||
*
|
||||
* @throws ApiClientException
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
@@ -873,8 +840,6 @@ class ApiClient
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $socket
|
||||
*
|
||||
* @throws TimeoutException
|
||||
*/
|
||||
private function send($socket, string $data): void
|
||||
@@ -893,8 +858,6 @@ class ApiClient
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $socket
|
||||
*
|
||||
* @throws TimeoutException
|
||||
* @throws ImmediateConnectionCloseException
|
||||
*/
|
||||
@@ -957,17 +920,11 @@ class ApiClient
|
||||
return $response;
|
||||
}
|
||||
|
||||
/** @param resource $socket */
|
||||
private function disconnect($socket): void
|
||||
{
|
||||
@fclose($socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function resolveOptions(array $options): array
|
||||
{
|
||||
$optionsResolver = new OptionsResolver();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet;
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ use App\Form\Model\BankAccountDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use App\Service\BookingPriceCalculator;
|
||||
use App\Service\InsuranceManager;
|
||||
use App\Service\BookingPriceCalculatorService;
|
||||
use App\Service\InsuranceService;
|
||||
|
||||
/**
|
||||
* Processes booking form data and converts it into BusProNet API payload format.
|
||||
@@ -25,8 +25,8 @@ use App\Service\InsuranceManager;
|
||||
class BookingDataProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InsuranceManager $insuranceService,
|
||||
private readonly BookingPriceCalculator $priceCalculatorService,
|
||||
private readonly InsuranceService $insuranceService,
|
||||
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||
private readonly ServiceMappingCollector $mappingCollector,
|
||||
private readonly ParticipantServiceProcessor $serviceProcessor,
|
||||
private readonly BookingPayloadBuilder $payloadBuilder,
|
||||
@@ -73,12 +73,14 @@ class BookingDataProcessor
|
||||
if (0 === $index) {
|
||||
// Copy address if first participant has no street (indicating empty/incomplete address)
|
||||
// This allows applicant and first participant to be different people with different addresses
|
||||
$isEmpty = null === $participantData->address
|
||||
|| null === $participantData->address->street
|
||||
|| '' === trim($participantData->address->street);
|
||||
if (null !== $booking->applicant->address) {
|
||||
$isEmpty = null === $participantData->address
|
||||
|| null === $participantData->address->street
|
||||
|| '' === trim($participantData->address->street);
|
||||
|
||||
if ($isEmpty) {
|
||||
$participantData->address = clone $booking->applicant->address;
|
||||
if ($isEmpty) {
|
||||
$participantData->address = clone $booking->applicant->address;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy body dimensions from applicant if not present in participant
|
||||
@@ -128,7 +130,7 @@ class BookingDataProcessor
|
||||
|
||||
// Parking service (for self-organized PKW transportation)
|
||||
$parkingServices = $booking->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_PARKING);
|
||||
if ([] !== $parkingServices) {
|
||||
if (false === empty($parkingServices)) {
|
||||
$participantData->parking = true;
|
||||
$participantData->parkingService = reset($parkingServices);
|
||||
} else {
|
||||
@@ -282,7 +284,7 @@ class BookingDataProcessor
|
||||
*
|
||||
* @param BookingDto|null $formData The booking edit form data containing updated participant and service selections
|
||||
*
|
||||
* @return array<string, mixed> The structured payload array ready for BusProNet API submission
|
||||
* @return array The structured payload array ready for BusProNet API submission
|
||||
*/
|
||||
public function createUpdateRequestPayload(?BookingDto $formData): array
|
||||
{
|
||||
@@ -320,7 +322,7 @@ class BookingDataProcessor
|
||||
|
||||
// Add purchase vouchers if any exist
|
||||
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($formData);
|
||||
if ([] !== $purchaseVouchers) {
|
||||
if (false === empty($purchaseVouchers)) {
|
||||
$payload['gutscheine']['gutschein'] = [];
|
||||
foreach ($purchaseVouchers as $code) {
|
||||
$payload['gutscheine']['gutschein'][] = [
|
||||
@@ -343,7 +345,7 @@ class BookingDataProcessor
|
||||
* @param BookingDto $bookingDto The booking creation form data
|
||||
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
|
||||
*
|
||||
* @return array<string, mixed> The structured payload array for BusProNet API submission
|
||||
* @return array The structured payload array for BusProNet API submission
|
||||
*/
|
||||
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
|
||||
{
|
||||
@@ -386,7 +388,7 @@ class BookingDataProcessor
|
||||
$participantPrices[$index] = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index);
|
||||
}
|
||||
|
||||
// Use InsuranceManager for proper type-based assignment with price tier matching
|
||||
// Use InsuranceService for proper type-based assignment with price tier matching
|
||||
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
|
||||
$selectableInsurances,
|
||||
$applicant->insurance,
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace App\BusProNet\DataProcessor;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Service\ParticipantStatusRuleRegistry;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
/**
|
||||
* Builds API payload structures for BusProNet booking requests.
|
||||
@@ -17,10 +17,9 @@ use App\Form\Model\ParticipantDto;
|
||||
*/
|
||||
class BookingPayloadBuilder
|
||||
{
|
||||
private const DEFAULT_PARTICIPANT_STATUS = 'F';
|
||||
|
||||
public function __construct(
|
||||
private readonly ServiceMappingCollector $mappingCollector,
|
||||
private readonly ParticipantStatusRuleRegistry $statusRuleRegistry,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -31,7 +30,7 @@ class BookingPayloadBuilder
|
||||
*
|
||||
* @param Booking $bookingData The booking data object
|
||||
*
|
||||
* @return array<string, mixed> The base payload structure
|
||||
* @return array The base payload structure
|
||||
*/
|
||||
public function buildBasePayload(Booking $bookingData): array
|
||||
{
|
||||
@@ -67,8 +66,8 @@ class BookingPayloadBuilder
|
||||
*
|
||||
* Bank account information is required for direct debit payments.
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
* @param array $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
*/
|
||||
public function addBankAccountToPayload(array &$payload, Booking $bookingData): void
|
||||
{
|
||||
@@ -87,9 +86,9 @@ class BookingPayloadBuilder
|
||||
*
|
||||
* Includes status, personal data, and wishes (room remarks, license plate) for each participant.
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
* @param array<int, ParticipantDto> $participantDtos The participant DTOs from the form (for wishes data)
|
||||
* @param array $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
* @param array $participantDtos The participant DTOs from the form (for wishes data)
|
||||
*/
|
||||
public function buildParticipantPayload(array &$payload, Booking $bookingData, array $participantDtos): void
|
||||
{
|
||||
@@ -142,8 +141,8 @@ class BookingPayloadBuilder
|
||||
* Includes additional services, transportation services, and accommodation details
|
||||
* with participant mappings and quantities.
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
* @param array $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
*/
|
||||
public function buildServicesPayload(array &$payload, Booking $bookingData): void
|
||||
{
|
||||
@@ -199,8 +198,8 @@ class BookingPayloadBuilder
|
||||
*
|
||||
* Only included in payload if there are actual pickup assignments for bus transportation.
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
* @param array $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
*/
|
||||
public function buildPickupPayload(array &$payload, Booking $bookingData): void
|
||||
{
|
||||
@@ -224,8 +223,8 @@ class BookingPayloadBuilder
|
||||
*
|
||||
* Only included in payload if there are actual drop-off assignments for bus transportation.
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
* @param array $payload The payload array to modify
|
||||
* @param Booking $bookingData The booking data object
|
||||
*/
|
||||
public function buildDropOffPayload(array &$payload, Booking $bookingData): void
|
||||
{
|
||||
@@ -250,7 +249,7 @@ class BookingPayloadBuilder
|
||||
* @param BookingDto $bookingDto The booking creation form data
|
||||
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
|
||||
*
|
||||
* @return array<string, mixed> The structured payload array for BusProNet API submission
|
||||
* @return array The structured payload array for BusProNet API submission
|
||||
*/
|
||||
public function buildCreatePayload(BookingDto $bookingDto, string $bookingType): array
|
||||
{
|
||||
@@ -303,7 +302,7 @@ class BookingPayloadBuilder
|
||||
foreach ($bookingDto->participants as $index => $participant) {
|
||||
$participantData = [
|
||||
'@id' => $index + 1,
|
||||
'status' => self::DEFAULT_PARTICIPANT_STATUS,
|
||||
'status' => $this->statusRuleRegistry->evaluateStatus($participant),
|
||||
'name' => $participant->lastName,
|
||||
'vorname' => $participant->firstName,
|
||||
'geschlecht' => $participant->gender ?? '',
|
||||
@@ -440,11 +439,11 @@ class BookingPayloadBuilder
|
||||
* Generic helper method that converts service ID => participant IDs mappings
|
||||
* into XML payload structure.
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param string $sectionKey The section key (e.g., 'beförderungen', 'versicherungen')
|
||||
* @param string $itemKey The item key (e.g., 'beförderung', 'versicherung')
|
||||
* @param string $idAttributeName The ID attribute name (e.g., '@idleistung', '@idversicherung')
|
||||
* @param array<int|string, int[]> $serviceMap Map of service ID to participant IDs
|
||||
* @param array $payload The payload array to modify
|
||||
* @param string $sectionKey The section key (e.g., 'beförderungen', 'versicherungen')
|
||||
* @param string $itemKey The item key (e.g., 'beförderung', 'versicherung')
|
||||
* @param string $idAttributeName The ID attribute name (e.g., '@idleistung', '@idversicherung')
|
||||
* @param array $serviceMap Map of service ID to participant IDs
|
||||
*/
|
||||
public function addServicesFromMap(
|
||||
array &$payload,
|
||||
@@ -476,9 +475,9 @@ class BookingPayloadBuilder
|
||||
* - abreise (departure date)
|
||||
* - anzahl (number of rooms of this type booked)
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param array<int|string, int[]> $roomMap Map of room ID to participant IDs
|
||||
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
|
||||
* @param array $payload The payload array to modify
|
||||
* @param array $roomMap Map of room ID to participant IDs
|
||||
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
|
||||
*/
|
||||
public function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingDto $bookingDto): void
|
||||
{
|
||||
@@ -521,8 +520,8 @@ class BookingPayloadBuilder
|
||||
/**
|
||||
* Adds purchase vouchers to the payload.
|
||||
*
|
||||
* @param array<string, mixed> $payload The payload array to modify
|
||||
* @param BookingDto $bookingDto The booking data
|
||||
* @param array $payload The payload array to modify
|
||||
* @param BookingDto $bookingDto The booking data
|
||||
*/
|
||||
public function addPurchaseVouchersToPayload(array &$payload, BookingDto $bookingDto): void
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\BusProNet\DataProcessor;
|
||||
|
||||
use App\BusProNet\Model\Address;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
@@ -85,6 +86,9 @@ class PersonalDataSynchronizer
|
||||
}
|
||||
|
||||
if ($participant->email || $participant->mobile) {
|
||||
if (null === $bookingData->participants[$participant->index]->communication) {
|
||||
$bookingData->participants[$participant->index]->communication = new Communication();
|
||||
}
|
||||
$bookingData->participants[$participant->index]->communication->email = $participant->email;
|
||||
$bookingData->participants[$participant->index]->communication->mobile = $participant->mobile;
|
||||
}
|
||||
@@ -123,6 +127,10 @@ class PersonalDataSynchronizer
|
||||
$applicant->dateOfBirth = new \DateTimeImmutable('-20 years');
|
||||
}
|
||||
|
||||
if (null === $applicant->communication) {
|
||||
$applicant->communication = new Communication();
|
||||
}
|
||||
|
||||
if (null === $applicant->communication->mobile || '' === $applicant->communication->mobile) {
|
||||
$applicant->communication->mobile = '12345';
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ServiceMappingCollector
|
||||
*
|
||||
* Groups participants by their assigned room ID.
|
||||
*
|
||||
* @return array<int, list<int>> Map of room ID to participant IDs
|
||||
* @return array<string, array<int>> Map of room ID to participant IDs
|
||||
*/
|
||||
public function collectRoomMappings(BookingDto $bookingDto): array
|
||||
{
|
||||
@@ -43,7 +43,7 @@ class ServiceMappingCollector
|
||||
* Groups board, ski passes, rentals, rental insurance, courses, parking, and additional services
|
||||
* by service ID with their participant assignments (1-based).
|
||||
*
|
||||
* @return array<int, list<int>> Map of service ID to participant IDs
|
||||
* @return array<string, array<int>> Map of service ID to participant IDs
|
||||
*/
|
||||
public function collectServiceMappings(BookingDto $bookingDto): array
|
||||
{
|
||||
@@ -99,7 +99,7 @@ class ServiceMappingCollector
|
||||
/**
|
||||
* Collects transportation service mappings.
|
||||
*
|
||||
* @return array<int, list<int>> Map of transportation service ID to participant IDs
|
||||
* @return array<string, array<int>> Map of transportation service ID to participant IDs
|
||||
*/
|
||||
public function collectTransportationMappings(BookingDto $bookingDto): array
|
||||
{
|
||||
@@ -126,7 +126,7 @@ class ServiceMappingCollector
|
||||
* Collects the unified pickup selection which applies to both directions.
|
||||
* The API receives this as outbound pickup data.
|
||||
*
|
||||
* @return array<int, list<int>> Map of pickup ID to participant IDs
|
||||
* @return array<string, array<int>> Map of pickup ID to participant IDs
|
||||
*/
|
||||
public function collectPickupMappings(BookingDto $bookingDto): array
|
||||
{
|
||||
@@ -148,7 +148,7 @@ class ServiceMappingCollector
|
||||
*
|
||||
* Collects the drop-off selection for the inbound/return direction.
|
||||
*
|
||||
* @return array<int, list<int>> Map of drop-off ID to participant IDs
|
||||
* @return array<string, array<int>> Map of drop-off ID to participant IDs
|
||||
*/
|
||||
public function collectDropOffMappings(BookingDto $bookingDto): array
|
||||
{
|
||||
@@ -171,7 +171,7 @@ class ServiceMappingCollector
|
||||
* CRITICAL: Insurance data is only included in CREATE flow, not in UPDATE flow.
|
||||
* IMPORTANT: Excludes synthetic "no insurance" option from BPN transmission.
|
||||
*
|
||||
* @return array<string, list<int>> Map of insurance ID to participant IDs
|
||||
* @return array<string, array<int>> Map of insurance ID to participant IDs
|
||||
*/
|
||||
public function collectInsuranceMappings(BookingDto $bookingDto): array
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@ class CountryDataProvider
|
||||
{
|
||||
}
|
||||
|
||||
/** @return array<string, Country> */
|
||||
public function getAll(): array
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Exception;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Exception;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Form\ChoiceLoader;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Form;
|
||||
|
||||
@@ -11,7 +10,6 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/** @extends AbstractType<string> */
|
||||
class CountryType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly CountryDataProvider $countries)
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace App\BusProNet\Model;
|
||||
*/
|
||||
class AgeConstraintResult
|
||||
{
|
||||
/** @param array<string, mixed> $metadata */
|
||||
public function __construct(
|
||||
public readonly string $type,
|
||||
public readonly ?int $ageFrom = null,
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace App\BusProNet\Model;
|
||||
*/
|
||||
class BaseData
|
||||
{
|
||||
/** @param array<array-key, mixed> $items */
|
||||
public function __construct(private readonly array $items)
|
||||
{
|
||||
}
|
||||
@@ -20,7 +19,7 @@ class BaseData
|
||||
/**
|
||||
* Retrieves all items in the base data collection.
|
||||
*
|
||||
* @return array<array-key, mixed> The complete items array
|
||||
* @return array The complete items array
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
|
||||
@@ -19,7 +19,6 @@ class Booking
|
||||
public ?int $agencyId = null;
|
||||
public ?int $bookingNumber = null;
|
||||
public ?string $status = null;
|
||||
public ?int $applicantId = null;
|
||||
public ?PersonalData $applicant = null;
|
||||
public ?int $participantCount = null;
|
||||
public ?float $price = null;
|
||||
@@ -37,50 +36,14 @@ class Booking
|
||||
public ?string $paymentType = null;
|
||||
public ?string $paymentLabel = null;
|
||||
public ?BankAccount $bankAccount = null;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $participantsStatus = [];
|
||||
|
||||
/**
|
||||
* @var array<int, PersonalData>
|
||||
*/
|
||||
public array $participants = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Service>
|
||||
*/
|
||||
public array $transportationServices = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Service>
|
||||
*/
|
||||
public array $additionalServices = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Room>
|
||||
*/
|
||||
public array $rooms = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Pickup>
|
||||
*/
|
||||
public array $pickups = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Pickup>
|
||||
*/
|
||||
public array $dropOffs = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Surcharge>
|
||||
*/
|
||||
public array $surcharges = [];
|
||||
|
||||
/**
|
||||
* @var array<string|int, Insurance>
|
||||
*/
|
||||
public array $insurances = [];
|
||||
public ?int $invoiceNumber = null;
|
||||
public ?float $totalPrice = null;
|
||||
@@ -221,7 +184,7 @@ class Booking
|
||||
public function getInsuranceForParticipant(int $participantIndex): ?Insurance
|
||||
{
|
||||
foreach ($this->insurances as $insurance) {
|
||||
if (in_array($participantIndex, $insurance->mapping, true)) {
|
||||
if (in_array($participantIndex, $insurance->mapping ?? [], true)) {
|
||||
return $insurance;
|
||||
}
|
||||
}
|
||||
@@ -371,8 +334,6 @@ class Booking
|
||||
*/
|
||||
public function isEditable(): bool
|
||||
{
|
||||
$now = new \DateTimeImmutable();
|
||||
|
||||
return $this->travelDate > $now && false === in_array($this->status, ['S', 'U']);
|
||||
return $this->travelDate > new \DateTimeImmutable() && false === in_array($this->status, ['S', 'U']);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class ContactFormResponse
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $addressId,
|
||||
public readonly int $personId,
|
||||
public readonly bool $isNewRecord,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,8 @@ namespace App\BusProNet\Model;
|
||||
*/
|
||||
class CrmAttributes
|
||||
{
|
||||
/**
|
||||
* @var array<int, CrmSelectionGroup>|null
|
||||
*/
|
||||
public ?array $selectionGroups = null;
|
||||
|
||||
/**
|
||||
* @var array<int, CrmAction>|null
|
||||
*/
|
||||
public ?array $crmActions = null;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $hotelCodes = [];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $roles = [];
|
||||
}
|
||||
@@ -15,15 +15,15 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
*/
|
||||
class CrmSelection
|
||||
{
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $label = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $mutable = false;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $selected = false;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class CrmSelectionGroup
|
||||
* Maps BusPro IDs of CRM selection groups representing
|
||||
* included services.
|
||||
*
|
||||
* @var array<int, string>
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public static array $includedServicesMapping = [
|
||||
7 => 'Skipass',
|
||||
@@ -32,16 +32,13 @@ class CrmSelectionGroup
|
||||
90 => 'Transport',
|
||||
];
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $label = null;
|
||||
|
||||
/**
|
||||
* @var array<int, CrmSelection>|null
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?array $selections = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* Represents error codes and messages for the BusProNet API.
|
||||
*
|
||||
* This class provides a static mapping of error codes to their corresponding
|
||||
* German error messages for consistent error handling across the application.
|
||||
* Error codes range from 100-999 and cover various API scenarios.
|
||||
*/
|
||||
class Error
|
||||
{
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private static array $errors = [
|
||||
100 => 'Anfrageknoten fehlt',
|
||||
101 => 'Satzknoten fehlt in Anfrageknoten',
|
||||
102 => 'Ungültiger Satztyp [#1#]',
|
||||
103 => 'User fehlt',
|
||||
104 => 'User fehlerhaft',
|
||||
105 => 'Key fehlt',
|
||||
106 => 'Key fehlerhaft',
|
||||
200 => 'Keine Einträge vorhanden',
|
||||
201 => 'Keine Partner zur Auswahl gefunden',
|
||||
202 => 'Keinen Partner mit der ID [#1#] gefunden',
|
||||
203 => 'Ungültiger Termin',
|
||||
300 => 'IDReise fehlt',
|
||||
301 => 'Reise nicht gefunden',
|
||||
302 => 'IDLeistung (Hin & Rück) fehlt',
|
||||
303 => 'IDLeistung passt nicht zu IDReise',
|
||||
304 => 'Es konnte nicht für alle Teilnehmer ein Sitzplatz ermittelt werden',
|
||||
305 => 'Anzahl Personen fehlt',
|
||||
306 => 'Leistungen mit ID #1# nicht gefunden',
|
||||
307 => 'Leistungen mit ID #1# passen nicht zur Reise mit ID #2#',
|
||||
308 => 'Keine Zahlungsarten gefunden',
|
||||
400 => 'IDPartner fehlt',
|
||||
401 => 'Partner nicht gefunden',
|
||||
402 => 'Bis-Termin fehlt',
|
||||
403 => 'Keine Unterbringungsleistungen gefunden',
|
||||
500 => 'Art fehlt',
|
||||
501 => 'Reise- und Buchungszeitraum fehlen',
|
||||
502 => 'Keine Kunden gefunden',
|
||||
600 => 'Keine Gutscheine gefunden',
|
||||
601 => 'Gutschein-IDs fehlen',
|
||||
602 => 'Gutscheine mit ID #1# nicht gefunden',
|
||||
610 => 'Buchungsart fehlt',
|
||||
611 => 'Buchungsart falsch',
|
||||
612 => 'Gutscheinart fehlt',
|
||||
613 => 'Gutscheinart falsch',
|
||||
614 => 'Gutscheinstamm-ID fehlt',
|
||||
615 => 'Gutschein (Stamm) mit ID #1# nicht gefunden',
|
||||
616 => 'Kapazität beim Gutschein #1# nicht ausreichend',
|
||||
617 => 'Rechnungsempfänger fehlt',
|
||||
618 => 'Zahlungsart fehlt',
|
||||
619 => 'Agentur-ID fehlt',
|
||||
620 => 'Agentur mit ID #1# nicht gefunden',
|
||||
650 => '#1#',
|
||||
700 => 'Keine Agenturen gefunden',
|
||||
701 => 'Agentur mit ID #1# nicht gefunden',
|
||||
800 => 'Produkt-ID fehlt',
|
||||
801 => 'Produkt mit ID #1# nicht gefunden',
|
||||
805 => 'Es wurden keine Produkte gefunden',
|
||||
810 => 'Stammdaten (#1#) nicht gefunden',
|
||||
900 => 'Buchungsart fehlt',
|
||||
901 => 'Buchungsart falsch',
|
||||
902 => 'Status fehlt',
|
||||
903 => 'Status falsch',
|
||||
904 => 'Agentur-ID fehlt',
|
||||
905 => 'Agentur mit ID #1# nicht gefunden',
|
||||
906 => 'Reise-ID fehlt',
|
||||
907 => 'Reise mit ID #1# nicht gefunden',
|
||||
908 => 'Reise mit ID #1# ist storniert',
|
||||
909 => 'Beförderungen fehlen',
|
||||
910 => 'Leistung mit ID #1# gehört nicht zur Reise',
|
||||
911 => 'Leistung mit ID #1# gehört nicht zum Produkt',
|
||||
912 => 'Beförderungsleistung für die #1# fehlt',
|
||||
913 => 'Unterbringungen fehlen',
|
||||
914 => 'Partner-ID fehlt',
|
||||
915 => 'Partner mit ID #1# nicht gefunden',
|
||||
916 => 'Ferienziel-Unterbringungen fehlen',
|
||||
917 => 'Ferienziel: Zimmer-IDZ fehlt',
|
||||
918 => 'Ferienziel: Kategorie fehlt',
|
||||
919 => 'Ferienziel: Verpflegungs-ID fehlt',
|
||||
920 => 'Ferienziel: Anreise fehlt',
|
||||
921 => 'Ferienziel: Anreise passt nicht zur Beförderungsleistung (Hinfahrt)',
|
||||
922 => 'Ferienziel: Abreise fehlt',
|
||||
923 => 'Ferienziel: Abreise passt nicht zur Beförderungsleistung (Rückfahrt)',
|
||||
924 => 'Ferienziel: Keine Preise zu den Daten gefunden',
|
||||
925 => 'Ferienziel: Preis zu den Daten nicht gefunden',
|
||||
930 => 'Zustiege fehlen',
|
||||
931 => 'Zustieg mit ID #1# nicht gefunden',
|
||||
932 => 'Zustieg mit ID #1# bei Leistung #2# nicht freigegeben',
|
||||
933 => 'Sitzplan mit ID #1# bei Leistung #2# nicht freigegeben',
|
||||
935 => 'Versicherung mit ID #1# nicht gefunden',
|
||||
940 => 'Zahlungsart fehlt',
|
||||
941 => 'Zahlungsart-ID fehlt',
|
||||
942 => 'Zahlungsart mit ID #1# nicht gültig',
|
||||
950 => 'Anmelder fehlt',
|
||||
951 => 'Teilnehmerliste fehlt',
|
||||
952 => 'Teilnehmer-ID fehlt',
|
||||
960 => 'Preisfehler: Struct #1# / Obj #2#',
|
||||
961 => 'Buchung konnte nicht gespeichert werden',
|
||||
980 => 'Reise ist fürs Internet gesperrt',
|
||||
981 => 'Reise ist nicht mehr buchbar (#1#)',
|
||||
982 => 'Optionsbuchungen nicht zugelassen',
|
||||
983 => 'Anfragebuchung nicht zugelassen',
|
||||
984 => 'Leistung mit ID #1# nicht fürs Internet buchbar',
|
||||
985 => 'Zustieg mit ID #1# nicht fürs Internet buchbar',
|
||||
999 => 'Systemfehler: #1#',
|
||||
];
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* Extended availability details for one service from VERFUEGBARKEIT2.
|
||||
*
|
||||
* This model augments classic availability with optional timing/date/description
|
||||
* fields used to enrich persisted travel snapshots.
|
||||
*/
|
||||
class ExtendedAvailability
|
||||
{
|
||||
public ?int $serviceId = null;
|
||||
public ?string $status = null;
|
||||
public ?int $available = null;
|
||||
public ?float $price = null;
|
||||
public ?\DateTimeImmutable $dateFrom = null;
|
||||
public ?\DateTimeImmutable $dateTo = null;
|
||||
public ?string $timeFrom = null;
|
||||
public ?string $description = null;
|
||||
public ?int $ageFrom = null;
|
||||
public ?int $ageTo = null;
|
||||
public ?bool $mandatory = null;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
/**
|
||||
* Response model for extended availability endpoint payloads.
|
||||
*/
|
||||
class ExtendedServiceAvailabilityResponse
|
||||
{
|
||||
/**
|
||||
* @param array<int, ExtendedAvailability> $services
|
||||
* @param array<string> $allowedBookingStatus
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $services,
|
||||
public readonly array $allowedBookingStatus = [],
|
||||
public readonly ?string $travelStatus = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, ExtendedAvailability>
|
||||
*/
|
||||
public function getServices(): array
|
||||
{
|
||||
return $this->services;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
*/
|
||||
class Guide
|
||||
{
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $name = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $phone = null;
|
||||
}
|
||||
@@ -15,24 +15,24 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
*/
|
||||
class Hotel
|
||||
{
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $code = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $name = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $city = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $street = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $phone = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $country = null;
|
||||
}
|
||||
@@ -182,15 +182,8 @@ class Insurance
|
||||
$urls = [];
|
||||
|
||||
foreach ($this->containedInsurances as $containedInsurance) {
|
||||
$url = match ($urlProperty) {
|
||||
'urlInfo' => $containedInsurance->urlInfo,
|
||||
'urlProductInfo' => $containedInsurance->urlProductInfo,
|
||||
'urlTerms' => $containedInsurance->urlTerms,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (null !== $url) {
|
||||
$urls[] = $url;
|
||||
if (null !== $containedInsurance->{$urlProperty}) {
|
||||
$urls[] = $containedInsurance->{$urlProperty};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,7 @@ class PersonalData
|
||||
#[Assert\Valid(groups: ['personal_data'])]
|
||||
public Communication $communication;
|
||||
|
||||
/** @var list<string> */
|
||||
public array $roles = [];
|
||||
/** @var list<string> */
|
||||
public array $hotelCodes = [];
|
||||
|
||||
public function __construct()
|
||||
|
||||
@@ -17,35 +17,34 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
*/
|
||||
class Pickup
|
||||
{
|
||||
#[Groups(['api:list', 'snapshot'])]
|
||||
#[Groups(['api:list'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:list', 'snapshot'])]
|
||||
#[Groups(['api:list'])]
|
||||
public ?string $code = null;
|
||||
|
||||
#[Groups(['api:list', 'snapshot'])]
|
||||
#[Groups(['api:list'])]
|
||||
public ?string $city = null;
|
||||
|
||||
#[Groups(['api:list', 'snapshot'])]
|
||||
#[Groups(['api:list'])]
|
||||
public ?string $postalCode = null;
|
||||
|
||||
#[Groups(['api:list', 'snapshot'])]
|
||||
#[Groups(['api:list'])]
|
||||
public ?string $street = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i'])]
|
||||
public ?\DateTimeImmutable $time = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?float $price = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?float $priceOutbound = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?float $priceInbound = null;
|
||||
|
||||
/** @var list<int> */
|
||||
#[Groups(['api:booking'])]
|
||||
public array $mapping = [];
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
|
||||
@@ -20,19 +20,19 @@ class Room
|
||||
public const SELECTION_TYPE_BY_PAX = 'by_pax';
|
||||
public const SELECTION_TYPE_BY_ROOM = 'by_room';
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $category = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $boardId = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $code = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $label = null;
|
||||
|
||||
#[Groups(['booking'])]
|
||||
@@ -46,35 +46,33 @@ class Room
|
||||
#[Groups(['booking'])]
|
||||
public ?int $totalCount = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $minPax = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $maxPax = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $nights = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $status = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $available = null;
|
||||
|
||||
#[Groups(['booking'])]
|
||||
public ?string $board = null;
|
||||
|
||||
/** @var list<int> */
|
||||
#[Groups(['booking'])]
|
||||
public array $mapping = [];
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?float $price = null;
|
||||
|
||||
#[Groups(['booking'])]
|
||||
public ?float $totalPrice = null;
|
||||
|
||||
/** @var array<int, float|null> */
|
||||
#[Groups(['booking'])]
|
||||
public array $individualPrice = [];
|
||||
|
||||
|
||||
@@ -17,90 +17,84 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
*/
|
||||
class Service
|
||||
{
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $category = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $status = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public bool $mandatory = false;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
public bool $autoBook = false;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $label = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $dateFrom = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $timeFrom = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $dayTime = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $dateTo = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $subType = null;
|
||||
|
||||
#[Groups(['booking'])]
|
||||
public ?int $totalCount = null;
|
||||
|
||||
/** @var list<int> */
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $mapping = [];
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?float $price = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?float $totalPrice = null;
|
||||
|
||||
/** @var array<int, float|null> */
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $individualPrice = [];
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $available = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $source = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $direction = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $ageFrom = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $ageTo = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $birthYearFrom = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?int $birthYearTo = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $ageConstraintType = null;
|
||||
|
||||
/** @var array<string, mixed>|null */
|
||||
#[Groups(['api:single'])]
|
||||
public ?array $ageConstraintMetadata = null;
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $rawAgeConstraintData = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $description = null;
|
||||
|
||||
/**
|
||||
@@ -110,6 +104,6 @@ class Service
|
||||
* When false, this service's price is excluded when determining which insurance
|
||||
* tier is appropriate for a participant.
|
||||
*/
|
||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public bool $includeInInsuranceCalculation = true;
|
||||
}
|
||||
@@ -13,9 +13,7 @@ namespace App\BusProNet\Model;
|
||||
class Surcharge
|
||||
{
|
||||
public ?string $label = null;
|
||||
/** @var list<int> */
|
||||
public array $mapping = [];
|
||||
public ?float $totalPrice = null;
|
||||
/** @var array<int, float|null> */
|
||||
public array $individualPrice = [];
|
||||
}
|
||||
@@ -20,43 +20,40 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
class Travel
|
||||
{
|
||||
use SortByPriceTrait;
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $code = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $productCode = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
public ?int $productId = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $label = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?int $hotelId = null;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?Hotel $hotel = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $dateFrom = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
public ?\DateTimeImmutable $dateTo = null;
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $type = null;
|
||||
|
||||
/**
|
||||
* Travel booking status from BusProNet API.
|
||||
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'.
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $status = null;
|
||||
|
||||
/**
|
||||
@@ -65,71 +62,53 @@ class Travel
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $allowedBookingStatus = [];
|
||||
|
||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?float $priceFrom = null;
|
||||
|
||||
/**
|
||||
* @var array<int, CrmSelectionGroup>
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $selectionGroups = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Service>
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $additionalServices = [];
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $additionalServicesMutable = true;
|
||||
|
||||
/**
|
||||
* @var array<int, Service>
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $transportationServices = [];
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $transportationServicesMutable = true;
|
||||
|
||||
/**
|
||||
* @var array<int, Pickup>
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $pickups = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Pickup>
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $dropOffs = [];
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $pickupsMutable = true;
|
||||
|
||||
/**
|
||||
* @var array<int, Room>
|
||||
*/
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public array $rooms = [];
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $roomsMutable = true;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $participantDataMutable = true;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public bool $participantCountMutable = true;
|
||||
|
||||
#[Groups(['api:single', 'snapshot'])]
|
||||
#[Groups(['api:single'])]
|
||||
public ?Guide $guide = null;
|
||||
|
||||
/**
|
||||
* @var array<string|int, Insurance> Available insurances for this travel package
|
||||
* @var array<Insurance> Available insurances for this travel package
|
||||
*/
|
||||
#[Groups(['api:single'])]
|
||||
public array $insurances = [];
|
||||
@@ -203,7 +182,7 @@ class Travel
|
||||
* Maps selection group IDs to their corresponding services based on
|
||||
* the predefined included services mapping.
|
||||
*
|
||||
* @return array<int, CrmSelectionGroup> The included services from selection groups
|
||||
* @return array<int, Service> The included services from selection groups
|
||||
*/
|
||||
public function getIncludedServices(): array
|
||||
{
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Service;
|
||||
|
||||
use App\BusProNet\Service\Contract\BookingStatusRuleInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
|
||||
/**
|
||||
* Registry for booking status evaluation rules.
|
||||
*
|
||||
* Evaluates registered rules in priority order and returns the first matching
|
||||
* status. Falls back to 'F' when no rule applies.
|
||||
*/
|
||||
class BookingStatusRuleRegistry
|
||||
{
|
||||
private const DEFAULT_STATUS = 'F';
|
||||
|
||||
/**
|
||||
* @var BookingStatusRuleInterface[]
|
||||
*/
|
||||
private array $sortedRules;
|
||||
|
||||
/**
|
||||
* @param BookingStatusRuleInterface[] $rules
|
||||
*/
|
||||
public function __construct(array $rules)
|
||||
{
|
||||
$this->sortedRules = $this->sortRulesByPriority($rules);
|
||||
}
|
||||
|
||||
public function evaluateStatus(BookingDto $bookingDto): string
|
||||
{
|
||||
foreach ($this->sortedRules as $rule) {
|
||||
if (true === $rule->evaluate($bookingDto)) {
|
||||
return $rule->getStatus();
|
||||
}
|
||||
}
|
||||
|
||||
return self::DEFAULT_STATUS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BookingStatusRuleInterface[] $rules
|
||||
*
|
||||
* @return BookingStatusRuleInterface[]
|
||||
*/
|
||||
private function sortRulesByPriority(array $rules): array
|
||||
{
|
||||
usort($rules, static fn (
|
||||
BookingStatusRuleInterface $a,
|
||||
BookingStatusRuleInterface $b,
|
||||
): int => $b->getPriority() <=> $a->getPriority());
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Service\Contract;
|
||||
|
||||
use App\Form\Model\BookingDto;
|
||||
|
||||
/**
|
||||
* Defines the contract for booking status evaluation rules.
|
||||
*
|
||||
* Rules are evaluated in priority order. The first matching rule determines
|
||||
* the enforced booking status.
|
||||
*/
|
||||
interface BookingStatusRuleInterface
|
||||
{
|
||||
/**
|
||||
* Evaluates whether this rule applies to the given booking.
|
||||
*/
|
||||
public function evaluate(BookingDto $bookingDto): bool;
|
||||
|
||||
/**
|
||||
* Returns the status code to assign when this rule matches.
|
||||
*/
|
||||
public function getStatus(): string;
|
||||
|
||||
/**
|
||||
* Returns the rule priority (higher values evaluated first).
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Returns a human-readable rule description.
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Service\Contract;
|
||||
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
/**
|
||||
* Defines the contract for participant status evaluation rules.
|
||||
*
|
||||
* Status rules are evaluated in priority order to determine the appropriate
|
||||
* status code for a participant during booking creation. The first rule that
|
||||
* matches (evaluate returns true) determines the participant's status.
|
||||
*/
|
||||
interface ParticipantStatusRuleInterface
|
||||
{
|
||||
/**
|
||||
* Evaluates whether this rule applies to the given participant.
|
||||
*
|
||||
* @param ParticipantDto $participant The participant to evaluate
|
||||
*
|
||||
* @return bool True if this rule applies, false otherwise
|
||||
*/
|
||||
public function evaluate(ParticipantDto $participant): bool;
|
||||
|
||||
/**
|
||||
* Returns the status code to assign when this rule matches.
|
||||
*
|
||||
* @return string The status code (e.g., 'O' for Option, 'F' for Final)
|
||||
*/
|
||||
public function getStatus(): string;
|
||||
|
||||
/**
|
||||
* Returns the priority of this rule.
|
||||
*
|
||||
* Higher priority rules are evaluated first. Rules with the same priority
|
||||
* are evaluated in registration order.
|
||||
*
|
||||
* @return int The priority value (higher = evaluated first)
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Returns a human-readable description of this rule.
|
||||
*
|
||||
* Used for debugging and logging purposes.
|
||||
*
|
||||
* @return string Description of what this rule checks
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Service;
|
||||
|
||||
use App\BusProNet\Service\Contract\ParticipantStatusRuleInterface;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
/**
|
||||
* Registry for participant status evaluation rules.
|
||||
*
|
||||
* Manages a collection of status rules and evaluates them in priority order
|
||||
* to determine the appropriate status code for a participant during booking
|
||||
* creation. Returns a default status of 'F' (Final) when no rules match.
|
||||
*/
|
||||
class ParticipantStatusRuleRegistry
|
||||
{
|
||||
private const DEFAULT_STATUS = 'F';
|
||||
|
||||
/**
|
||||
* @var ParticipantStatusRuleInterface[]
|
||||
*/
|
||||
private array $sortedRules;
|
||||
|
||||
/**
|
||||
* @param ParticipantStatusRuleInterface[] $rules The rules to register
|
||||
*/
|
||||
public function __construct(array $rules)
|
||||
{
|
||||
$this->sortedRules = $this->sortRulesByPriority($rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates all rules to determine the participant's status.
|
||||
*
|
||||
* Rules are evaluated in priority order (highest first). The first rule
|
||||
* that matches determines the status. Returns 'F' if no rules match.
|
||||
*
|
||||
* @param ParticipantDto $participant The participant to evaluate
|
||||
*
|
||||
* @return string The determined status code
|
||||
*/
|
||||
public function evaluateStatus(ParticipantDto $participant): string
|
||||
{
|
||||
foreach ($this->sortedRules as $rule) {
|
||||
if (true === $rule->evaluate($participant)) {
|
||||
return $rule->getStatus();
|
||||
}
|
||||
}
|
||||
|
||||
return self::DEFAULT_STATUS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts rules by priority in descending order.
|
||||
*
|
||||
* @param ParticipantStatusRuleInterface[] $rules The rules to sort
|
||||
*
|
||||
* @return ParticipantStatusRuleInterface[] The sorted rules
|
||||
*/
|
||||
private function sortRulesByPriority(array $rules): array
|
||||
{
|
||||
usort($rules, static fn (
|
||||
ParticipantStatusRuleInterface $a,
|
||||
ParticipantStatusRuleInterface $b,
|
||||
): int => $b->getPriority() <=> $a->getPriority());
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -5,37 +5,26 @@ declare(strict_types=1);
|
||||
namespace App\BusProNet\Service\StatusRule;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Service\Contract\BookingStatusRuleInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\BusProNet\Service\Contract\ParticipantStatusRuleInterface;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
/**
|
||||
* Status rule for bookings with chaperon (Begleitperson) services.
|
||||
* Status rule for participants with chaperon (Begleitperson) services.
|
||||
*
|
||||
* Assigns 'Option' status to bookings where any participant has selected any
|
||||
* service containing 'Begleitperson' in its label.
|
||||
* Assigns 'Option' status to participants who have selected any service
|
||||
* containing 'Begleitperson' in its label. This applies to accompanying
|
||||
* persons whose booking confirmation may depend on the main participant.
|
||||
*/
|
||||
class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
||||
class ChaperonServiceStatusRule implements ParticipantStatusRuleInterface
|
||||
{
|
||||
private const STATUS = 'O';
|
||||
private const PRIORITY = 100;
|
||||
private const SEARCH_TERM = 'begleitperson';
|
||||
|
||||
/**
|
||||
* @see BookingStatusRuleInterface::evaluate()
|
||||
* @see ParticipantStatusRuleInterface::evaluate()
|
||||
*/
|
||||
public function evaluate(BookingDto $bookingDto): bool
|
||||
{
|
||||
foreach ($bookingDto->participants as $participant) {
|
||||
if (true === $this->participantHasChaperonService($participant)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function participantHasChaperonService(ParticipantDto $participant): bool
|
||||
public function evaluate(ParticipantDto $participant): bool
|
||||
{
|
||||
// Check array services
|
||||
$arrayServices = [
|
||||
@@ -47,7 +36,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
||||
|
||||
foreach ($arrayServices as $services) {
|
||||
foreach ($services as $service) {
|
||||
if (true === $this->containsSearchTerm($service->label)) {
|
||||
if ($service instanceof Service && true === $this->containsSearchTerm($service->label)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +58,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BookingStatusRuleInterface::getStatus()
|
||||
* @see ParticipantStatusRuleInterface::getStatus()
|
||||
*/
|
||||
public function getStatus(): string
|
||||
{
|
||||
@@ -77,7 +66,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BookingStatusRuleInterface::getPriority()
|
||||
* @see ParticipantStatusRuleInterface::getPriority()
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
@@ -85,7 +74,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BookingStatusRuleInterface::getDescription()
|
||||
* @see ParticipantStatusRuleInterface::getDescription()
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
|
||||
@@ -28,9 +28,9 @@ trait SortByPriceTrait
|
||||
* collections in the application. Items are sorted from lowest to highest price,
|
||||
* with null prices being treated as zero (appearing first).
|
||||
*
|
||||
* @param array<array-key, object> $items Array of objects with a price property to sort
|
||||
* @param array $items Array of objects with a price property to sort
|
||||
*
|
||||
* @return array<array-key, object> The sorted array of objects (ascending by price)
|
||||
* @return array The sorted array of objects (ascending by price)
|
||||
*/
|
||||
protected function sortByPrice(array $items): array
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ use Carbon\Exceptions\InvalidFormatException;
|
||||
|
||||
trait TypeConversionTrait
|
||||
{
|
||||
/** @return list<string> */
|
||||
protected function stringToArray(?string $string, string $separator = ','): array
|
||||
{
|
||||
if (true === empty($string)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Traits;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Utility;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Utility;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlLoader;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class HotelLoader extends AbstractLoader
|
||||
{
|
||||
/** @return array<int, Hotel> */
|
||||
public function loadAll(?string $filename = 'hotel.xml'): array
|
||||
{
|
||||
try {
|
||||
@@ -92,12 +91,6 @@ class HotelLoader extends AbstractLoader
|
||||
public function patchHotelDetails(Travel $travel): void
|
||||
{
|
||||
$hotelId = $travel->hotelId;
|
||||
if (null === $hotelId) {
|
||||
$travel->hotel = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$hotel = $this->loadById($hotelId);
|
||||
$travel->hotel = $hotel;
|
||||
|
||||
@@ -22,12 +22,11 @@ class InsuranceLoader extends AbstractLoader
|
||||
parent::__construct($cache, $xmlExport);
|
||||
}
|
||||
|
||||
/** @return array<string|int, Insurance> */
|
||||
public function loadAll(?string $filename = 'versicherungen.xml'): array
|
||||
{
|
||||
try {
|
||||
return $this->cache->get('bpn_insurances', function (ItemInterface $item) use ($filename) {
|
||||
$item->expiresAfter(24 * 60 * 60);
|
||||
$item->expiresAfter(3 * 60 * 60);
|
||||
|
||||
$crawler = $this->loadXml($filename);
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class PickupLoader extends AbstractLoader
|
||||
{
|
||||
/** @return array<int|string, Pickup> */
|
||||
public function loadAll(?string $filename = 'zustiege.xml'): array
|
||||
{
|
||||
try {
|
||||
@@ -66,11 +65,6 @@ class PickupLoader extends AbstractLoader
|
||||
$travel->dropOffs = $this->patchAndOrderDropOffs($travel->dropOffs, $travel->pickups);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Pickup> $pickups
|
||||
*
|
||||
* @return array<int, Pickup>
|
||||
*/
|
||||
private function patchAndSortPickups(array $pickups): array
|
||||
{
|
||||
$this->enrichPickupDetails($pickups);
|
||||
@@ -143,9 +137,6 @@ class PickupLoader extends AbstractLoader
|
||||
{
|
||||
foreach ($pickups as $pickup) {
|
||||
$pickupData = $this->loadById($pickup->id);
|
||||
if (null === $pickupData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pickup->code = $pickupData->code;
|
||||
$pickup->postalCode = $pickupData->postalCode;
|
||||
|
||||
@@ -51,7 +51,7 @@ class TravelLoader extends AbstractLoader
|
||||
* including ID, code, label, dates, and associated hotels. The mapping is
|
||||
* cached for 3 hours to improve performance.
|
||||
*
|
||||
* @return array<int, array<string, mixed>> Mapping of travel IDs to their metadata and file paths
|
||||
* @return array<int, array> Mapping of travel IDs to their metadata and file paths
|
||||
*/
|
||||
public function generateFilesMap(): array
|
||||
{
|
||||
@@ -74,7 +74,7 @@ class TravelLoader extends AbstractLoader
|
||||
$travelDataNodes = $crawler->filterXPath('//reisen/reise/termin');
|
||||
|
||||
$travelDataNodes->each(function (Crawler $node) use (&$mapping, $hotels, $file) {
|
||||
$travelId = (int) $node->attr('idbuspro');
|
||||
$travelId = $node->attr('idbuspro');
|
||||
|
||||
$mapping[$travelId] = [
|
||||
'id' => $travelId,
|
||||
@@ -102,7 +102,7 @@ class TravelLoader extends AbstractLoader
|
||||
return $mapping;
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new \RuntimeException('Cache key error in generateFilesMap', 0, $e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,6 @@ class TravelLoader extends AbstractLoader
|
||||
*
|
||||
* @throws TravelNotFoundException When travel ID is not found
|
||||
* @throws HotelNotInTravelException When hotel ID exists but not for this travel
|
||||
* @throws \RuntimeException When cached mapping generation fails unexpectedly
|
||||
*/
|
||||
public function loadById(int $dateId, ?int $hotelId = null, ?string $filename = null): Travel
|
||||
{
|
||||
|
||||
@@ -39,7 +39,6 @@ abstract class AbstractParser
|
||||
return 0 < $node->count() && $this->stringToBool($node->text());
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
protected function getArrayValue(Crawler $node, string $separator = ','): array
|
||||
{
|
||||
if (0 === $node->count()) {
|
||||
@@ -48,23 +47,4 @@ abstract class AbstractParser
|
||||
|
||||
return $this->stringToArray($node->text(), $separator);
|
||||
}
|
||||
|
||||
protected function getAttrOrNullValue(Crawler $node, string $attribute): ?string
|
||||
{
|
||||
if (0 === $node->count()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $node->attr($attribute);
|
||||
}
|
||||
|
||||
protected function getRequiredAttrValue(Crawler $node, string $attribute, string $context): string
|
||||
{
|
||||
$value = $this->getAttrOrNullValue($node, $attribute);
|
||||
if (null === $value || '' === trim($value)) {
|
||||
throw new \InvalidArgumentException(sprintf('Missing required attribute "%s" in %s.', $attribute, $context));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ class AgeConstraintParserRegistry
|
||||
return $this->mergeConstraintResults($results, $constraintData);
|
||||
}
|
||||
|
||||
/** @param list<AgeConstraintResult> $results */
|
||||
private function mergeConstraintResults(array $results, string $rawData): AgeConstraintResult
|
||||
{
|
||||
if (empty($results)) {
|
||||
|
||||
@@ -11,7 +11,6 @@ class ApiResponseParser extends AbstractParser
|
||||
/**
|
||||
* @throws ResponseParserException
|
||||
*/
|
||||
/** @param array<string, mixed> $additionalArgs */
|
||||
public function parseXmlString(string $type, string $xml, array $additionalArgs = []): mixed
|
||||
{
|
||||
$crawler = new Crawler($xml);
|
||||
@@ -26,69 +25,56 @@ class ApiResponseParser extends AbstractParser
|
||||
|
||||
$resultNode = $crawler->filterXPath('//ergebnis');
|
||||
|
||||
try {
|
||||
switch ($responseType) {
|
||||
case ApiClient::TYPE_NOTIFICATION:
|
||||
return (new NotificationParser())->parse($crawler);
|
||||
case ApiClient::TYPE_CUSTOMER_DATA:
|
||||
$subType = $this->getStringOrNullValue($resultNode->filterXPath('//art'));
|
||||
if (null === $subType || '' === $subType) {
|
||||
throw new ResponseParserException('Unable to determine customer data subtype from XML response');
|
||||
}
|
||||
switch ($responseType) {
|
||||
case ApiClient::TYPE_NOTIFICATION:
|
||||
return (new NotificationParser())->parse($crawler);
|
||||
case ApiClient::TYPE_CUSTOMER_DATA:
|
||||
$subType = $resultNode->filterXPath('//art')->text();
|
||||
|
||||
switch ($subType) {
|
||||
case 'Adressdaten':
|
||||
case 'Adressdaten_Ändern':
|
||||
case 'Newsletter':
|
||||
return (new PersonalDataParser())->parse($resultNode);
|
||||
case 'Adresse_Neu':
|
||||
return (new ContactFormResponseParser())->parse($resultNode);
|
||||
case 'SelektionCRM':
|
||||
case 'SelektionCRM_Ändern':
|
||||
return (new CrmAttributesResponseParser())->parse($resultNode);
|
||||
case 'Vorgänge':
|
||||
return (new BookingsParser())->parse($resultNode);
|
||||
case 'Vorgang_Details':
|
||||
return (new BookingParser())->parse($resultNode);
|
||||
case 'Dokumentdruck':
|
||||
return (new DocumentsParser())->parseDocuments($resultNode);
|
||||
case 'Vorgangdruck':
|
||||
return (new DocumentsParser())->parseConfirmation($resultNode);
|
||||
}
|
||||
break;
|
||||
case ApiClient::TYPE_BASE_DATA_COUNTRIES:
|
||||
return (new CountriesParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_MUTABLE_DATA:
|
||||
return (new MutableDataParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_AVAILABILITY:
|
||||
return (new AvailabilitiesParser())->parseServices($resultNode);
|
||||
case ApiClient::TYPE_AVAILABILITY_EXTENDED:
|
||||
return (new ExtendedAvailabilitiesParser())->parseServices($resultNode);
|
||||
case ApiClient::TYPE_AVAILABILITY_HOTEL:
|
||||
return (new AvailabilitiesParser())->parseRooms($resultNode);
|
||||
case ApiClient::TYPE_BOOKING_UPDATE:
|
||||
return (new BookingUpdateParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_BOOKING:
|
||||
return (new BookingResponseParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PRODUCTS:
|
||||
return (new ProductsParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PRODUCT_DATA:
|
||||
$travelNode = $crawler->filterXPath('//reise/termin');
|
||||
switch ($subType) {
|
||||
case 'Adressdaten':
|
||||
case 'Adressdaten_Ändern':
|
||||
case 'Newsletter':
|
||||
return (new PersonalDataParser())->parse($resultNode);
|
||||
case 'Adresse_Neu':
|
||||
return (new ContactFormResponseParser())->parse($resultNode);
|
||||
case 'SelektionCRM':
|
||||
case 'SelektionCRM_Ändern':
|
||||
return (new CrmAttributesResponseParser())->parse($resultNode);
|
||||
case 'Vorgänge':
|
||||
return (new BookingsParser())->parse($resultNode);
|
||||
case 'Vorgang_Details':
|
||||
return (new BookingParser())->parse($resultNode);
|
||||
case 'Dokumentdruck':
|
||||
return (new DocumentsParser())->parseDocuments($resultNode);
|
||||
case 'Vorgangdruck':
|
||||
return (new DocumentsParser())->parseConfirmation($resultNode);
|
||||
}
|
||||
break;
|
||||
case ApiClient::TYPE_BASE_DATA_COUNTRIES:
|
||||
return (new CountriesParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_MUTABLE_DATA:
|
||||
return (new MutableDataParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_AVAILABILITY:
|
||||
return (new AvailabilitiesParser())->parseServices($resultNode);
|
||||
case ApiClient::TYPE_AVAILABILITY_HOTEL:
|
||||
return (new AvailabilitiesParser())->parseRooms($resultNode);
|
||||
case ApiClient::TYPE_BOOKING_UPDATE:
|
||||
return (new BookingUpdateParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_BOOKING:
|
||||
return (new BookingResponseParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PRODUCTS:
|
||||
return (new ProductsParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PRODUCT_DATA:
|
||||
$travelNode = $crawler->filterXPath('//reise/termin');
|
||||
|
||||
if (0 === $travelNode->count()) {
|
||||
throw new ResponseParserException('No travel data node found in product data response');
|
||||
}
|
||||
|
||||
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
|
||||
case ApiClient::TYPE_AGENCIES:
|
||||
return (new AgencyParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PURCHASE_VOUCHER:
|
||||
return (new PurchaseVoucherParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PROMO_VOUCHER:
|
||||
return (new PromoVoucherParser())->parse($resultNode);
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
throw new ResponseParserException('Invalid XML response structure: '.$e->getMessage(), 0, $e);
|
||||
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
|
||||
case ApiClient::TYPE_AGENCIES:
|
||||
return (new AgencyParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PURCHASE_VOUCHER:
|
||||
return (new PurchaseVoucherParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PROMO_VOUCHER:
|
||||
return (new PromoVoucherParser())->parse($resultNode);
|
||||
}
|
||||
|
||||
throw new ResponseParserException('Unable to parse XML response');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ use Symfony\Component\DomCrawler\Crawler;
|
||||
*/
|
||||
class BookingInsurancesParser extends AbstractParser
|
||||
{
|
||||
/** @return array<string, Insurance> */
|
||||
public function parse(Crawler $result): array
|
||||
{
|
||||
$insurances = [];
|
||||
|
||||
@@ -40,10 +40,10 @@ class BookingParser extends AbstractParser
|
||||
|
||||
$travelData = $node->filterXPath('//reise');
|
||||
|
||||
$booking->travelName = $this->getAttrOrNullValue($travelData, 'bezeichnung');
|
||||
$booking->travelName = $travelData->attr('bezeichnung');
|
||||
$booking->dateId = $this->getIntOrNullValue($node->filterXPath('//idreise'));
|
||||
$booking->travelCode = $this->getAttrOrNullValue($travelData, 'code');
|
||||
$booking->travelDate = $this->stringToDate($this->getAttrOrNullValue($travelData, 'termin'));
|
||||
$booking->travelCode = $travelData->attr('code');
|
||||
$booking->travelDate = $this->stringToDate($travelData->attr('termin'));
|
||||
|
||||
$booking->hotelId = $this->getIntOrNullValue($node->filterXPath('//idpartner'));
|
||||
$booking->hotelName = $this->getStringOrNullValue($node->filterXPath('//partner'));
|
||||
@@ -56,17 +56,17 @@ class BookingParser extends AbstractParser
|
||||
$booking->participants = $this->parseParticipants($node->filterXPath('//teilnehmerliste/teilnehmer'));
|
||||
|
||||
$paymentData = $node->filterXPath('//zahlung');
|
||||
$booking->paymentId = $this->getAttrOrNullValue($paymentData, 'idzahlungsart');
|
||||
$booking->paymentLabel = $this->getAttrOrNullValue($paymentData, 'bezeichnung');
|
||||
$booking->paymentType = $this->getAttrOrNullValue($paymentData, 'art');
|
||||
$booking->paymentId = (int) $paymentData->attr('idzahlungsart');
|
||||
$booking->paymentLabel = $paymentData->attr('bezeichnung');
|
||||
$booking->paymentType = $paymentData->attr('art');
|
||||
|
||||
if (0 < $paymentData->children()->count()) {
|
||||
$bankDataNode = $paymentData->children()->first();
|
||||
$bankAccount = new BankAccount();
|
||||
$bankAccount->iban = $this->getAttrOrNullValue($bankDataNode, 'iban');
|
||||
$bankAccount->bic = $this->getAttrOrNullValue($bankDataNode, 'bic');
|
||||
$bankAccount->bankName = $this->getAttrOrNullValue($bankDataNode, 'kreditinstitut');
|
||||
$bankAccount->holder = $this->getAttrOrNullValue($bankDataNode, 'kontoinhaber');
|
||||
$bankAccount->iban = $bankDataNode->attr('iban');
|
||||
$bankAccount->bic = $bankDataNode->attr('bic');
|
||||
$bankAccount->bankName = $bankDataNode->attr('kreditinstitut');
|
||||
$bankAccount->holder = $bankDataNode->attr('kontoinhaber');
|
||||
$booking->bankAccount = $bankAccount;
|
||||
}
|
||||
|
||||
@@ -112,18 +112,12 @@ class BookingParser extends AbstractParser
|
||||
return $booking;
|
||||
}
|
||||
|
||||
/** @return array<int, PersonalData> */
|
||||
private function parseParticipants(Crawler $node): array
|
||||
{
|
||||
$participants = [];
|
||||
|
||||
$node->each(function (Crawler $node) use (&$participants) {
|
||||
$id = (int) ($this->getAttrOrNullValue($node, 'id') ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = (int) $node->attr('id');
|
||||
$participants[$id - 1] = $this->parsePersonalData($node);
|
||||
});
|
||||
|
||||
|
||||
@@ -32,11 +32,7 @@ class BookingResponseParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): BookingResponse
|
||||
{
|
||||
$status = $this->getStringOrNullValue($node->filterXPath('//buchung'));
|
||||
if (null === $status || '' === $status) {
|
||||
throw new \InvalidArgumentException('Missing required booking status in booking response.');
|
||||
}
|
||||
|
||||
$status = $node->filterXPath('//buchung')->text();
|
||||
$bookingNumber = $this->getIntOrNullValue($node->filterXPath('//vorgang'));
|
||||
$totalPrice = $this->getFloatOrNullValue($node->filterXPath('//gesamtpreis'));
|
||||
$message = $this->getStringOrNullValue($node->filterXPath('//hinweis'));
|
||||
@@ -64,24 +60,18 @@ class BookingResponseParser extends AbstractParser
|
||||
$priceItems = [];
|
||||
|
||||
$node->filterXPath('//preise/preis')->each(function (Crawler $priceNode) use (&$priceItems): void {
|
||||
$position = (int) $this->getRequiredAttrValue($priceNode, 'position', 'booking response price item');
|
||||
$type = $this->getRequiredAttrValue($priceNode, 'art', 'booking response price item');
|
||||
$label = $this->getRequiredAttrValue($priceNode, 'bezeichnung', 'booking response price item');
|
||||
$unitPrice = $this->stringToFloat($this->getRequiredAttrValue($priceNode, 'preis', 'booking response price item'));
|
||||
$totalPrice = $this->stringToFloat($this->getRequiredAttrValue($priceNode, 'gesamtpreis', 'booking response price item'));
|
||||
|
||||
$priceItems[] = new PriceItem(
|
||||
position: $position,
|
||||
type: $type,
|
||||
subType: $this->getAttrOrNullValue($priceNode, 'unterart'),
|
||||
label: $label,
|
||||
dateFrom: $this->getAttrOrNullValue($priceNode, 'terminvon'),
|
||||
dateTo: $this->getAttrOrNullValue($priceNode, 'terminbis'),
|
||||
quantity: (int) ($this->getAttrOrNullValue($priceNode, 'anzahl') ?? 0),
|
||||
assignment: $this->getAttrOrNullValue($priceNode, 'zuordnung'),
|
||||
unitPrice: $unitPrice,
|
||||
totalPrice: $totalPrice,
|
||||
id: $this->getAttrOrNullValue($priceNode, 'id')
|
||||
position: (int) $priceNode->attr('position'),
|
||||
type: $priceNode->attr('art'),
|
||||
subType: $priceNode->attr('unterart'),
|
||||
label: $priceNode->attr('bezeichnung'),
|
||||
dateFrom: $priceNode->attr('terminvon'),
|
||||
dateTo: $priceNode->attr('terminbis'),
|
||||
quantity: (int) $priceNode->attr('anzahl'),
|
||||
assignment: $priceNode->attr('zuordnung'),
|
||||
unitPrice: $this->stringToFloat($priceNode->attr('preis')),
|
||||
totalPrice: $this->stringToFloat($priceNode->attr('gesamtpreis')),
|
||||
id: $priceNode->attr('id')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -104,26 +94,25 @@ class BookingResponseParser extends AbstractParser
|
||||
$finalPaymentAmount = null;
|
||||
$finalPaymentDate = null;
|
||||
|
||||
$depositNode = $paymentNode->filterXPath('.//anzahlung');
|
||||
$depositNode = $paymentNode->filterXPath('//anzahlung');
|
||||
if ($depositNode->count() > 0) {
|
||||
$depositAmount = $this->stringToFloat($this->getAttrOrNullValue($depositNode, 'betrag'));
|
||||
$depositDate = $this->getAttrOrNullValue($depositNode, 'termin');
|
||||
$depositAmount = $this->stringToFloat($depositNode->attr('betrag'));
|
||||
$depositDate = $depositNode->attr('termin');
|
||||
}
|
||||
|
||||
$finalPaymentNode = $paymentNode->filterXPath('.//restzahlung');
|
||||
$finalPaymentNode = $paymentNode->filterXPath('//restzahlung');
|
||||
if ($finalPaymentNode->count() > 0) {
|
||||
$finalPaymentAmount = $this->stringToFloat($this->getAttrOrNullValue($finalPaymentNode, 'betrag'));
|
||||
$finalPaymentDate = $this->getAttrOrNullValue($finalPaymentNode, 'termin');
|
||||
$finalPaymentAmount = $this->stringToFloat($finalPaymentNode->attr('betrag'));
|
||||
$finalPaymentDate = $finalPaymentNode->attr('termin');
|
||||
}
|
||||
|
||||
// Parse purchase vouchers (kaufgutschein elements)
|
||||
$appliedPurchaseVouchers = [];
|
||||
$voucherNodes = $paymentNode->filterXPath('.//kaufgutschein');
|
||||
$voucherNodes = $paymentNode->filterXPath('//kaufgutschein');
|
||||
$voucherNodes->each(function (Crawler $voucherNode) use (&$appliedPurchaseVouchers): void {
|
||||
$amount = $this->getAttrOrNullValue($voucherNode, 'betrag');
|
||||
$appliedPurchaseVouchers[] = [
|
||||
'nummer' => $this->getAttrOrNullValue($voucherNode, 'nummer') ?? '',
|
||||
'betrag' => $this->stringToFloat($amount),
|
||||
'nummer' => $voucherNode->attr('nummer') ?? '',
|
||||
'betrag' => $this->stringToFloat($voucherNode->attr('betrag')),
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
@@ -10,12 +9,10 @@ class BookingUpdateParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): BookingUpdate
|
||||
{
|
||||
$changeStatus = $this->getStringOrNullValue($node->filterXPath('//aenderung'));
|
||||
|
||||
$bookingUpdate = new BookingUpdate();
|
||||
$bookingUpdate->valid = 'möglich' === $changeStatus;
|
||||
$bookingUpdate->success = 'erfolgt' === $changeStatus;
|
||||
$bookingUpdate->status = $this->getStringOrNullValue($node->filterXPath('//status'));
|
||||
$bookingUpdate->valid = 'möglich' === $node->filterXPath('//aenderung')->text();
|
||||
$bookingUpdate->success = 'erfolgt' === $node->filterXPath('//aenderung')->text();
|
||||
$bookingUpdate->status = $node->filterXPath('//status')->text();
|
||||
|
||||
return $bookingUpdate;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
@@ -22,6 +21,7 @@ class BookingsParser extends AbstractParser
|
||||
$booking = new Booking();
|
||||
|
||||
$booking->id = $this->getIntOrNullValue($node->filterXPath('//id'));
|
||||
$booking->applicantId = $this->getIntOrNullValue($node->filterXPath('//idadresse_anmelder'));
|
||||
$booking->bookingNumber = $this->getIntOrNullValue($node->filterXPath('//vorgangsnummer'));
|
||||
$booking->status = $this->getStringOrNullValue($node->filterXPath('//status'));
|
||||
$booking->participantCount = $this->getIntOrNullValue($node->filterXPath('//personen'));
|
||||
|
||||
@@ -11,12 +11,8 @@ class ContactFormResponseParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): RegistrationResponse
|
||||
{
|
||||
$addressId = $this->getIntOrNullValue($node->filterXPath('//idadresse'));
|
||||
$personId = $this->getIntOrNullValue($node->filterXPath('//idperson'));
|
||||
if (null === $addressId || null === $personId) {
|
||||
throw new \InvalidArgumentException('Missing required idadresse or idperson in contact form response.');
|
||||
}
|
||||
|
||||
$addressId = (int) $node->filterXPath('//idadresse')->text();
|
||||
$personId = (int) $node->filterXPath('//idperson')->text();
|
||||
$isNewRecord = $this->getBoolValue($node->filterXPath('//neuanlage'));
|
||||
|
||||
return new RegistrationResponse(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class CrmAttributesResponseParser
|
||||
|
||||
$result
|
||||
->filterXPath('//selektionsmerkmale/selektionsgruppe')
|
||||
->each(function (Crawler $node) use (&$groups, &$roles) {
|
||||
->each(function (Crawler $node) use (&$groups, &$roles, &$hotelCode) {
|
||||
$group = new CrmSelectionGroup();
|
||||
$group->id = (int) $node->attr('id');
|
||||
$group->label = $node->attr('bezeichnung');
|
||||
@@ -47,14 +47,14 @@ class CrmAttributesResponseParser
|
||||
$roles[] = 'ROLE_HOUSE_MANAGER';
|
||||
$hotelCodes[] = $matches[1];
|
||||
}
|
||||
if (self::BPN_CRM_ID_ADMIN === $attribute->id && true === $attribute->selected) {
|
||||
if (static::BPN_CRM_ID_ADMIN === $attribute->id && true === $attribute->selected) {
|
||||
$roles[] = 'ROLE_ADMIN';
|
||||
$roles[] = 'ROLE_HOUSE_MANAGER';
|
||||
}
|
||||
if (self::BPN_CRM_ID_MANAGER === $attribute->id && true === $attribute->selected) {
|
||||
if (static::BPN_CRM_ID_MANAGER === $attribute->id && true === $attribute->selected) {
|
||||
$roles[] = 'ROLE_MANAGER';
|
||||
}
|
||||
if (self::BPN_CRM_ID_TEAMER === $attribute->id && true === $attribute->selected) {
|
||||
if (static::BPN_CRM_ID_TEAMER === $attribute->id && true === $attribute->selected) {
|
||||
$roles[] = 'ROLE_TEAMER';
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class CrmAttributesResponseParser
|
||||
;
|
||||
|
||||
if (true === in_array('ROLE_ADMIN', $roles, true)) {
|
||||
$hotelCodes[] = self::BPN_DEFAULT_HOTEL_CODE;
|
||||
$hotelCodes[] = static::BPN_DEFAULT_HOTEL_CODE;
|
||||
}
|
||||
|
||||
$response = new CrmAttributes();
|
||||
|
||||
@@ -44,30 +44,15 @@ class DocumentsParser
|
||||
|
||||
private function decode(Crawler $node): string
|
||||
{
|
||||
if (0 === $node->count()) {
|
||||
throw new \InvalidArgumentException('Missing PDF node in document response.');
|
||||
}
|
||||
|
||||
$parts = explode(',', $node->text(), 2);
|
||||
if (2 !== count($parts)) {
|
||||
throw new \InvalidArgumentException('Invalid PDF payload in document response.');
|
||||
}
|
||||
|
||||
[, $pdfData] = $parts;
|
||||
[, $pdfData] = explode(',', $node->text());
|
||||
|
||||
return base64_decode($pdfData);
|
||||
}
|
||||
|
||||
/** @return array{string, string} */
|
||||
private function getFileInfo(Crawler $node): array
|
||||
{
|
||||
$pdfData = $this->decode($node->filterXPath('.//pdf'));
|
||||
$filenameNode = $node->filterXPath('.//datei');
|
||||
if (0 === $filenameNode->count()) {
|
||||
throw new \InvalidArgumentException('Missing filename node in document response.');
|
||||
}
|
||||
|
||||
$filename = $filenameNode->text();
|
||||
$pdfData = $this->decode($node->filterXPath('//pdf'));
|
||||
$filename = $node->filterXPath('//datei')->text();
|
||||
|
||||
return [$filename, $pdfData];
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\ExtendedAvailability;
|
||||
use App\BusProNet\Model\ExtendedServiceAvailabilityResponse;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* Parser for VERFUEGBARKEIT2 service payloads.
|
||||
*
|
||||
* Extracts extended service-level attributes (dates, times, hints, constraints)
|
||||
* plus optional travel-level booking-status metadata.
|
||||
*/
|
||||
class ExtendedAvailabilitiesParser extends AbstractParser
|
||||
{
|
||||
/**
|
||||
* Parses extended availability services from API result node.
|
||||
*/
|
||||
public function parseServices(Crawler $result): ExtendedServiceAvailabilityResponse
|
||||
{
|
||||
$availabilities = [];
|
||||
|
||||
$result
|
||||
->filterXPath('//leistungen/leistung')
|
||||
->each(function (Crawler $node) use (&$availabilities) {
|
||||
$serviceId = (int) $node->attr('id');
|
||||
|
||||
$availability = new ExtendedAvailability();
|
||||
$availability->serviceId = $serviceId;
|
||||
$availability->status = $node->attr('status');
|
||||
$availability->available = (int) $node->attr('frei');
|
||||
$availability->price = $this->stringToFloat($node->attr('preis'));
|
||||
$availability->dateFrom = $this->stringToDate($node->attr('termin'));
|
||||
$availability->dateTo = $this->stringToDate($node->attr('bis'));
|
||||
$availability->timeFrom = $node->attr('uhrzeit_von');
|
||||
$availability->description = $node->attr('hinweis');
|
||||
|
||||
$ageFrom = $node->attr('altervon');
|
||||
if (null !== $ageFrom && '' !== trim($ageFrom)) {
|
||||
$availability->ageFrom = (int) $ageFrom;
|
||||
}
|
||||
|
||||
$ageTo = $node->attr('alterbis');
|
||||
if (null !== $ageTo && '' !== trim($ageTo)) {
|
||||
$availability->ageTo = (int) $ageTo;
|
||||
}
|
||||
|
||||
$mandatory = $node->attr('pflicht');
|
||||
if (null !== $mandatory && '' !== trim($mandatory)) {
|
||||
$availability->mandatory = $this->stringToBool($mandatory);
|
||||
}
|
||||
|
||||
$availabilities[$serviceId] = $availability;
|
||||
})
|
||||
;
|
||||
|
||||
$allowedBookingStatus = [];
|
||||
$travelStatus = null;
|
||||
$reiseNode = $result->filterXPath('//reise');
|
||||
|
||||
if (0 < $reiseNode->count()) {
|
||||
$travelStatus = $reiseNode->attr('status');
|
||||
|
||||
$bookingStatusPossible = $reiseNode->attr('buchungstatusmoeglich') ?? '';
|
||||
if ('' !== trim($bookingStatusPossible)) {
|
||||
$allowedBookingStatus = str_split($bookingStatusPossible);
|
||||
}
|
||||
}
|
||||
|
||||
return new ExtendedServiceAvailabilityResponse($availabilities, $allowedBookingStatus, $travelStatus);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class InsuranceParser extends AbstractParser
|
||||
|
||||
// Parse all individual insurances for reference lookup
|
||||
$insurance = $this->parseInsuranceNode($node, false);
|
||||
if (null !== $insurance->id) {
|
||||
if (null !== $insurance && null !== $insurance->id) {
|
||||
// Set complementary flag from XML attribute
|
||||
$insurance->complementary = $isComplementary;
|
||||
|
||||
@@ -53,7 +53,7 @@ class InsuranceParser extends AbstractParser
|
||||
$xmlContent->filterXPath('//versicherungspakete/versicherungspaket')
|
||||
->each(function (Crawler $node) use (&$insurances, $individualInsurances) {
|
||||
$insurance = $this->parseInsuranceNode($node, true, $individualInsurances);
|
||||
if (null !== $insurance->id) {
|
||||
if (null !== $insurance && null !== $insurance->id) {
|
||||
// Parse contained insurance IDs
|
||||
$insurance->containedInsuranceIds = $this->parseContainedInsuranceIds($node);
|
||||
|
||||
@@ -75,8 +75,8 @@ class InsuranceParser extends AbstractParser
|
||||
/**
|
||||
* Determines if a package contains any family insurances.
|
||||
*
|
||||
* @param Crawler $packageNode The package XML node
|
||||
* @param array<string, Insurance> $individualInsurances Parsed individual insurances for reference
|
||||
* @param Crawler $packageNode The package XML node
|
||||
* @param array<int, Insurance> $individualInsurances Parsed individual insurances for reference
|
||||
*
|
||||
* @return bool True if any contained insurance is a family insurance
|
||||
*/
|
||||
@@ -96,13 +96,13 @@ class InsuranceParser extends AbstractParser
|
||||
/**
|
||||
* Parses a single insurance or package node.
|
||||
*
|
||||
* @param Crawler $node The XML node to parse
|
||||
* @param bool $isPackage Whether this is a package node
|
||||
* @param array<string, Insurance> $individualInsurances Individual insurances for package family detection
|
||||
* @param Crawler $node The XML node to parse
|
||||
* @param bool $isPackage Whether this is a package node
|
||||
* @param array<int, Insurance> $individualInsurances Individual insurances for package family detection
|
||||
*
|
||||
* @return Insurance The parsed insurance object
|
||||
* @return Insurance|null The parsed insurance object
|
||||
*/
|
||||
private function parseInsuranceNode(Crawler $node, bool $isPackage, array $individualInsurances = []): Insurance
|
||||
private function parseInsuranceNode(Crawler $node, bool $isPackage, array $individualInsurances = []): ?Insurance
|
||||
{
|
||||
$insurance = new Insurance();
|
||||
$insurance->package = $isPackage;
|
||||
@@ -159,7 +159,7 @@ class InsuranceParser extends AbstractParser
|
||||
*
|
||||
* @param Crawler $xmlContent The XML content to scan
|
||||
*
|
||||
* @return list<string> Array of referenced insurance IDs
|
||||
* @return array<int> Array of referenced insurance IDs
|
||||
*/
|
||||
private function collectReferencedInsuranceIds(Crawler $xmlContent): array
|
||||
{
|
||||
@@ -180,7 +180,7 @@ class InsuranceParser extends AbstractParser
|
||||
*
|
||||
* @param Crawler $node The package XML node
|
||||
*
|
||||
* @return list<string> Array of contained insurance IDs
|
||||
* @return array<int> Array of contained insurance IDs (always int, as they reference individual insurances)
|
||||
*/
|
||||
private function parseContainedInsuranceIds(Crawler $node): array
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
@@ -24,10 +23,8 @@ class PersonalDataParser extends AbstractParser
|
||||
$personalData->name = $this->getStringOrNullValue($addressDataNode->filterXPath('//name'));
|
||||
$personalData->salutation = $this->getStringOrNullValue($addressDataNode->filterXPath('//anrede'));
|
||||
$personalData->title = $this->getStringOrNullValue($addressDataNode->filterXPath('//titel'));
|
||||
$gender = $this->getStringOrNullValue($addressDataNode->filterXPath('//geschlecht'));
|
||||
$nationality = $this->getStringOrNullValue($addressDataNode->filterXPath('//nationalitaet'));
|
||||
$personalData->gender = null !== $gender ? strtoupper($gender) : null;
|
||||
$personalData->nationality = null !== $nationality ? strtoupper($nationality) : null;
|
||||
$personalData->gender = strtoupper($this->getStringOrNullValue($addressDataNode->filterXPath('//geschlecht')));
|
||||
$personalData->nationality = strtoupper($this->getStringOrNullValue($addressDataNode->filterXPath('//nationalitaet')));
|
||||
|
||||
$personalData->height = $this->getStringOrNullValue($addressDataNode->filterXPath('//sonstiges1'));
|
||||
$personalData->weight = $this->getStringOrNullValue($addressDataNode->filterXPath('//sonstiges2'));
|
||||
@@ -60,7 +57,7 @@ class PersonalDataParser extends AbstractParser
|
||||
$remarksNode = $addressDataNode->filterXPath('//bemerkung');
|
||||
|
||||
if (0 < $remarksNode->count()) {
|
||||
$personalData->remarks = $this->getStringOrNullValue($remarksNode);
|
||||
$personalData->remarks = $remarksNode->text();
|
||||
}
|
||||
|
||||
return $personalData;
|
||||
|
||||
@@ -7,7 +7,6 @@ use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class PickupsParser extends AbstractParser
|
||||
{
|
||||
/** @return array<int, Pickup> */
|
||||
public function parse(Crawler $result): array
|
||||
{
|
||||
$pickups = [];
|
||||
|
||||
Loaded 100 of 377 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user