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,14 +40,6 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
|||||||
MAILER_DSN=null://null
|
MAILER_DSN=null://null
|
||||||
###< symfony/mailer ###
|
###< symfony/mailer ###
|
||||||
|
|
||||||
MAILJET_API_KEY=
|
|
||||||
MAILJET_API_SECRET=
|
|
||||||
MAILJET_API_BASE_URL=https://api.mailjet.com/v3/REST
|
|
||||||
MAILJET_DEFAULT_LIST_ID=
|
|
||||||
# Development default for HTTP Basic Auth password; override in production secrets.
|
|
||||||
MAILJET_WEBHOOK_BASIC_PASSWORD=secret
|
|
||||||
APP_NEWSLETTER_CONFIRMATION_TTL_HOURS=1
|
|
||||||
|
|
||||||
APP_BPN_USER=
|
APP_BPN_USER=
|
||||||
APP_BPN_PASSWORD=
|
APP_BPN_PASSWORD=
|
||||||
APP_BPN_IP=
|
APP_BPN_IP=
|
||||||
@@ -63,21 +55,11 @@ APP_TERMS_AND_CONDITIONS_URL="https://www.ep-reisen.de/skireisen/infos/allgemein
|
|||||||
APP_PRIVACY_POLICY_URL="https://www.ep-reisen.de/unternehmen/impressum-skireiseveranstalter/datenschutz-winterreisen/"
|
APP_PRIVACY_POLICY_URL="https://www.ep-reisen.de/unternehmen/impressum-skireiseveranstalter/datenschutz-winterreisen/"
|
||||||
APP_CMS_API_BASE_URL="https://www.ep-reisen.de/"
|
APP_CMS_API_BASE_URL="https://www.ep-reisen.de/"
|
||||||
APP_WEBSITE_BASE_URL="https://www.ep-reisen.de/"
|
APP_WEBSITE_BASE_URL="https://www.ep-reisen.de/"
|
||||||
APP_BASE_URL="https://my.ep-reisen.de"
|
|
||||||
APP_CUSTOMER_SERVICE_EMAIL="[email protected]"
|
APP_CUSTOMER_SERVICE_EMAIL="[email protected]"
|
||||||
APP_INSURANCE_REVOCATION_URL="https://www.hmrv.de/daten-aendern/vertrag-beenden/widerruf"
|
|
||||||
|
|
||||||
# Travel Data Service Configuration
|
# Travel Data Service Configuration
|
||||||
APP_TRAVEL_PREFER_REMOTE=false
|
APP_TRAVEL_PREFER_REMOTE=false
|
||||||
APP_TRAVEL_ENABLE_FALLBACK=true
|
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
|
# Booking Configuration
|
||||||
# Status for new bookings: 'F' (Fixed/Final), 'O' (Option - requires agency confirmation)
|
# Status for new bookings: 'F' (Fixed/Final), 'O' (Option - requires agency confirmation)
|
||||||
@@ -87,7 +69,6 @@ DEFAULT_BOOKING_STATUS=F
|
|||||||
API_KEYS=
|
API_KEYS=
|
||||||
|
|
||||||
XML_EXPORT_PATH="%kernel.project_dir%/var/xmlexport"
|
XML_EXPORT_PATH="%kernel.project_dir%/var/xmlexport"
|
||||||
XML_EXPORT_CONTINGENTS_PATH="%kernel.project_dir%/var/xmlexportzimmer"
|
|
||||||
JSON_EXPORT_PATH="%kernel.project_dir%/var/jsonexport"
|
JSON_EXPORT_PATH="%kernel.project_dir%/var/jsonexport"
|
||||||
|
|
||||||
SFTP_XML_EXPORT_HOST=
|
SFTP_XML_EXPORT_HOST=
|
||||||
@@ -95,11 +76,6 @@ SFTP_XML_EXPORT_PORT=
|
|||||||
SFTP_XML_EXPORT_USER=
|
SFTP_XML_EXPORT_USER=
|
||||||
SFTP_XML_EXPORT_PASSWORD=
|
SFTP_XML_EXPORT_PASSWORD=
|
||||||
|
|
||||||
SFTP_XML_EXPORT_CONTINGENTS_HOST=
|
|
||||||
SFTP_XML_EXPORT_CONTINGENTS_PORT=
|
|
||||||
SFTP_XML_EXPORT_CONTINGENTS_USER=
|
|
||||||
SFTP_XML_EXPORT_CONTINGENTS_PASSWORD=
|
|
||||||
|
|
||||||
###> league/oauth2-server-bundle ###
|
###> league/oauth2-server-bundle ###
|
||||||
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/secret/private.key
|
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/secret/private.key
|
||||||
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/secret/public.key
|
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/secret/public.key
|
||||||
|
|||||||
@@ -4,4 +4,3 @@ APP_SECRET='$ecretf0rt3st'
|
|||||||
SYMFONY_DEPRECATIONS_HELPER=999999
|
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||||
PANTHER_APP_ENV=panther
|
PANTHER_APP_ENV=panther
|
||||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||||
MAILJET_WEBHOOK_BASIC_PASSWORD=secret
|
|
||||||
@@ -33,7 +33,3 @@ yarn-error.log
|
|||||||
###< friendsofphp/php-cs-fixer ###
|
###< friendsofphp/php-cs-fixer ###
|
||||||
|
|
||||||
/ray.php
|
/ray.php
|
||||||
|
|
||||||
###> phpstan/phpstan ###
|
|
||||||
phpstan.neon
|
|
||||||
###< phpstan/phpstan ###
|
|
||||||
@@ -60,7 +60,7 @@ Authorization: Bearer {{$auth.token("oauth2_api")}}
|
|||||||
|
|
||||||
### API travel
|
### API travel
|
||||||
# @no-cookie-jar
|
# @no-cookie-jar
|
||||||
GET {{base_url}}/api/travels/DPWMP140326
|
GET {{base_url}}/api/travels/SBW131225
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|
||||||
@@ -100,27 +100,6 @@ Authorization: Bearer {{$auth.token("oauth2_api")}}
|
|||||||
"phone": "{{$random.phoneNumber.cellPhone}}"
|
"phone": "{{$random.phoneNumber.cellPhone}}"
|
||||||
}
|
}
|
||||||
|
|
||||||
### API newsletter lists map
|
|
||||||
# @no-cookie-jar
|
|
||||||
GET {{base_url}}/api/newsletters
|
|
||||||
Accept: application/json
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer {{$auth.token("oauth2_newsletter")}}
|
|
||||||
|
|
||||||
### API newsletter subscriptions
|
|
||||||
# @no-cookie-jar
|
|
||||||
POST {{base_url}}/api/newsletter-subscriptions
|
|
||||||
Accept: application/json
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer {{$auth.token("oauth2_newsletter")}}
|
|
||||||
|
|
||||||
{
|
|
||||||
"email": "{{$random.email}}",
|
|
||||||
"firstName": "{{$random.address.firstName}}",
|
|
||||||
"lastName": "{{$random.address.lastName}}",
|
|
||||||
"listIds": [10321569,10321382,10321383]
|
|
||||||
}
|
|
||||||
|
|
||||||
### API pickups planning webhook
|
### API pickups planning webhook
|
||||||
# @no-cookie-jar
|
# @no-cookie-jar
|
||||||
POST {{base_url}}/api/pickups-planning
|
POST {{base_url}}/api/pickups-planning
|
||||||
|
|||||||
@@ -13,12 +13,3 @@ htmx.config.allowScriptTags = false
|
|||||||
htmx.config.withCredentials = true
|
htmx.config.withCredentials = true
|
||||||
htmx.config.selfRequestsOnly = false
|
htmx.config.selfRequestsOnly = false
|
||||||
htmx.config.timeout = 50000 // 50 seconds - slightly higher than backend timeout (45s)
|
htmx.config.timeout = 50000 // 50 seconds - slightly higher than backend timeout (45s)
|
||||||
|
|
||||||
document.body.addEventListener('htmx:beforeSwap', (event) => {
|
|
||||||
// Symfony renders invalid forms with 422; let HTMX swap those responses so inline
|
|
||||||
// validation errors are actually shown to the user.
|
|
||||||
if (event.detail?.xhr?.status === 422) {
|
|
||||||
event.detail.shouldSwap = true
|
|
||||||
event.detail.isError = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Controller } from '@hotwired/stimulus'
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static outlets = [ 'mobilenav' ]
|
|
||||||
static classes = [ 'closed' ]
|
|
||||||
|
|
||||||
trigger({ params: { mode } }) {
|
|
||||||
this.mobilenavOutlet.toggle(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle(mode) {
|
|
||||||
// Add animation classes only now to avoid fouc
|
|
||||||
this.element.classList.add('transition-transform', 'duration-300')
|
|
||||||
this.element.classList.toggle(this.closedClass, 'close' === mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Controller } from '@hotwired/stimulus'
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
|
|
||||||
static targets = [ 'field' ]
|
|
||||||
|
|
||||||
toggle() {
|
|
||||||
let type = this.fieldTarget.getAttribute('type') === 'password' ? 'text' : 'password'
|
|
||||||
this.fieldTarget.setAttribute('type', type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Controller } from '@hotwired/stimulus'
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
|
|
||||||
static classes = ['closed', 'iconClosed', 'iconExpanded']
|
|
||||||
static targets = ['submenu', 'button', 'icon']
|
|
||||||
static values = { expanded: Boolean }
|
|
||||||
|
|
||||||
toggleSubmenu() {
|
|
||||||
this.expandedValue = !this.expandedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
expandedValueChanged(expanded) {
|
|
||||||
this.submenuTarget.classList.toggle(this.closedClass, false === expanded)
|
|
||||||
if (false === this.hasButtonTarget) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.buttonTarget.setAttribute('aria-expanded', expanded)
|
|
||||||
if (false === this.hasIconTarget) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (true === expanded) {
|
|
||||||
this.iconTarget.classList.add(...this.iconExpandedClasses)
|
|
||||||
this.iconTarget.classList.remove(...this.iconClosedClasses)
|
|
||||||
} else {
|
|
||||||
this.iconTarget.classList.add(...this.iconClosedClasses)
|
|
||||||
this.iconTarget.classList.remove(...this.iconExpandedClasses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<symbol id="icon-table" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125m17.25-3.75h-7.5c-.621 0-1.125.504-1.125 1.125m8.625-1.125c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125M12 10.875v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 10.875c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125M13.125 12h7.5m-7.5 0c-.621 0-1.125.504-1.125 1.125M20.625 12c.621 0 1.125.504 1.125 1.125v1.5c0 .621-.504 1.125-1.125 1.125m-17.25 0h7.5M12 14.625v-1.5m0 1.5c0 .621-.504 1.125-1.125 1.125M12 14.625c0 .621.504 1.125 1.125 1.125m-2.25 0c.621 0 1.125.504 1.125 1.125m0 1.5v-1.5m0 0c0-.621.504-1.125 1.125-1.125m0 0h7.5" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-dots" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-feedback" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-login" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-logout" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-alert" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-close" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-back" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-list" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-menu" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-edit" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-plus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-minus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-search" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-user" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-users" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-delete" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-check" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-upload" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-download" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-sort" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-arrow-left" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-arrow-right" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-document" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-filter" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-refresh" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-settings" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-at" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" d="M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 10-2.636 6.364M16.5 12V8.25" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-mail" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-house" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205l3 1m1.5.5l-1.5-.5M6.75 7.364V3h-3v18m3-13.636l10.5-3.819" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-phone" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-mobile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-calendar" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-hand" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.05 4.575a1.575 1.575 0 10-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 013.15 0v1.5m-3.15 0l.075 5.925m3.075.75V4.575m0 0a1.575 1.575 0 013.15 0V15M6.9 7.575a1.575 1.575 0 10-3.15 0v8.175a6.75 6.75 0 006.75 6.75h2.018a5.25 5.25 0 003.712-1.538l1.732-1.732a5.25 5.25 0 001.538-3.712l.003-2.024a.668.668 0 01.198-.471 1.575 1.575 0 10-2.228-2.228 3.818 3.818 0 00-1.12 2.687M6.9 7.575V12m6.27 4.318A4.49 4.49 0 0116.35 15m.002 0h-.002" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-copy" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-photo" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-print" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
||||||
<path d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-locked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-unlocked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-external-link" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-cookie" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M21.1634 10.9711C21.5132 10.8204 21.9579 11.0073 21.9811 11.3875C22.1494 14.1467 21.1794 16.9625 19.071 19.0709C15.1657 22.976 8.83405 22.9761 4.92886 19.0709C1.02367 15.1657 1.02377 8.83405 4.92885 4.9288C7.03704 2.82061 9.8525 1.85015 12.6115 2.01806C12.9916 2.04119 13.1787 2.48598 13.028 2.83576C12.4774 4.11367 12.7239 5.65275 13.7677 6.69657C14.3307 7.25963 15.038 7.58971 15.7706 7.68848C16.0483 7.72594 16.2738 7.95144 16.3113 8.2292C16.4101 8.96178 16.7402 9.66905 17.3032 10.2321C18.347 11.2759 19.8857 11.5218 21.1634 10.9711ZM19.7845 13.8397C19.8558 13.5362 19.6123 13.2603 19.3017 13.2337C18.0569 13.1271 16.8417 12.599 15.889 11.6463C15.3166 11.0739 14.8979 10.4061 14.6309 9.69455C14.5744 9.54422 14.4556 9.42534 14.3052 9.36892C13.5936 9.10186 12.9259 8.68322 12.3535 8.11078C11.4005 7.15778 10.8715 5.94208 10.7648 4.69683C10.7381 4.38618 10.4621 4.14264 10.1586 4.2141C8.76039 4.54327 7.43341 5.25267 6.34306 6.34301C3.21903 9.46721 3.21893 14.5326 6.34307 17.6567C9.46721 20.7809 14.5326 20.7808 17.6568 17.6567C18.7474 16.566 19.4557 15.2384 19.7845 13.8397ZM9.8786 14.1212C10.4644 14.707 10.4644 15.6567 9.8786 16.2425C9.29281 16.8283 8.34307 16.8283 7.75728 16.2425C7.17149 15.6567 7.17149 14.707 7.75728 14.1212C8.34306 13.5354 9.29281 13.5354 9.8786 14.1212ZM10.2321 8.11078C10.8179 8.69657 10.8179 9.64631 10.2321 10.2321C9.64636 10.8179 8.69661 10.8179 8.11083 10.2321C7.52504 9.64631 7.52504 8.69656 8.11083 8.11078C8.69661 7.52499 9.64636 7.52499 10.2321 8.11078ZM15.889 13.7676C16.4748 14.3534 16.4748 15.3032 15.889 15.889C15.3032 16.4747 14.3535 16.4747 13.7677 15.889C13.1819 15.3032 13.1819 14.3534 13.7677 13.7676C14.3535 13.1818 15.3032 13.1818 15.889 13.7676Z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-mask" fill="currentColor" stroke="none" viewBox="0 0 576 512">
|
|
||||||
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
||||||
<path d="M288 64C39.52 64 0 182.1 0 273.5C0 379.5 78.8 448 176 448c27.33 0 51.21-6.516 66.11-36.79l19.93-40.5C268.3 358.6 278.1 352.4 288 352.1c9.9 .3711 19.7 6.501 25.97 18.63l19.93 40.5C348.8 441.5 372.7 448 400 448c97.2 0 176-68.51 176-174.5C576 182.1 536.5 64 288 64zM400 400c-18.12 0-19.56-2.924-23.04-9.986l-20.35-41.33c-13.87-26.86-38.85-43.53-66.82-44.57L288 304L286.2 304.1c-27.96 1.049-52.94 17.71-67.24 45.41l-19.93 40.49C195.6 397.1 194.1 400 176 400c-75.36 0-128-52.04-128-126.5C48 229.3 48 112 288 112s240 117.3 240 161.5C528 347.1 475.4 400 400 400zM160 192C124.7 192 96 220.7 96 256s28.65 64 64 64c35.35 0 64-28.65 64-64S195.3 192 160 192zM416 192c-35.35 0-64 28.65-64 64s28.65 64 64 64c35.35 0 64-28.65 64-64S451.3 192 416 192z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-excel" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
|
||||||
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
||||||
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM229.1 233.3L192 280.9L154.9 233.3C146.8 222.8 131.8 220.9 121.3 229.1C110.8 237.2 108.9 252.3 117.1 262.8L161.6 320l-44.53 57.25c-8.156 10.47-6.25 25.56 4.188 33.69C125.7 414.3 130.8 416 135.1 416c7.156 0 14.25-3.188 18.97-9.25L192 359.1l37.06 47.65C233.8 412.8 240.9 416 248 416c5.125 0 10.31-1.656 14.72-5.062c10.44-8.125 12.34-23.22 4.188-33.69L222.4 320l44.53-57.25c8.156-10.47 6.25-25.56-4.188-33.69C252.2 220.9 237.2 222.8 229.1 233.3z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-pdf" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
|
||||||
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
||||||
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM202 286.1c.877-2.688 1.74-5.398 2.582-8.145c1.434-5.762 7.488-31.54 7.488-52.47C212.1 207 197.1 192 178.6 192C160.1 192 145.1 207 145.1 225.5c0 .2969 .1641 28.81 13.85 62.3c-7.035 19.36-15.57 38.8-25.41 57.93c-21.49 10.11-39.24 22.23-52.8 36.07c-6.234 6.438-9.367 14.74-9.367 24.72c0 18.45 15.01 33.46 33.46 33.46c10.8 0 20.98-5.227 27.22-13.98c7.322-10.28 18.38-26.9 30.47-48.95c15.8-6.352 33.88-11.72 53.88-16c13.55 9.578 28.9 17.29 45.71 22.95c4.527 1.551 9.402 2.348 14.43 2.348c20.26 0 36.13-16.19 36.13-36.86c0-20.33-16.54-36.87-36.87-36.87h-3.705c-2.727 .125-20.51 1.141-45.37 5.367C216.9 308.9 208.6 298.3 202 286.1zM110.2 410.4c-3.273 4.688-12.03 2.777-12.03-5.312c0-1.754 .6289-3.43 1.729-4.555c9.02-9.219 19.94-17.05 31.85-23.72C122.3 393.1 114.3 404.7 110.2 410.4zM178.6 218.8c3.693 0 6.703 3.008 6.703 6.703c0 15.21-4.109 34.84-5.746 42.1C172.1 245 171.9 227.2 171.9 225.5C171.9 221.8 174.9 218.8 178.6 218.8zM162.3 348.3c6.611-13.48 13.22-28.46 19.38-44.7c6.389 10.92 14.56 21.86 24.96 31.97C192.6 338.8 177.4 342.9 162.3 348.3zM272.4 339.5h3.352c5.539 0 10.05 4.5 10.05 10.79c0 5.129-4.176 9.32-9.32 9.32c-2.029 0-4.059-.3164-5.852-.9414c-12.33-4.137-23.11-9.32-32.54-15.19C258.3 340.3 272.1 339.5 272.4 339.5z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-file" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
|
||||||
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
||||||
<path d="M365.3 93.38l-74.63-74.64C278.6 6.743 262.3 0 245.4 0L64-.0001c-35.35 0-64 28.65-64 64l.0065 384c0 35.35 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM320 464H64.02c-8.836 0-15.1-7.163-16-15.1L48 64.13c-.0004-8.837 7.163-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1v288C336 456.8 328.8 464 320 464z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-sigma" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
|
||||||
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
||||||
<path d="M335.1 136l.0005-56H83.59l165 158.7C253.3 243.2 255.1 249.5 255.1 256s-2.656 12.78-7.375 17.31L83.59 432h252.4l-.0005-56c0-13.25 10.75-24 24-24C373.2 352 384 362.8 384 376v80c0 13.25-10.75 24-24 24H23.99c-9.782 0-18.59-5.938-22.25-15.03s-1.438-19.47 5.625-26.28L197.4 256L7.364 73.31C.3015 66.5-1.917 56.13 1.739 47.03S14.21 32 23.99 32h336C373.2 32 384 42.75 384 56v80C384 149.3 373.2 160 359.1 160C346.7 160 335.1 149.3 335.1 136z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-percent" fill="currentColor" stroke="none" viewBox="0 0 384 512">
|
|
||||||
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
|
||||||
<path d="M376.1 71.03c-9.375-9.375-24.56-9.375-33.94 0l-336 336c-9.375 9.375-9.375 24.56 0 33.94C11.72 445.7 17.84 448 24 448s12.28-2.344 16.97-7.031l336-336C386.3 95.59 386.3 80.41 376.1 71.03zM64 176c26.51 0 48-21.49 48-48S90.51 80 64 80C37.49 80 16 101.5 16 128S37.49 176 64 176zM320 336c-26.51 0-48 21.49-48 48s21.49 48 48 48c26.51 0 48-21.49 48-48S346.5 336 320 336z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-chair" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
|
|
||||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M288 16c26.5 0 48 21.5 48 48V256h16V64c0-35.3-28.7-64-64-64H160C124.7 0 96 28.7 96 64V256h16V64c0-26.5 21.5-48 48-48H288zM48 168c0-4.4-3.6-8-8-8s-8 3.6-8 8v80c0 4.4 3.6 8 8 8s8-3.6 8-8V168zm368 0c0-4.4-3.6-8-8-8s-8 3.6-8 8v80c0 4.4 3.6 8 8 8s8-3.6 8-8V168zM80 304H368c6.1 0 11.6 3.4 14.3 8.8l16 32c2.5 5 2.2 10.9-.7 15.6s-8.1 7.6-13.6 7.6H224 64c-5.5 0-10.7-2.9-13.6-7.6s-3.2-10.6-.7-15.6l16-32c2.7-5.4 8.3-8.8 14.3-8.8zm152 80H384c11.1 0 21.4-5.7 27.2-15.2s6.4-21.2 1.4-31.1l-16-32C391.2 294.8 380.1 288 368 288H80c-12.1 0-23.2 6.8-28.6 17.7l-16 32c-5 9.9-4.4 21.7 1.4 31.1S52.9 384 64 384H216v64H184c-30.9 0-56 25.1-56 56c0 4.4 3.6 8 8 8s8-3.6 8-8c0-22.1 17.9-40 40-40h32v40c0 4.4 3.6 8 8 8s8-3.6 8-8V464h32c22.1 0 40 17.9 40 40c0 4.4 3.6 8 8 8s8-3.6 8-8c0-30.9-25.1-56-56-56H232V384z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-eye" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-profile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-chart" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-info" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-star" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-cancel" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-bell" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-folder" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
||||||
<path d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-power" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-no-symbol" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-bus" xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24" stroke="currentColor" fill="none">
|
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
<path d="M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
|
||||||
<path d="M18 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
|
||||||
<path d="M4 17h-2v-11a1 1 0 0 1 1 -1h14a5 7 0 0 1 5 7v5h-2m-4 0h-8" />
|
|
||||||
<path d="M16 5l1.5 7l4.5 0" />
|
|
||||||
<path d="M2 10l15 0" />
|
|
||||||
<path d="M7 5l0 5" />
|
|
||||||
<path d="M12 5l0 5" />
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-ski" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
|
||||||
<path d="M380.7 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM2.7 268.9c6.1-11.8 20.6-16.3 32.4-10.2L239.8 365l49.6-74.5-82.7-87.9c-9.5-10.1-14.4-22.4-15.1-34.9l87.7 42 49.4 52.5c12.8 13.6 14.5 34.1 4.2 49.6l-50.2 75.4 135.8 70.5c13.6 7.1 29.8 7.2 43.6 .3l15.2-7.6c11.9-5.9 26.3-1.1 32.2 10.7s1.1 26.3-10.7 32.2l-15.2 7.6c-27.5 13.7-59.9 13.5-87.2-.7L12.9 301.3C1.2 295.2-3.4 280.7 2.7 268.9zM118.9 65.6L137 74.2l8.7-17.4c4-7.9 13.6-11.1 21.5-7.2s11.1 13.6 7.2 21.5l-8.5 16.9 55.5 26.6c1.7-.9 3.5-1.7 5.4-2.5l80.9-32.4c32.4-13 68.6 6.5 75.6 40.7l12.3 59.7 62.9 30.1c12 5.7 17 20 11.3 32s-20 17-32 11.3l-54.2-25.9c-1-.3-1.9-.6-2.8-1l-229.1-110-9.2 18.4c-4 7.9-13.6 11.1-21.5 7.2s-11.1-13.6-7.2-21.5l9-18-17.6-8.4c-8-3.8-11.3-13.4-7.5-21.3s13.4-11.3 21.3-7.5zm217.3 64.7c-1-4.9-6.2-7.7-10.8-5.8l-45.7 18.3 65.5 31.5-9-43.9z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-snowboard" fill="currentColor" stroke="nonw" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
||||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
|
||||||
<path d="M204.3 3c11.6-6.4 26.2-2.3 32.6 9.3l31.9 57.4c1.3 2.3 3.6 3.8 6.2 4.1l33.8 3.4c24.1 2.4 46.9 12 65.4 27.6L503.5 213.6c10.1 8.5 11.4 23.7 2.9 33.8s-23.7 11.4-33.8 2.9l-68.2-57.5-82.3 43.9 24.7 18.9c24.9 19 34.6 51.9 24.1 81.4l-28.2 79c-1.4 3.8-3.6 7-6.3 9.6l93.3 35.7c4.6 1.8 9.4 2.6 14.3 2.6H472c13.3 0 24 10.7 24 24s-10.7 24-24 24H443.8c-10.8 0-21.4-2-31.5-5.8L60.1 371.3c-11.5-4.4-22-11.2-30.8-20L7 329c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.4 22.4c4 4 8.7 7.1 14 9.1l19.4 7.4c-2.9-11.4 2.9-23.4 14-28.1L192 272l0-68.2c0-21.2 12-40.6 31-50.1L284.4 123l-14.1-1.4c-18.3-1.8-34.5-12.5-43.4-28.5L195 35.7c-6.4-11.6-2.3-26.2 9.3-32.6zm91.8 407.2c-.3-3.4 .1-6.9 1.3-10.3l28.2-79c3.5-9.8 .3-20.8-8-27.1L240 234.4l0 42.9c0 16.1-9.7 30.7-24.6 36.9L134 348.2l162.1 62.1zM259.6 189l20.5 15.7 84.6-45.1-17.2-14.5L259.6 189zM384 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-hourglass" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
|
|
||||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
|
||||||
<path d="M24 0C10.7 0 0 10.7 0 24S10.7 48 24 48h8V67c0 40.3 16 79 44.5 107.5L158.1 256 76.5 337.5C48 366 32 404.7 32 445v19H24c-13.3 0-24 10.7-24 24s10.7 24 24 24H360c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8V445c0-40.3-16-79-44.5-107.5L225.9 256l81.5-81.5C336 146 352 107.3 352 67V48h8c13.3 0 24-10.7 24-24s-10.7-24-24-24H24zM192 289.9l81.5 81.5C293 391 304 417.4 304 445v19H80V445c0-27.6 11-54 30.5-73.5L192 289.9zm0-67.9l-81.5-81.5C91 121 80 94.6 80 67V48H304V67c0 27.6-11 54-30.5 73.5L192 222.1z"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="icon-car" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="64" cy="176" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><circle cx="192" cy="176" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="88" y1="176" x2="168" y2="176" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M216,176h24a8,8,0,0,0,8-8V128a8,8,0,0,0-8-8H208L162.34,74.34A8,8,0,0,0,156.69,72H44.28a8,8,0,0,0-6.65,3.56L8,120v48a8,8,0,0,0,8,8H40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="208" y1="120" x2="8" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></symbol>
|
|
||||||
<symbol id="icon-flag-a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#fff" d="M640 480H0V0h640z"/><path fill="#ed2939" d="M640 480H0V320h640zm0-319.9H0V.1h640z"/></g></symbol>
|
|
||||||
<symbol id="icon-flag-ch" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#d52b1e" d="M0 0h640v480H0z"/><g fill="#fff"><path d="M170 195h300v90H170z"/><path d="M275 90h90v300h-90z"/></g></g></symbol>
|
|
||||||
<symbol id="icon-flag-d" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><path fill="#ffce00" d="M0 320h640v160H0z"/><path d="M0 0h640v160H0z"/><path fill="#d00" d="M0 160h640v160H0z"/></symbol>
|
|
||||||
<symbol id="icon-flag-f" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#00267f" d="M0 0h213.3v480H0z"/><path fill="#f31830" d="M426.7 0H640v480H426.7z"/></g></symbol>
|
|
||||||
<symbol id="icon-flag-i" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#009246" d="M0 0h213.3v480H0z"/><path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/></g></symbol>
|
|
||||||
<symbol id="icon-flag-nl" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#21468b" d="M0 320h640v160H0z"/><path fill="#fff" d="M0 160h640v160H0z"/><path fill="#ae1c28" d="M0 0h640v160H0z"/></g></symbol>
|
|
||||||
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
|
||||||
<symbol id="icon-spinner" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
|
||||||
<g fill="none" fill-rule="evenodd" stroke-width="2">
|
|
||||||
<circle cx="22" cy="22" r="1">
|
|
||||||
<animate attributeName="r"
|
|
||||||
begin="0s" dur="1.8s"
|
|
||||||
values="1; 20"
|
|
||||||
calcMode="spline"
|
|
||||||
keyTimes="0; 1"
|
|
||||||
keySplines="0.165, 0.84, 0.44, 1"
|
|
||||||
repeatCount="indefinite" />
|
|
||||||
<animate attributeName="stroke-opacity"
|
|
||||||
begin="0s" dur="1.8s"
|
|
||||||
values="1; 0"
|
|
||||||
calcMode="spline"
|
|
||||||
keyTimes="0; 1"
|
|
||||||
keySplines="0.3, 0.61, 0.355, 1"
|
|
||||||
repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
<circle cx="22" cy="22" r="1">
|
|
||||||
<animate attributeName="r"
|
|
||||||
begin="-0.9s" dur="1.8s"
|
|
||||||
values="1; 20"
|
|
||||||
calcMode="spline"
|
|
||||||
keyTimes="0; 1"
|
|
||||||
keySplines="0.165, 0.84, 0.44, 1"
|
|
||||||
repeatCount="indefinite" />
|
|
||||||
<animate attributeName="stroke-opacity"
|
|
||||||
begin="-0.9s" dur="1.8s"
|
|
||||||
values="1; 0"
|
|
||||||
calcMode="spline"
|
|
||||||
keyTimes="0; 1"
|
|
||||||
keySplines="0.3, 0.61, 0.355, 1"
|
|
||||||
repeatCount="indefinite" />
|
|
||||||
</circle>
|
|
||||||
</g>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.3 KiB |
@@ -1,7 +1,6 @@
|
|||||||
@import "components/button.css";
|
|
||||||
@import "components/datatable.css";
|
|
||||||
@import "components/forms.css";
|
|
||||||
@import "components/pagination.css";
|
|
||||||
@import "components/toast.css";
|
|
||||||
@import "components/tooltip.css";
|
|
||||||
@import "components/typography.css";
|
@import "components/typography.css";
|
||||||
|
@import "components/forms.css";
|
||||||
|
@import "components/button.css";
|
||||||
|
@import "components/tooltip.css";
|
||||||
|
@import "components/toast.css";
|
||||||
|
@import "components/pagination.css";
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.button {
|
.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 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 text-sm md:text-base;
|
@apply uppercase leading-none text-center;
|
||||||
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-dark;
|
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-light;
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,52 +20,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button--secondary {
|
.button--secondary {
|
||||||
@apply bg-none bg-primary-light text-white;
|
@apply bg-none bg-primary-dark text-white;
|
||||||
@apply hover:bg-primary-light/50;
|
@apply hover:bg-primary-light/80;
|
||||||
}
|
|
||||||
|
|
||||||
.button--neutral {
|
|
||||||
@apply bg-none bg-gray-200;
|
|
||||||
@apply hover:bg-gray-200/50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button--warning {
|
|
||||||
@apply bg-none bg-red-500 text-white;
|
|
||||||
@apply hover:bg-red-500/50;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sbw {
|
.sbw {
|
||||||
.button--primary {
|
.button--primary {
|
||||||
@apply bg-none bg-sbw-secondary text-white;
|
@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 {
|
.button--secondary {
|
||||||
@apply bg-none bg-sbw-primary-dark text-white;
|
@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 {
|
.snz {
|
||||||
.button--primary {
|
.button--primary {
|
||||||
@apply bg-none bg-snz-secondary text-white;
|
@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 {
|
.button--secondary {
|
||||||
@apply bg-none bg-snz-primary-dark text-white;
|
@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 {
|
.ser {
|
||||||
.button--primary {
|
.button--primary {
|
||||||
@apply bg-none bg-ser-secondary text-white;
|
@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 {
|
.button--secondary {
|
||||||
@apply bg-none bg-ser-primary text-white;
|
@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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
.data-table-wrapper {
|
|
||||||
@apply overflow-x-auto lg:overflow-x-visible mb-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table-wrapper__inner {
|
|
||||||
@apply align-middle inline-block min-w-full overflow-hidden lg:overflow-visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table {
|
|
||||||
@apply min-w-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tbody {
|
|
||||||
@apply bg-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tr {
|
|
||||||
@apply border border-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table th {
|
|
||||||
@apply px-4 py-2 bg-gray-200 text-left leading-4 font-bold text-primary align-top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table td {
|
|
||||||
@apply px-4 py-2 leading-5 align-top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tbody tr {
|
|
||||||
@apply hover:bg-gray-100;
|
|
||||||
}
|
|
||||||
+3
-10
@@ -14,10 +14,8 @@
|
|||||||
"doctrine/doctrine-bundle": "^2.13",
|
"doctrine/doctrine-bundle": "^2.13",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.3",
|
"doctrine/doctrine-migrations-bundle": "^3.3",
|
||||||
"doctrine/orm": "^3.3",
|
"doctrine/orm": "^3.3",
|
||||||
|
"easycorp/easyadmin-bundle": "^4.27",
|
||||||
"fakerphp/faker": "^1.24",
|
"fakerphp/faker": "^1.24",
|
||||||
"flagception/flagception-bundle": "^6.1",
|
|
||||||
"knplabs/knp-menu-bundle": "^3.7",
|
|
||||||
"knplabs/knp-paginator-bundle": "^6.10",
|
|
||||||
"league/flysystem-bundle": "^3.4",
|
"league/flysystem-bundle": "^3.4",
|
||||||
"league/flysystem-sftp-v3": "^3.29",
|
"league/flysystem-sftp-v3": "^3.29",
|
||||||
"league/oauth2-server-bundle": "^1.0",
|
"league/oauth2-server-bundle": "^1.0",
|
||||||
@@ -63,7 +61,6 @@
|
|||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/html-extra": "^3.15",
|
"twig/html-extra": "^3.15",
|
||||||
"twig/intl-extra": "*",
|
"twig/intl-extra": "*",
|
||||||
"twig/string-extra": "^3.24",
|
|
||||||
"twig/twig": "^2.12|^3.0",
|
"twig/twig": "^2.12|^3.0",
|
||||||
"zenstruck/schedule-bundle": "^1.8"
|
"zenstruck/schedule-bundle": "^1.8"
|
||||||
},
|
},
|
||||||
@@ -98,8 +95,6 @@
|
|||||||
"symfony/polyfill-php81": "*"
|
"symfony/polyfill-php81": "*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"rector": "vendor/bin/rector process",
|
|
||||||
"rector:dry-run": "vendor/bin/rector process --dry-run",
|
|
||||||
"auto-scripts": {
|
"auto-scripts": {
|
||||||
"cache:clear": "symfony-cmd",
|
"cache:clear": "symfony-cmd",
|
||||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||||
@@ -124,11 +119,9 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"deployer/deployer": "^7.5",
|
"deployer/deployer": "^7.5",
|
||||||
"friendsofphp/php-cs-fixer": "^3.75",
|
"friendsofphp/php-cs-fixer": "^3.75",
|
||||||
"phpstan/phpstan": "^2.1",
|
|
||||||
"phpstan/phpstan-doctrine": "^2.0",
|
|
||||||
"phpstan/phpstan-symfony": "^2.0",
|
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"rector/rector": "^2.4",
|
"spatie/ray": "^1.43",
|
||||||
|
"spatie/x-ray": "^1.2",
|
||||||
"symfony/browser-kit": "6.4.*",
|
"symfony/browser-kit": "6.4.*",
|
||||||
"symfony/css-selector": "6.4.*",
|
"symfony/css-selector": "6.4.*",
|
||||||
"symfony/debug-bundle": "6.4.*",
|
"symfony/debug-bundle": "6.4.*",
|
||||||
|
|||||||
Generated
+1448
-1434
File diff suppressed because it is too large.
Load diff
+2
-3
@@ -16,7 +16,6 @@ return [
|
|||||||
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
||||||
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
||||||
Zenstruck\ScheduleBundle\ZenstruckScheduleBundle::class => ['all' => true],
|
Zenstruck\ScheduleBundle\ZenstruckScheduleBundle::class => ['all' => true],
|
||||||
Flagception\Bundle\FlagceptionBundle\FlagceptionBundle::class => ['all' => true],
|
Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
|
||||||
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
|
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
|
||||||
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
|
|
||||||
];
|
];
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
flagception:
|
|
||||||
features: {}
|
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
flysystem:
|
flysystem:
|
||||||
storages:
|
storages:
|
||||||
xml_export.storage:
|
xml_export.storage:
|
||||||
local:
|
adapter: 'local'
|
||||||
|
options:
|
||||||
directory: '%env(resolve:XML_EXPORT_PATH)%'
|
directory: '%env(resolve:XML_EXPORT_PATH)%'
|
||||||
xml_source.storage:
|
xml_source.storage:
|
||||||
sftp:
|
adapter: 'sftp'
|
||||||
|
options:
|
||||||
host: '%env(SFTP_XML_EXPORT_HOST)%'
|
host: '%env(SFTP_XML_EXPORT_HOST)%'
|
||||||
port: '%env(int:SFTP_XML_EXPORT_PORT)%'
|
port: '%env(int:SFTP_XML_EXPORT_PORT)%'
|
||||||
username: '%env(SFTP_XML_EXPORT_USER)%'
|
username: '%env(SFTP_XML_EXPORT_USER)%'
|
||||||
password: '%env(SFTP_XML_EXPORT_PASSWORD)%'
|
password: '%env(SFTP_XML_EXPORT_PASSWORD)%'
|
||||||
xml_dump.storage:
|
xml_dump.storage:
|
||||||
local:
|
adapter: 'local'
|
||||||
|
options:
|
||||||
directory: '%kernel.project_dir%/var/bpn'
|
directory: '%kernel.project_dir%/var/bpn'
|
||||||
json_export.storage:
|
json_export.storage:
|
||||||
local:
|
adapter: 'local'
|
||||||
|
options:
|
||||||
directory: '%env(resolve:JSON_EXPORT_PATH)%'
|
directory: '%env(resolve:JSON_EXPORT_PATH)%'
|
||||||
@@ -9,8 +9,9 @@ framework:
|
|||||||
# Remove or comment this section to explicitly disable session support.
|
# Remove or comment this section to explicitly disable session support.
|
||||||
session:
|
session:
|
||||||
handler_id: null
|
handler_id: null
|
||||||
cookie_secure: true
|
cookie_secure: auto
|
||||||
cookie_samesite: lax
|
# cookie_samesite: lax
|
||||||
|
cookie_samesite: none # this is to make login work in staging environment. to be removed when going live!
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
knp_menu:
|
|
||||||
twig:
|
|
||||||
template: _partials/_menu.html.twig
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
knp_paginator:
|
|
||||||
default_options:
|
|
||||||
page_name: page
|
|
||||||
sort_field_name: sort
|
|
||||||
sort_direction_name: direction
|
|
||||||
distinct: true
|
|
||||||
filter_field_name: filterField
|
|
||||||
filter_value_name: filterValue
|
|
||||||
template:
|
|
||||||
pagination: '_partials/_paginator_sliding.html.twig'
|
|
||||||
sortable: '_partials/_paginator_sortable_link.html.twig'
|
|
||||||
@@ -3,10 +3,9 @@ league_oauth2_server:
|
|||||||
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
|
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
|
||||||
private_key_passphrase: null
|
private_key_passphrase: null
|
||||||
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
|
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
|
||||||
require_code_challenge_for_public_clients: true
|
|
||||||
enable_client_credentials_grant: true
|
enable_client_credentials_grant: true
|
||||||
enable_auth_code_grant: true
|
enable_auth_code_grant: true
|
||||||
enable_refresh_token_grant: true
|
enable_refresh_token_grant: false
|
||||||
enable_password_grant: false
|
enable_password_grant: false
|
||||||
access_token_ttl: PT10M
|
access_token_ttl: PT10M
|
||||||
resource_server:
|
resource_server:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ framework:
|
|||||||
max_retries: 3
|
max_retries: 3
|
||||||
multiplier: 2
|
multiplier: 2
|
||||||
failed: 'doctrine://default?queue_name=failed'
|
failed: 'doctrine://default?queue_name=failed'
|
||||||
sync: 'sync://'
|
# sync: 'sync://'
|
||||||
|
|
||||||
default_bus: messenger.bus.default
|
default_bus: messenger.bus.default
|
||||||
|
|
||||||
@@ -21,10 +21,9 @@ framework:
|
|||||||
messenger.bus.default: []
|
messenger.bus.default: []
|
||||||
|
|
||||||
routing:
|
routing:
|
||||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: sync
|
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
|
||||||
Symfony\Component\Notifier\Message\ChatMessage: async
|
Symfony\Component\Notifier\Message\ChatMessage: async
|
||||||
Symfony\Component\Notifier\Message\SmsMessage: async
|
Symfony\Component\Notifier\Message\SmsMessage: async
|
||||||
App\Message\MailjetNewsletterEventMessage: async
|
|
||||||
|
|
||||||
# Route your messages to the transports
|
# Route your messages to the transports
|
||||||
# 'App\Message\YourMessage': async
|
# 'App\Message\YourMessage': async
|
||||||
@@ -4,7 +4,6 @@ monolog:
|
|||||||
- bpn
|
- bpn
|
||||||
- core
|
- core
|
||||||
- auth
|
- auth
|
||||||
- admin
|
|
||||||
|
|
||||||
when@dev:
|
when@dev:
|
||||||
monolog:
|
monolog:
|
||||||
@@ -28,16 +27,11 @@ when@dev:
|
|||||||
path: "%kernel.logs_dir%/%kernel.environment%.core.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.core.log"
|
||||||
level: debug
|
level: debug
|
||||||
channels: ["core"]
|
channels: ["core"]
|
||||||
admin:
|
|
||||||
type: stream
|
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.admin.log"
|
|
||||||
level: debug
|
|
||||||
channels: ["admin"]
|
|
||||||
main:
|
main:
|
||||||
type: stream
|
type: stream
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
level: debug
|
level: debug
|
||||||
channels: ["!event", "!bpn", "!core", "!auth", "!admin"]
|
channels: ["!event", "!bpn", "!core", "!auth"]
|
||||||
# uncomment to get logging in your browser
|
# uncomment to get logging in your browser
|
||||||
# you may have to allow bigger header sizes in your Web server configuration
|
# you may have to allow bigger header sizes in your Web server configuration
|
||||||
#firephp:
|
#firephp:
|
||||||
@@ -65,7 +59,7 @@ when@test:
|
|||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
level: debug
|
level: debug
|
||||||
|
|
||||||
when@prod: &prod_config
|
when@prod:
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
database:
|
database:
|
||||||
@@ -90,19 +84,13 @@ when@prod: &prod_config
|
|||||||
path: "%kernel.logs_dir%/%kernel.environment%.core.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.core.log"
|
||||||
level: info
|
level: info
|
||||||
channels: ["core"]
|
channels: ["core"]
|
||||||
admin:
|
|
||||||
type: rotating_file
|
|
||||||
max_files: 7
|
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.admin.log"
|
|
||||||
level: info
|
|
||||||
channels: ["admin"]
|
|
||||||
main:
|
main:
|
||||||
type: fingers_crossed
|
type: fingers_crossed
|
||||||
action_level: error
|
action_level: error
|
||||||
handler: nested
|
handler: nested
|
||||||
excluded_http_codes: [404, 405]
|
excluded_http_codes: [404, 405]
|
||||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||||
channels: ["!bpn", "!core", "!auth", "!admin"]
|
channels: ["!bpn", "!core", "!auth"]
|
||||||
nested:
|
nested:
|
||||||
type: rotating_file
|
type: rotating_file
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.framework.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.framework.log"
|
||||||
@@ -117,5 +105,3 @@ when@prod: &prod_config
|
|||||||
channels: [deprecation]
|
channels: [deprecation]
|
||||||
path: php://stderr
|
path: php://stderr
|
||||||
formatter: monolog.formatter.json
|
formatter: monolog.formatter.json
|
||||||
|
|
||||||
when@staging: *prod_config
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
security:
|
security:
|
||||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
Symfony\Component\Security\Core\User\InMemoryUser: 'plaintext'
|
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
@@ -9,12 +8,6 @@ security:
|
|||||||
entity:
|
entity:
|
||||||
class: App\Entity\User
|
class: App\Entity\User
|
||||||
property: email
|
property: email
|
||||||
mailjet_webhook_provider:
|
|
||||||
memory:
|
|
||||||
users:
|
|
||||||
mailjet:
|
|
||||||
password: '%env(MAILJET_WEBHOOK_BASIC_PASSWORD)%'
|
|
||||||
roles: ['ROLE_MAILJET_WEBHOOK']
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
@@ -26,15 +19,7 @@ security:
|
|||||||
pattern: ^/api
|
pattern: ^/api
|
||||||
security: true
|
security: true
|
||||||
stateless: true
|
stateless: true
|
||||||
provider: app_user_provider
|
|
||||||
oauth2: true
|
oauth2: true
|
||||||
mailjet_webhook:
|
|
||||||
pattern: ^/webhooks/mailjet/newsletter$
|
|
||||||
security: true
|
|
||||||
stateless: true
|
|
||||||
provider: mailjet_webhook_provider
|
|
||||||
http_basic:
|
|
||||||
realm: 'Mailjet Webhook'
|
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
@@ -47,7 +32,6 @@ security:
|
|||||||
# Easy way to control access for large sections of your site
|
# Easy way to control access for large sections of your site
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
- { path: ^/webhooks/mailjet/newsletter$, roles: ROLE_MAILJET_WEBHOOK, requires_channel: https }
|
|
||||||
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED, requires_channel: https }
|
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED, requires_channel: https }
|
||||||
- { path: ^/admin, roles: ROLE_ADMIN, requires_channel: https }
|
- { path: ^/admin, roles: ROLE_ADMIN, requires_channel: https }
|
||||||
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
|
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
|
||||||
@@ -59,7 +43,6 @@ when@test:
|
|||||||
# important to generate secure password hashes. In tests however, secure hashes
|
# important to generate secure password hashes. In tests however, secure hashes
|
||||||
# are not important, waste resources and increase test times. The following
|
# are not important, waste resources and increase test times. The following
|
||||||
# reduces the work factor to the lowest possible values.
|
# reduces the work factor to the lowest possible values.
|
||||||
Symfony\Component\Security\Core\User\InMemoryUser: 'plaintext'
|
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
algorithm: auto
|
algorithm: auto
|
||||||
cost: 4 # Lowest possible value for bcrypt
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
twig:
|
twig:
|
||||||
file_name_pattern: '*.twig'
|
file_name_pattern: '*.twig'
|
||||||
form_themes: ['forms.html.twig']
|
form_themes: ['forms.html.twig']
|
||||||
paths:
|
|
||||||
'%kernel.project_dir%/assets/images': images
|
|
||||||
globals:
|
globals:
|
||||||
htmx_change_trigger: 'change delay:100ms'
|
htmx_change_trigger: 'change delay:100ms'
|
||||||
website_base_url: '%env(APP_WEBSITE_BASE_URL)%'
|
website_base_url: '%env(APP_WEBSITE_BASE_URL)%'
|
||||||
@@ -10,7 +8,6 @@ twig:
|
|||||||
legal_notice_url: '%env(APP_LEGAL_NOTICE_URL)%'
|
legal_notice_url: '%env(APP_LEGAL_NOTICE_URL)%'
|
||||||
terms_and_conditions_url: '%env(APP_TERMS_AND_CONDITIONS_URL)%'
|
terms_and_conditions_url: '%env(APP_TERMS_AND_CONDITIONS_URL)%'
|
||||||
privacy_policy_url: '%env(APP_PRIVACY_POLICY_URL)%'
|
privacy_policy_url: '%env(APP_PRIVACY_POLICY_URL)%'
|
||||||
insurance_revocation_url: '%env(APP_INSURANCE_REVOCATION_URL)%'
|
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
twig:
|
twig:
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
twig_component:
|
||||||
|
anonymous_template_directory: 'components/'
|
||||||
|
defaults:
|
||||||
|
# Namespace & directory for components
|
||||||
|
App\Twig\Components\: 'components/'
|
||||||
@@ -24,22 +24,6 @@ zenstruck_schedule:
|
|||||||
frequency: "30 1 * * *"
|
frequency: "30 1 * * *"
|
||||||
description: "Removes expired drafts"
|
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:
|
when@staging:
|
||||||
zenstruck_schedule:
|
zenstruck_schedule:
|
||||||
mailer:
|
mailer:
|
||||||
@@ -53,8 +37,8 @@ when@prod:
|
|||||||
tasks:
|
tasks:
|
||||||
- task: app:bpn:xml-sync
|
- task: app:bpn:xml-sync
|
||||||
frequency: "*/15 8-19 * * *"
|
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
|
- task: app:bpn:xml-sync
|
||||||
frequency: "0 20-23,0-7 * * *"
|
frequency: "0 20-23,0-7 * * *"
|
||||||
description: "Syncs BusPro XML data hourly outside peak hours"
|
description: "Sync BusPro XML data hourly outside peak hours"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
easyadmin:
|
||||||
|
resource: .
|
||||||
|
type: easyadmin.routes
|
||||||
+27
-118
@@ -9,39 +9,26 @@ parameters:
|
|||||||
path_to_keys: '%kernel.project_dir%/config/secret'
|
path_to_keys: '%kernel.project_dir%/config/secret'
|
||||||
bpn_debug: '%env(APP_BPN_DEBUG)%'
|
bpn_debug: '%env(APP_BPN_DEBUG)%'
|
||||||
default_booking_status: '%env(DEFAULT_BOOKING_STATUS)%'
|
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)%'
|
|
||||||
|
|
||||||
# MailJet list ids and their labels
|
# Body dimensions choices for BodyDimensionsType
|
||||||
mailjet_lists:
|
body_dimensions.height_choices:
|
||||||
10321569: 'E&P Newsletter'
|
'bis 148cm': '-148'
|
||||||
10321382: 'Reisen-Alert Ahrntal'
|
'149 - 157cm': '149-157'
|
||||||
10321383: 'Reisen-Alert Davos'
|
'158 - 166cm': '158-166'
|
||||||
10321391: 'Reisen-Alert Familienreisen'
|
'167 - 178cm': '167-178'
|
||||||
10321384: 'Reisen-Alert Arosa-Lenzerheide'
|
'179 - 185cm': '179-185'
|
||||||
10321385: 'Reisen-Alert Les Deux Alpes'
|
'186 - 194cm': '186-194'
|
||||||
10321386: 'Reisen-Alert Montafon'
|
'195cm oder mehr': '195+'
|
||||||
10321387: 'Reisen-Alert Portes du Soleil'
|
body_dimensions.shoe_size_min: 36
|
||||||
10321388: 'Reisen-Alert Saalbach-Hinterglemm'
|
body_dimensions.shoe_size_max: 48
|
||||||
10538809: 'Reisen-Alert Scuol'
|
body_dimensions.weight_choices:
|
||||||
10321389: 'Reisen-Alert Stubaital'
|
'42 - 48kg': '42-48'
|
||||||
10554990: 'Reisen-Alert Ski & Boarderweek'
|
'49 - 57kg': '49-57'
|
||||||
|
'58 - 66kg': '58-66'
|
||||||
# MailJet contact metadata names for name synchronization
|
'67 - 78kg': '67-78'
|
||||||
mailjet_contact_metadata_fields:
|
'79 - 85kg': '79-85'
|
||||||
firstName: 'vorname'
|
'86 - 94kg': '86-94'
|
||||||
lastName: 'nachname'
|
'95kg oder mehr': '95+'
|
||||||
|
|
||||||
# Body dimensions ranges for BodyDimensionsType and ParticipantValidator
|
|
||||||
body_dimension_ranges:
|
|
||||||
height_min: 145
|
|
||||||
height_max: 210
|
|
||||||
weight_min: 40
|
|
||||||
weight_max: 120
|
|
||||||
shoe_size_min: 35
|
|
||||||
shoe_size_max: 50
|
|
||||||
|
|
||||||
# domain mapping for theme, gtm id and cmp url
|
# domain mapping for theme, gtm id and cmp url
|
||||||
domain_config:
|
domain_config:
|
||||||
@@ -108,65 +95,21 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$path: '%path_to_keys%'
|
$path: '%path_to_keys%'
|
||||||
|
|
||||||
App\Menu\AdminMenuBuilder:
|
|
||||||
arguments:
|
|
||||||
$factory: '@knp_menu.factory'
|
|
||||||
tags:
|
|
||||||
- name: knp_menu.menu_builder
|
|
||||||
method: createMainMenu
|
|
||||||
alias: admin_main
|
|
||||||
|
|
||||||
App\Service\BpnXmlSyncManager:
|
|
||||||
arguments:
|
|
||||||
$xmlSource: '@xml_source.storage'
|
|
||||||
$xmlExport: '@xml_export.storage'
|
|
||||||
$bpnCache: '@bpn.cache'
|
|
||||||
|
|
||||||
App\Command\BpnXmlCacheInvalidateCommand:
|
|
||||||
arguments:
|
|
||||||
$xmlExport: '@xml_export.storage'
|
|
||||||
$bpnCache: '@bpn.cache'
|
|
||||||
$cache: '@cache.app'
|
|
||||||
|
|
||||||
App\Security\Crypt:
|
App\Security\Crypt:
|
||||||
arguments:
|
arguments:
|
||||||
$path: '%path_to_keys%'
|
$path: '%path_to_keys%'
|
||||||
|
|
||||||
App\Service\TravelSnapshotManager:
|
App\Service\TravelDataService:
|
||||||
arguments:
|
|
||||||
$retentionBufferDays: '%travel_snapshot_retention_buffer_days%'
|
|
||||||
|
|
||||||
App\Service\TravelDataProvider:
|
|
||||||
arguments:
|
arguments:
|
||||||
$logger: '@monolog.logger.core'
|
$logger: '@monolog.logger.core'
|
||||||
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
|
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
|
||||||
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
|
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
|
||||||
|
|
||||||
App\Service\ParticipantFormSupport:
|
App\Service\BookingEditDataLoaderService:
|
||||||
arguments:
|
|
||||||
$bodyDimensionRanges: '%body_dimension_ranges%'
|
|
||||||
|
|
||||||
App\Service\BookingEditDataLoader:
|
|
||||||
arguments:
|
arguments:
|
||||||
$bpnCache: '@bpn.cache'
|
$bpnCache: '@bpn.cache'
|
||||||
|
|
||||||
App\BusProNet\XmlLoader\TravelLoader:
|
App\Service\VoucherValidationService:
|
||||||
arguments:
|
|
||||||
$cache: '@bpn.cache'
|
|
||||||
|
|
||||||
App\BusProNet\XmlLoader\HotelLoader:
|
|
||||||
arguments:
|
|
||||||
$cache: '@bpn.cache'
|
|
||||||
|
|
||||||
App\BusProNet\XmlLoader\PickupLoader:
|
|
||||||
arguments:
|
|
||||||
$cache: '@bpn.cache'
|
|
||||||
|
|
||||||
App\BusProNet\XmlLoader\InsuranceLoader:
|
|
||||||
arguments:
|
|
||||||
$cache: '@bpn.cache'
|
|
||||||
|
|
||||||
App\Service\VoucherValidator:
|
|
||||||
arguments:
|
arguments:
|
||||||
$cache: '@cache.app'
|
$cache: '@cache.app'
|
||||||
$logger: '@monolog.logger.bpn'
|
$logger: '@monolog.logger.bpn'
|
||||||
@@ -183,12 +126,12 @@ services:
|
|||||||
arguments:
|
arguments:
|
||||||
$handlers:
|
$handlers:
|
||||||
# Simple handlers with no external dependencies - instantiated directly
|
# Simple handlers with no external dependencies - instantiated directly
|
||||||
|
- 'App\Form\Service\ParticipantDateOfBirthFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantAssignedRoomFieldHandler'
|
- 'App\Form\Service\ParticipantAssignedRoomFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantRemarksRoomFieldHandler'
|
- 'App\Form\Service\ParticipantRemarksRoomFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantAdditionalServicesFieldHandler'
|
- 'App\Form\Service\ParticipantAdditionalServicesFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantCoursesFieldHandler'
|
- 'App\Form\Service\ParticipantCoursesFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantBoardFieldHandler'
|
- 'App\Form\Service\ParticipantBoardFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantVegFieldHandler'
|
|
||||||
- 'App\Form\Service\ParticipantRentalsFieldHandler'
|
- 'App\Form\Service\ParticipantRentalsFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantSkiPassFieldHandler'
|
- 'App\Form\Service\ParticipantSkiPassFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantTransportationOutboundFieldHandler'
|
- 'App\Form\Service\ParticipantTransportationOutboundFieldHandler'
|
||||||
@@ -199,25 +142,22 @@ services:
|
|||||||
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
|
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
|
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
|
||||||
# Complex handlers with dependencies - use service references
|
# Complex handlers with dependencies - use service references
|
||||||
- '@App\Form\Service\ParticipantDateOfBirthFieldHandler'
|
|
||||||
- '@App\Form\Service\ParticipantTransportationDiscountReplacementFieldHandler'
|
- '@App\Form\Service\ParticipantTransportationDiscountReplacementFieldHandler'
|
||||||
- '@App\Form\Service\ParticipantBulkInsuranceFieldHandler'
|
- '@App\Form\Service\ParticipantBulkInsuranceFieldHandler'
|
||||||
- '@App\Form\Service\ParticipantInsuranceFieldHandler'
|
- '@App\Form\Service\ParticipantInsuranceFieldHandler'
|
||||||
- '@App\Form\Service\ParticipantPurchaseVoucherFieldHandler'
|
- '@App\Form\Service\ParticipantPurchaseVoucherFieldHandler'
|
||||||
- '@App\Form\Service\ParticipantPromoVoucherFieldHandler'
|
- '@App\Form\Service\ParticipantPromoVoucherFieldHandler'
|
||||||
|
|
||||||
# Booking Status Rules
|
# Participant Status Rules
|
||||||
App\BusProNet\Service\StatusRule\Selection1473StatusRule: ~
|
|
||||||
App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule: ~
|
App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule: ~
|
||||||
|
|
||||||
# Booking Status Rule Registry
|
# Participant Status Rule Registry
|
||||||
App\BusProNet\Service\BookingStatusRuleRegistry:
|
App\BusProNet\Service\ParticipantStatusRuleRegistry:
|
||||||
arguments:
|
arguments:
|
||||||
$rules:
|
$rules:
|
||||||
- '@App\BusProNet\Service\StatusRule\Selection1473StatusRule'
|
|
||||||
- '@App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule'
|
- '@App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule'
|
||||||
|
|
||||||
App\Service\CmsDataProvider:
|
App\Service\CmsDataService:
|
||||||
arguments:
|
arguments:
|
||||||
$httpClient: '@typo3.client'
|
$httpClient: '@typo3.client'
|
||||||
$apiKey: 'AbcAbc123'# dummy key, not yet implemented
|
$apiKey: 'AbcAbc123'# dummy key, not yet implemented
|
||||||
@@ -226,38 +166,7 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: monolog.processor }
|
- { name: monolog.processor }
|
||||||
|
|
||||||
App\Service\NewsletterManager:
|
|
||||||
arguments:
|
|
||||||
$newsletterConfirmationTtlHours: '%newsletter_confirmation_ttl_hours%'
|
|
||||||
$mailjetLists: '%mailjet_lists%'
|
|
||||||
$defaultMailjetListId: '%env(default::MAILJET_DEFAULT_LIST_ID)%'
|
|
||||||
|
|
||||||
App\Controller\Api\NewsletterSubscriptionController:
|
|
||||||
arguments:
|
|
||||||
$mailjetLists: '%mailjet_lists%'
|
|
||||||
|
|
||||||
App\Controller\Webhook\MailjetNewsletterWebhookController:
|
|
||||||
arguments:
|
|
||||||
$mailjetLists: '%mailjet_lists%'
|
|
||||||
|
|
||||||
App\Service\DomainConfigProvider:
|
App\Service\DomainConfigProvider:
|
||||||
arguments:
|
arguments:
|
||||||
$domainConfig: '%domain_config%'
|
$domainConfig: '%domain_config%'
|
||||||
|
|
||||||
App\Email\Mailer:
|
|
||||||
arguments:
|
|
||||||
$defaults:
|
|
||||||
from: '%default_email_from%'
|
|
||||||
to: '%default_email_to%'
|
|
||||||
|
|
||||||
App\Validator\Constraints\ParticipantValidator:
|
|
||||||
arguments:
|
|
||||||
$bodyDimensionRanges: '%body_dimension_ranges%'
|
|
||||||
|
|
||||||
App\Service\MailjetApiClient:
|
|
||||||
arguments:
|
|
||||||
$apiKey: '%env(default::MAILJET_API_KEY)%'
|
|
||||||
$apiSecret: '%env(default::MAILJET_API_SECRET)%'
|
|
||||||
$apiBaseUrl: '%env(default::MAILJET_API_BASE_URL)%'
|
|
||||||
$defaultListId: '%env(default::MAILJET_DEFAULT_LIST_ID)%'
|
|
||||||
$contactMetadataFields: '%mailjet_contact_metadata_fields%'
|
|
||||||
+2
-3
@@ -101,7 +101,7 @@ host('staging')
|
|||||||
->set('bin/php', '/usr/local/bin/php')
|
->set('bin/php', '/usr/local/bin/php')
|
||||||
->set('rsync_src', __DIR__)
|
->set('rsync_src', __DIR__)
|
||||||
->set('rsync', $rsyncOptions)
|
->set('rsync', $rsyncOptions)
|
||||||
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://my.ep-reisen.net --web-basic-auth=myep:staging')
|
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://my.ep-reisen.net')
|
||||||
;
|
;
|
||||||
|
|
||||||
task('deploy', [
|
task('deploy', [
|
||||||
@@ -121,8 +121,7 @@ task('deploy', [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
task('deploy:assets', function () {
|
task('deploy:assets', function () {
|
||||||
runLocally('ddev exec npm ci');
|
runLocally('npm ci && npm run build');
|
||||||
runLocally('ddev exec npm run build');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
task('deploy:stop-workers', function () {
|
task('deploy:stop-workers', function () {
|
||||||
|
|||||||
@@ -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,120 +0,0 @@
|
|||||||
# Body Dimensions Transition Plan (Stakeholder Draft)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Prevent edit-form failures for existing bookings while we transition from legacy body-dimension values to a better long-term field model.
|
|
||||||
|
|
||||||
This plan introduces a temporary compatibility phase and keeps sunsetting manual and straightforward.
|
|
||||||
|
|
||||||
## Current Problem
|
|
||||||
|
|
||||||
- Existing bookings may contain legacy values such as `-148`, `149-157`, or `195+`.
|
|
||||||
- Integer-only form fields can fail when these legacy strings are loaded.
|
|
||||||
- Result: participant edit forms can crash before submit.
|
|
||||||
|
|
||||||
## Proposed Transition Strategy
|
|
||||||
|
|
||||||
Use **text input fields** for body dimensions during transition, with explicit server-side format validation.
|
|
||||||
|
|
||||||
### Key Decisions
|
|
||||||
|
|
||||||
- No gating by booking create date.
|
|
||||||
- One unified behavior for all bookings.
|
|
||||||
- Accept both legacy and numeric formats during transition.
|
|
||||||
- Keep manual sunsetting later (no automated migration switch).
|
|
||||||
|
|
||||||
## Functional Scope
|
|
||||||
|
|
||||||
Affected fields:
|
|
||||||
|
|
||||||
- `height`
|
|
||||||
- `weight`
|
|
||||||
- `shoeSize`
|
|
||||||
|
|
||||||
Affected components:
|
|
||||||
|
|
||||||
- `src/Form/BodyDimensionsType.php`
|
|
||||||
- `src/Validator/Constraints/ParticipantValidator.php`
|
|
||||||
- `src/Form/BookingParticipantType.php` (option passthrough already in place)
|
|
||||||
- `config/services.yaml` (ranges remain as validation/config source)
|
|
||||||
|
|
||||||
## Validation Rules During Transition
|
|
||||||
|
|
||||||
### Accepted formats
|
|
||||||
|
|
||||||
- Numeric value: `^\d+$` (e.g. `176`)
|
|
||||||
- Legacy lower/open/range tokens:
|
|
||||||
- `^-\d+$` (e.g. `-148`)
|
|
||||||
- `^\d+\+$` (e.g. `195+`)
|
|
||||||
- `^\d+\s*-\s*\d+$` (e.g. `149-157`)
|
|
||||||
|
|
||||||
### Requiredness
|
|
||||||
|
|
||||||
- Keep existing rule: body dimensions required when rentals are selected.
|
|
||||||
|
|
||||||
### Range checks
|
|
||||||
|
|
||||||
- Keep current numeric range checks based on `body_dimension_ranges`.
|
|
||||||
- Apply range checks only to plain numeric values in transition phase.
|
|
||||||
|
|
||||||
## UX/Behavior Expectations
|
|
||||||
|
|
||||||
- Existing legacy values render without form initialization errors.
|
|
||||||
- New numeric entries are accepted and validated.
|
|
||||||
- Invalid free text (e.g. `abc`, `17x`) shows clear validation errors.
|
|
||||||
- HTMX refresh behavior remains unchanged.
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
1. **Form type change**
|
|
||||||
- In `BodyDimensionsType`, replace integer fields with text fields for all three body dimensions.
|
|
||||||
- Keep labels/help text; keep placeholders based on configured ranges.
|
|
||||||
|
|
||||||
2. **Validator extension**
|
|
||||||
- Add format validation for all three fields in `ParticipantValidator`.
|
|
||||||
- Keep existing required-when-rentals logic.
|
|
||||||
- Keep existing range validation for numeric values.
|
|
||||||
|
|
||||||
3. **Message tuning**
|
|
||||||
- Add one clear message for invalid format.
|
|
||||||
- Retain existing range message for numeric out-of-range values.
|
|
||||||
|
|
||||||
4. **Testing and QA**
|
|
||||||
- Validate old bookings with legacy values open/edit successfully.
|
|
||||||
- Validate numeric path (valid and invalid range).
|
|
||||||
- Validate invalid format handling.
|
|
||||||
- Validate rental-required behavior.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- No 500 error when editing participants with legacy body-dimension values.
|
|
||||||
- Legacy values can be loaded and submitted in transition phase.
|
|
||||||
- Numeric values are accepted and range-validated.
|
|
||||||
- Invalid text is rejected with a user-facing validation message.
|
|
||||||
- Existing participant edit/refresh flows continue to work.
|
|
||||||
|
|
||||||
## Risks and Mitigations
|
|
||||||
|
|
||||||
- **Risk:** Free-text field semantics are less strict than integer fields.
|
|
||||||
- **Mitigation:** strict server-side regex + range checks.
|
|
||||||
|
|
||||||
- **Risk:** Inconsistent data representations during transition.
|
|
||||||
- **Mitigation:** explicit acceptance policy and manual sunset plan.
|
|
||||||
|
|
||||||
## Effort Estimate
|
|
||||||
|
|
||||||
- Implementation: 0.5 day
|
|
||||||
- Validation/message tuning: 0.25 day
|
|
||||||
- QA/manual testing: 0.5 day
|
|
||||||
- **Total:** ~1 to 1.5 days
|
|
||||||
|
|
||||||
## Manual Sunset Plan (Later)
|
|
||||||
|
|
||||||
When stakeholders approve end of transition:
|
|
||||||
|
|
||||||
1. Replace text fields with final semantic field type(s).
|
|
||||||
2. Remove legacy format acceptance from validator.
|
|
||||||
3. Keep only final numeric/range validation behavior.
|
|
||||||
4. Remove transition-specific tests and copy.
|
|
||||||
|
|
||||||
Expected cleanup effort: ~0.25 to 0.5 day.
|
|
||||||
@@ -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.
|
|
||||||
@@ -886,9 +886,10 @@ Keys stored in `config/secret/` directory.
|
|||||||
|
|
||||||
**Location:** `src/Security/Voter/BookingVoter.php`
|
**Location:** `src/Security/Voter/BookingVoter.php`
|
||||||
|
|
||||||
This voter currently exists as legacy authorization logic and is not actively invoked by booking controllers.
|
| Operation | Requirements |
|
||||||
|
|-----------|--------------|
|
||||||
Current booking routes (`/bookings/*`) are guarded with `ROLE_USER`, while final document access checks are enforced by BusProNet.
|
| `VIEW` | User owns booking (addressId match) |
|
||||||
|
| `EDIT` | User owns booking AND booking is editable |
|
||||||
|
|
||||||
### 7.4 OAuth2
|
### 7.4 OAuth2
|
||||||
|
|
||||||
@@ -1103,16 +1104,6 @@ php bin/console app:bpn:replay <file> [--dry-run|-d] [--output|-o <file>]
|
|||||||
|
|
||||||
Replays stored XML requests against the API for debugging.
|
Replays stored XML requests against the API for debugging.
|
||||||
|
|
||||||
#### app:bpn:xml-anonymize
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php bin/console app:bpn:xml-anonymize <file-or-folder> [<output-file-or-folder>]
|
|
||||||
```
|
|
||||||
|
|
||||||
Anonymizes a single BPN XML dump or all XML dumps in a folder. Request/response
|
|
||||||
pairs are processed together so the anonymized identities stay consistent across
|
|
||||||
both files.
|
|
||||||
|
|
||||||
#### app:bpn:xml-sync
|
#### app:bpn:xml-sync
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1139,6 +1130,14 @@ php bin/console app:draft:inspect [--days|-d <n>] [--booking-id|-b <id>] [--dele
|
|||||||
|
|
||||||
Inspects and optionally deletes drafts.
|
Inspects and optionally deletes drafts.
|
||||||
|
|
||||||
|
#### app:draft:backfill-booking-number
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php bin/console app:draft:backfill-booking-number [--dry-run]
|
||||||
|
```
|
||||||
|
|
||||||
|
Populates missing booking numbers from API.
|
||||||
|
|
||||||
#### app:cleanup:log-entries
|
#### app:cleanup:log-entries
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -1155,36 +1154,6 @@ php bin/console app:cleanup:xml-dumps
|
|||||||
|
|
||||||
Removes XML debug dumps older than 3 days.
|
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 converted into durable per-list newsletter consent records and then deleted.
|
|
||||||
|
|
||||||
#### app:mailjet:newsletter-webhook
|
|
||||||
|
|
||||||
```bash
|
|
||||||
php bin/console app:mailjet:newsletter-webhook register
|
|
||||||
php bin/console app:mailjet:newsletter-webhook remove 123
|
|
||||||
php bin/console app:mailjet:newsletter-webhook deactivate
|
|
||||||
```
|
|
||||||
|
|
||||||
Creates or deletes the Mailjet `unsub` callback for `POST /webhooks/mailjet/newsletter`.
|
|
||||||
|
|
||||||
- Uses `APP_BASE_URL` by default and appends the webhook path.
|
|
||||||
- The command prepends `mailjet:<password>@` to the URL before registering it with Mailjet.
|
|
||||||
- Configure the plain password in `MAILJET_WEBHOOK_BASIC_PASSWORD`; Symfony uses it for the webhook firewall.
|
|
||||||
- Pass `--url` to target a different full webhook URL.
|
|
||||||
- `register` prints the API response payload so you can note the callback ID.
|
|
||||||
- `remove` and `deactivate` require the callback ID returned by Mailjet.
|
|
||||||
|
|
||||||
### 10.3 Setup Commands
|
### 10.3 Setup Commands
|
||||||
|
|
||||||
#### app:crypto:generate-keys
|
#### app:crypto:generate-keys
|
||||||
|
|||||||
+6
-68
@@ -30,59 +30,6 @@
|
|||||||
"Auth URL": "https://myep.ddev.site/authorize",
|
"Auth URL": "https://myep.ddev.site/authorize",
|
||||||
"Token URL": "https://myep.ddev.site/token",
|
"Token URL": "https://myep.ddev.site/token",
|
||||||
"Scope": "api"
|
"Scope": "api"
|
||||||
},
|
|
||||||
"oauth2_newsletter": {
|
|
||||||
"Type": "OAuth2",
|
|
||||||
"Grant Type": "Client Credentials",
|
|
||||||
"Client ID": "{{oauth2_api_client_id}}",
|
|
||||||
"Client Secret": "{{oauth2_api_client_secret}}",
|
|
||||||
"Auth URL": "https://myep.ddev.site/authorize",
|
|
||||||
"Token URL": "https://myep.ddev.site/token",
|
|
||||||
"Scope": "api"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"oauth2_newsletter": {
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +42,8 @@
|
|||||||
"Grant Type": "Authorization Code",
|
"Grant Type": "Authorization Code",
|
||||||
"Client ID": "{{oauth2_client_id}}",
|
"Client ID": "{{oauth2_client_id}}",
|
||||||
"Client Secret": "{{oauth2_client_secret}}",
|
"Client Secret": "{{oauth2_client_secret}}",
|
||||||
"Auth URL": "https://my.ep-reisen.de/authorize",
|
"Auth URL": "https://myep.ep-reisen.de/authorize",
|
||||||
"Token URL": "https://my.ep-reisen.de/token",
|
"Token URL": "https://myep.ep-reisen.de/token",
|
||||||
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
||||||
"Scope": "email"
|
"Scope": "email"
|
||||||
},
|
},
|
||||||
@@ -105,8 +52,8 @@
|
|||||||
"Grant Type": "Authorization Code",
|
"Grant Type": "Authorization Code",
|
||||||
"Client ID": "{{oauth2_client_id}}",
|
"Client ID": "{{oauth2_client_id}}",
|
||||||
"Client Secret": "{{oauth2_client_secret}}",
|
"Client Secret": "{{oauth2_client_secret}}",
|
||||||
"Auth URL": "https://my.ep-reisen.de/authorize",
|
"Auth URL": "https://myep.ep-reisen.de/authorize",
|
||||||
"Token URL": "https://my.ep-reisen.de/token",
|
"Token URL": "https://myep.ep-reisen.de/token",
|
||||||
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
"Redirect URL": "https://myep-team.ep-reisen.de/auth/check",
|
||||||
"Scope": "email profile"
|
"Scope": "email profile"
|
||||||
},
|
},
|
||||||
@@ -115,17 +62,8 @@
|
|||||||
"Grant Type": "Client Credentials",
|
"Grant Type": "Client Credentials",
|
||||||
"Client ID": "{{oauth2_api_client_id}}",
|
"Client ID": "{{oauth2_api_client_id}}",
|
||||||
"Client Secret": "{{oauth2_api_client_secret}}",
|
"Client Secret": "{{oauth2_api_client_secret}}",
|
||||||
"Auth URL": "https://my.ep-reisen.de/authorize",
|
"Auth URL": "https://myep.ep-reisen.de/authorize",
|
||||||
"Token URL": "https://my.ep-reisen.de/token",
|
"Token URL": "https://myep.ep-reisen.de/token",
|
||||||
"Scope": "api"
|
|
||||||
},
|
|
||||||
"oauth2_newsletter": {
|
|
||||||
"Type": "OAuth2",
|
|
||||||
"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",
|
|
||||||
"Scope": "api"
|
"Scope": "api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-172
@@ -1,172 +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}}"
|
|
||||||
}
|
|
||||||
|
|
||||||
> {%
|
|
||||||
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}}
|
|
||||||
|
|
||||||
|
|
||||||
### Get contact metadata by ID (after find/create)
|
|
||||||
# @no-cookie-jar
|
|
||||||
GET {{mailjet_base_url}}/contactdata/{{contact_id}}
|
|
||||||
Accept: application/json
|
|
||||||
Authorization: Basic {{mailjet_api_key}} {{mailjet_api_secret}}
|
|
||||||
|
|
||||||
### 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260422120000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Store requested Mailjet list IDs on newsletter opt-in confirmations';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql("ALTER TABLE newsletter_opt_in_confirmation ADD mailjet_list_ids JSON DEFAULT NULL COMMENT '(DC2Type:json)'");
|
|
||||||
$this->addSql('UPDATE newsletter_opt_in_confirmation SET mailjet_list_ids = JSON_ARRAY(10321569) WHERE mailjet_list_ids IS NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE newsletter_opt_in_confirmation DROP mailjet_list_ids');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260423120000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Store optional first and last names on newsletter opt-in confirmations';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql("ALTER TABLE newsletter_opt_in_confirmation ADD first_name VARCHAR(255) DEFAULT NULL, ADD last_name VARCHAR(255) DEFAULT NULL");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE newsletter_opt_in_confirmation DROP first_name, DROP last_name');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260424110732 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Track newsletter revocations on opt-in confirmations';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql("ALTER TABLE newsletter_opt_in_confirmation ADD revoked_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)'");
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE newsletter_opt_in_confirmation DROP revoked_at');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260429120000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Create per-list newsletter consent table and backfill confirmed opt-ins';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql("CREATE TABLE newsletter_consent (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(255) NOT NULL, mailjet_list_id INT NOT NULL, confirmed_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', revoked_at DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)', first_name VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', updated_at DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', UNIQUE INDEX UNIQ_NEWSLETTER_CONSENT_EMAIL_LIST (email, mailjet_list_id), INDEX IDX_NEWSLETTER_CONSENT_EMAIL (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB");
|
|
||||||
$this->addSql("INSERT IGNORE INTO newsletter_consent (email, mailjet_list_id, confirmed_at, revoked_at, first_name, last_name, created_at, updated_at) SELECT email, 10321569, confirmed_at, revoked_at, first_name, last_name, created_at, COALESCE(confirmed_at, created_at) FROM newsletter_opt_in_confirmation WHERE confirmed_at IS NOT NULL AND (mailjet_list_ids IS NULL OR JSON_LENGTH(mailjet_list_ids) = 0)");
|
|
||||||
$this->addSql("INSERT IGNORE INTO newsletter_consent (email, mailjet_list_id, confirmed_at, revoked_at, first_name, last_name, created_at, updated_at) SELECT c.email, CAST(j.list_id AS UNSIGNED), c.confirmed_at, c.revoked_at, c.first_name, c.last_name, c.created_at, COALESCE(c.confirmed_at, c.created_at) FROM newsletter_opt_in_confirmation c JOIN JSON_TABLE(c.mailjet_list_ids, '$[*]' COLUMNS (list_id VARCHAR(32) PATH '$')) j WHERE c.confirmed_at IS NOT NULL");
|
|
||||||
$this->addSql('DELETE FROM newsletter_opt_in_confirmation WHERE confirmed_at IS NOT NULL');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('DROP TABLE newsletter_consent');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20260429130000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Rename newsletter opt-in confirmation table to newsletter opt-in request';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('RENAME TABLE newsletter_opt_in_confirmation TO newsletter_opt_in_request');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('RENAME TABLE newsletter_opt_in_request TO newsletter_opt_in_confirmation');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+1987
-1600
File diff suppressed because it is too large.
Load diff
+3
-2
@@ -5,7 +5,7 @@
|
|||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.16.0",
|
||||||
"@hotwired/stimulus": "^3.0.0",
|
"@hotwired/stimulus": "^3.0.0",
|
||||||
"@symfony/stimulus-bridge": "^3.2.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/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"toastify-js": "^1.12.0",
|
"toastify-js": "^1.12.0",
|
||||||
"webpack": "^5.74.0",
|
"webpack": "^5.74.0",
|
||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^5.1.0",
|
||||||
|
"webpack-notifier": "^1.15.0"
|
||||||
},
|
},
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
includes:
|
|
||||||
- vendor/phpstan/phpstan-symfony/extension.neon
|
|
||||||
- vendor/phpstan/phpstan-doctrine/extension.neon
|
|
||||||
|
|
||||||
parameters:
|
|
||||||
level: 6
|
|
||||||
paths:
|
|
||||||
- src
|
|
||||||
tmpDir: var/phpstan
|
|
||||||
parallel:
|
|
||||||
maximumNumberOfProcesses: 1
|
|
||||||
symfony:
|
|
||||||
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
|
|
||||||
doctrine:
|
|
||||||
objectManagerLoader: tests/doctrine-object-manager.php
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Rector\Config\RectorConfig;
|
|
||||||
|
|
||||||
return RectorConfig::configure()
|
|
||||||
->withPaths([
|
|
||||||
__DIR__.'/src',
|
|
||||||
__DIR__.'/tests',
|
|
||||||
])
|
|
||||||
->withImportNames(
|
|
||||||
importShortClasses: false,
|
|
||||||
removeUnusedImports: true,
|
|
||||||
)
|
|
||||||
->withComposerBased(
|
|
||||||
doctrine: true,
|
|
||||||
symfony: true,
|
|
||||||
)
|
|
||||||
->withSymfonyContainerXml(
|
|
||||||
__DIR__.'/var/cache/dev/App_KernelDevDebugContainer.xml'
|
|
||||||
)
|
|
||||||
->withFluentCallNewLine()
|
|
||||||
->withCache(__DIR__.'/var/rector');
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Admin\Field;
|
||||||
|
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
|
||||||
|
|
||||||
|
class JsonDataField implements FieldInterface
|
||||||
|
{
|
||||||
|
use FieldTrait;
|
||||||
|
|
||||||
|
public static function new(string $propertyName, ?string $label = null): self
|
||||||
|
{
|
||||||
|
return (new self())
|
||||||
|
->setProperty($propertyName)
|
||||||
|
->setLabel($label)
|
||||||
|
->setTemplatePath('admin/field/json.html.twig');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,13 @@ use App\BusProNet\Exception\ApiClientException;
|
|||||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||||
use App\BusProNet\Exception\ResponseParserException;
|
use App\BusProNet\Exception\ResponseParserException;
|
||||||
use App\BusProNet\Exception\TimeoutException;
|
use App\BusProNet\Exception\TimeoutException;
|
||||||
use App\BusProNet\Model\Agency;
|
|
||||||
use App\BusProNet\Model\BaseData;
|
use App\BusProNet\Model\BaseData;
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
use App\BusProNet\Model\BookingResponse;
|
use App\BusProNet\Model\BookingResponse;
|
||||||
use App\BusProNet\Model\BookingUpdate;
|
use App\BusProNet\Model\BookingUpdate;
|
||||||
use App\BusProNet\Model\CrmAttributes;
|
use App\BusProNet\Model\CrmAttributes;
|
||||||
use App\BusProNet\Model\ExtendedServiceAvailabilityResponse;
|
|
||||||
use App\BusProNet\Model\Notification;
|
use App\BusProNet\Model\Notification;
|
||||||
use App\BusProNet\Model\PersonalData;
|
use App\BusProNet\Model\PersonalData;
|
||||||
use App\BusProNet\Model\PersonalDataUpdateResponse;
|
|
||||||
use App\BusProNet\Model\PromoVoucher;
|
use App\BusProNet\Model\PromoVoucher;
|
||||||
use App\BusProNet\Model\PurchaseVoucher;
|
use App\BusProNet\Model\PurchaseVoucher;
|
||||||
use App\BusProNet\Model\RegistrationResponse;
|
use App\BusProNet\Model\RegistrationResponse;
|
||||||
@@ -41,7 +38,6 @@ class ApiClient
|
|||||||
public const TYPE_BASE_DATA_COUNTRIES = 'STAMMLAENDER';
|
public const TYPE_BASE_DATA_COUNTRIES = 'STAMMLAENDER';
|
||||||
public const TYPE_MUTABLE_DATA = 'MOEGLICHEAENDERUNGEN';
|
public const TYPE_MUTABLE_DATA = 'MOEGLICHEAENDERUNGEN';
|
||||||
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
|
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
|
||||||
public const TYPE_AVAILABILITY_EXTENDED = 'VERFUEGBARKEIT2';
|
|
||||||
public const TYPE_AVAILABILITY_HOTEL = 'VERFUEGBARKEITHOTEL';
|
public const TYPE_AVAILABILITY_HOTEL = 'VERFUEGBARKEITHOTEL';
|
||||||
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
|
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
|
||||||
public const TYPE_BOOKING = 'BUCHUNG';
|
public const TYPE_BOOKING = 'BUCHUNG';
|
||||||
@@ -51,13 +47,11 @@ class ApiClient
|
|||||||
public const TYPE_PURCHASE_VOUCHER = 'GUTSCHEINPRUEFUNGEINLOESUNG';
|
public const TYPE_PURCHASE_VOUCHER = 'GUTSCHEINPRUEFUNGEINLOESUNG';
|
||||||
public const TYPE_PROMO_VOUCHER = 'AKTIONSGUTSCHEIN';
|
public const TYPE_PROMO_VOUCHER = 'AKTIONSGUTSCHEIN';
|
||||||
|
|
||||||
/** @var array<string, mixed> */
|
|
||||||
private array $config;
|
private array $config;
|
||||||
private float $operationStartTime;
|
private float $operationStartTime;
|
||||||
private int $selectedPort;
|
private int $selectedPort;
|
||||||
private int $requestCounter = 0;
|
private int $requestCounter = 0;
|
||||||
|
|
||||||
/** @param array<string, mixed> $options */
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly SerializerInterface $serializer,
|
private readonly SerializerInterface $serializer,
|
||||||
private readonly ApiResponseParser $responseParser,
|
private readonly ApiResponseParser $responseParser,
|
||||||
@@ -128,7 +122,7 @@ class ApiClient
|
|||||||
string $password,
|
string $password,
|
||||||
PersonalData $personalData,
|
PersonalData $personalData,
|
||||||
bool $debug = false,
|
bool $debug = false,
|
||||||
): Notification|PersonalDataUpdateResponse {
|
): Notification|PersonalData {
|
||||||
$data = [
|
$data = [
|
||||||
'user' => $this->config['bpn_username'],
|
'user' => $this->config['bpn_username'],
|
||||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||||
@@ -321,21 +315,6 @@ class ApiClient
|
|||||||
return $this->sendRequest(static::TYPE_AVAILABILITY, $data);
|
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
|
* @throws ApiClientException
|
||||||
*/
|
*/
|
||||||
@@ -617,10 +596,6 @@ class ApiClient
|
|||||||
/**
|
/**
|
||||||
* @throws ApiClientException
|
* @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
|
private function sendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed
|
||||||
{
|
{
|
||||||
return $this->executeWithRetry(fn () => $this->doSendRequest($type, $data, $additionalArgs, $debug), $type);
|
return $this->executeWithRetry(fn () => $this->doSendRequest($type, $data, $additionalArgs, $debug), $type);
|
||||||
@@ -629,7 +604,6 @@ class ApiClient
|
|||||||
/**
|
/**
|
||||||
* @throws ApiClientException
|
* @throws ApiClientException
|
||||||
*/
|
*/
|
||||||
/** @param array<string, mixed> $data */
|
|
||||||
private function sendRequestRaw(array $data): string
|
private function sendRequestRaw(array $data): string
|
||||||
{
|
{
|
||||||
return $this->executeWithRetry(fn () => $this->doSendRequestRaw($data));
|
return $this->executeWithRetry(fn () => $this->doSendRequestRaw($data));
|
||||||
@@ -685,10 +659,6 @@ class ApiClient
|
|||||||
* @throws ApiClientException
|
* @throws ApiClientException
|
||||||
* @throws ImmediateConnectionCloseException
|
* @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
|
private function doSendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed
|
||||||
{
|
{
|
||||||
$requestId = $this->getRequestId();
|
$requestId = $this->getRequestId();
|
||||||
@@ -746,8 +716,6 @@ class ApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $data
|
|
||||||
*
|
|
||||||
* @throws ApiClientException
|
* @throws ApiClientException
|
||||||
* @throws ImmediateConnectionCloseException
|
* @throws ImmediateConnectionCloseException
|
||||||
*/
|
*/
|
||||||
@@ -809,8 +777,6 @@ class ApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return resource
|
|
||||||
*
|
|
||||||
* @throws ApiClientException
|
* @throws ApiClientException
|
||||||
* @throws TimeoutException
|
* @throws TimeoutException
|
||||||
*/
|
*/
|
||||||
@@ -845,7 +811,6 @@ class ApiClient
|
|||||||
$this->logger->error('Connection retry timeout exceeded', [
|
$this->logger->error('Connection retry timeout exceeded', [
|
||||||
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
||||||
'total_timeout' => $this->config['total_timeout'],
|
'total_timeout' => $this->config['total_timeout'],
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
throw new TimeoutException('Connection timeout exceeded during retries');
|
throw new TimeoutException('Connection timeout exceeded during retries');
|
||||||
}
|
}
|
||||||
@@ -854,7 +819,6 @@ class ApiClient
|
|||||||
'error_message' => $errStr,
|
'error_message' => $errStr,
|
||||||
'error_number' => $errNo,
|
'error_number' => $errNo,
|
||||||
'attempt' => $tries,
|
'attempt' => $tries,
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
++$tries;
|
++$tries;
|
||||||
sleep(1);
|
sleep(1);
|
||||||
@@ -868,7 +832,6 @@ class ApiClient
|
|||||||
'error_message' => $errStr,
|
'error_message' => $errStr,
|
||||||
'error_number' => $errNo,
|
'error_number' => $errNo,
|
||||||
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
throw new ApiClientException('Unable to open socket');
|
throw new ApiClientException('Unable to open socket');
|
||||||
}
|
}
|
||||||
@@ -877,8 +840,6 @@ class ApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param resource $socket
|
|
||||||
*
|
|
||||||
* @throws TimeoutException
|
* @throws TimeoutException
|
||||||
*/
|
*/
|
||||||
private function send($socket, string $data): void
|
private function send($socket, string $data): void
|
||||||
@@ -887,7 +848,6 @@ class ApiClient
|
|||||||
if (microtime(true) - $this->operationStartTime > $this->config['total_timeout']) {
|
if (microtime(true) - $this->operationStartTime > $this->config['total_timeout']) {
|
||||||
$this->logger->error('Total timeout exceeded before send', [
|
$this->logger->error('Total timeout exceeded before send', [
|
||||||
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
throw new TimeoutException('Total operation timeout exceeded before send');
|
throw new TimeoutException('Total operation timeout exceeded before send');
|
||||||
}
|
}
|
||||||
@@ -898,8 +858,6 @@ class ApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param resource $socket
|
|
||||||
*
|
|
||||||
* @throws TimeoutException
|
* @throws TimeoutException
|
||||||
* @throws ImmediateConnectionCloseException
|
* @throws ImmediateConnectionCloseException
|
||||||
*/
|
*/
|
||||||
@@ -918,7 +876,6 @@ class ApiClient
|
|||||||
'total_timeout' => $totalTimeout,
|
'total_timeout' => $totalTimeout,
|
||||||
'bytes_received' => strlen($response),
|
'bytes_received' => strlen($response),
|
||||||
'read_attempts' => $readAttempts,
|
'read_attempts' => $readAttempts,
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
throw new TimeoutException('Total operation timeout exceeded while receiving data');
|
throw new TimeoutException('Total operation timeout exceeded while receiving data');
|
||||||
}
|
}
|
||||||
@@ -936,7 +893,6 @@ class ApiClient
|
|||||||
'elapsed_time' => $elapsedTime,
|
'elapsed_time' => $elapsedTime,
|
||||||
'bytes_received' => 0,
|
'bytes_received' => 0,
|
||||||
'read_attempts' => $readAttempts,
|
'read_attempts' => $readAttempts,
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
throw new ImmediateConnectionCloseException('Server closed connection immediately - server may be busy');
|
throw new ImmediateConnectionCloseException('Server closed connection immediately - server may be busy');
|
||||||
}
|
}
|
||||||
@@ -945,7 +901,6 @@ class ApiClient
|
|||||||
'elapsed_time' => $elapsedTime,
|
'elapsed_time' => $elapsedTime,
|
||||||
'bytes_received' => strlen($response),
|
'bytes_received' => strlen($response),
|
||||||
'read_attempts' => $readAttempts,
|
'read_attempts' => $readAttempts,
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
throw new TimeoutException('Stream timeout while reading from socket');
|
throw new TimeoutException('Stream timeout while reading from socket');
|
||||||
}
|
}
|
||||||
@@ -958,7 +913,6 @@ class ApiClient
|
|||||||
$this->logger->warning('Server closed connection without sending data', [
|
$this->logger->warning('Server closed connection without sending data', [
|
||||||
'elapsed_time' => $elapsedTime,
|
'elapsed_time' => $elapsedTime,
|
||||||
'read_attempts' => $readAttempts,
|
'read_attempts' => $readAttempts,
|
||||||
'port' => $this->selectedPort,
|
|
||||||
]);
|
]);
|
||||||
throw new ImmediateConnectionCloseException('Server closed connection without sending data');
|
throw new ImmediateConnectionCloseException('Server closed connection without sending data');
|
||||||
}
|
}
|
||||||
@@ -966,17 +920,11 @@ class ApiClient
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param resource $socket */
|
|
||||||
private function disconnect($socket): void
|
private function disconnect($socket): void
|
||||||
{
|
{
|
||||||
@fclose($socket);
|
@fclose($socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $options
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function resolveOptions(array $options): array
|
private function resolveOptions(array $options): array
|
||||||
{
|
{
|
||||||
$optionsResolver = new OptionsResolver();
|
$optionsResolver = new OptionsResolver();
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ final class Constants
|
|||||||
// Booking status codes from buchungstatusmoeglich attribute
|
// Booking status codes from buchungstatusmoeglich attribute
|
||||||
public const BOOKING_STATUS_FREE = 'F';
|
public const BOOKING_STATUS_FREE = 'F';
|
||||||
public const BOOKING_STATUS_INQUIRY = 'A';
|
public const BOOKING_STATUS_INQUIRY = 'A';
|
||||||
public const BOOKING_STATUS_OPTION = 'O'; // Optionsbuchung
|
public const BOOKING_STATUS_OPEN = 'O'; // Optionsbuchung
|
||||||
|
|
||||||
// Payment methods
|
// Payment methods
|
||||||
public const PAYMENT_METHOD_TRANSFER = 'transfer';
|
public const PAYMENT_METHOD_TRANSFER = 'transfer';
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ use App\BusProNet\Constants;
|
|||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
use App\BusProNet\Utility\DirectionMapper;
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
use App\BusProNet\XmlLoader\AgencyLoader;
|
|
||||||
use App\Form\Model\BankAccountDto;
|
use App\Form\Model\BankAccountDto;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Form\Model\RoomSelectionDto;
|
use App\Form\Model\RoomSelectionDto;
|
||||||
use App\Service\BookingPriceCalculator;
|
use App\Service\BookingPriceCalculatorService;
|
||||||
use App\Service\InsuranceManager;
|
use App\Service\InsuranceService;
|
||||||
use function Symfony\Component\String\u;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes booking form data and converts it into BusProNet API payload format.
|
* Processes booking form data and converts it into BusProNet API payload format.
|
||||||
@@ -27,8 +25,8 @@ use function Symfony\Component\String\u;
|
|||||||
class BookingDataProcessor
|
class BookingDataProcessor
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly InsuranceManager $insuranceService,
|
private readonly InsuranceService $insuranceService,
|
||||||
private readonly BookingPriceCalculator $priceCalculatorService,
|
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||||
private readonly ServiceMappingCollector $mappingCollector,
|
private readonly ServiceMappingCollector $mappingCollector,
|
||||||
private readonly ParticipantServiceProcessor $serviceProcessor,
|
private readonly ParticipantServiceProcessor $serviceProcessor,
|
||||||
private readonly BookingPayloadBuilder $payloadBuilder,
|
private readonly BookingPayloadBuilder $payloadBuilder,
|
||||||
@@ -44,12 +42,11 @@ class BookingDataProcessor
|
|||||||
*
|
*
|
||||||
* IMPORTANT: Address handling for edit mode
|
* IMPORTANT: Address handling for edit mode
|
||||||
* - ParticipantDto::fromPersonalData() clones address objects to prevent shared references
|
* - ParticipantDto::fromPersonalData() clones address objects to prevent shared references
|
||||||
* - For normal bookings, participant[0]'s missing address/dimensions are filled from the
|
* - First participant's address is NOT auto-populated from applicant
|
||||||
* applicant (same person in normal bookings, but BPN may omit fields in <teilnehmer>)
|
* - Template uses placeholders to show applicant's address as hints
|
||||||
* - For internal agency bookings, this copy is skipped: the applicant is a customer, not
|
* - This prevents address data from being inadvertently modified during form binding
|
||||||
* the first participant, so copying applicant data into participant[0] would be incorrect
|
|
||||||
*/
|
*/
|
||||||
public function createBookingDtoFromBooking(Booking $booking, Travel $travel, bool $isInternalAgencyBooking = false): BookingDto
|
public function createBookingDtoFromBooking(Booking $booking, Travel $travel): BookingDto
|
||||||
{
|
{
|
||||||
$dto = new BookingDto($travel, $booking->hotelId);
|
$dto = new BookingDto($travel, $booking->hotelId);
|
||||||
|
|
||||||
@@ -71,18 +68,19 @@ class BookingDataProcessor
|
|||||||
$participantData = ParticipantDto::fromPersonalData($participant);
|
$participantData = ParticipantDto::fromPersonalData($participant);
|
||||||
$participantData->index = $index;
|
$participantData->index = $index;
|
||||||
|
|
||||||
// For normal bookings, applicant === participant[0]: copy address and body dimensions
|
// First participant (applicant): copy data from applicant if participant data is empty
|
||||||
// from applicant when BPN returns full data only in <anmelder> and minimal data in
|
// BPN API may return full data only in <anmelder> but minimal/empty data in <teilnehmer id="1">
|
||||||
// <teilnehmer id="1">. Skipped for internal agency bookings where the applicant is
|
if (0 === $index) {
|
||||||
// a customer (a different entity from the first traveler).
|
|
||||||
if (0 === $index && false === $isInternalAgencyBooking) {
|
|
||||||
// Copy address if first participant has no street (indicating empty/incomplete address)
|
// Copy address if first participant has no street (indicating empty/incomplete address)
|
||||||
$isEmpty = null === $participantData->address
|
// This allows applicant and first participant to be different people with different addresses
|
||||||
|| null === $participantData->address->street
|
if (null !== $booking->applicant->address) {
|
||||||
|| u($participantData->address->street)->trim()->isEmpty();
|
$isEmpty = null === $participantData->address
|
||||||
|
|| null === $participantData->address->street
|
||||||
|
|| '' === trim($participantData->address->street);
|
||||||
|
|
||||||
if ($isEmpty) {
|
if ($isEmpty) {
|
||||||
$participantData->address = clone $booking->applicant->address;
|
$participantData->address = clone $booking->applicant->address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy body dimensions from applicant if not present in participant
|
// Copy body dimensions from applicant if not present in participant
|
||||||
@@ -109,9 +107,6 @@ class BookingDataProcessor
|
|||||||
$participantData->board = $booking
|
$participantData->board = $booking
|
||||||
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_BOARD);
|
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_BOARD);
|
||||||
|
|
||||||
$vegServices = $booking->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_VEG);
|
|
||||||
$participantData->veg = false === empty($vegServices) ? reset($vegServices) : null;
|
|
||||||
|
|
||||||
$participantData->rentals = $booking
|
$participantData->rentals = $booking
|
||||||
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_RENTALS);
|
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_RENTALS);
|
||||||
|
|
||||||
@@ -156,9 +151,6 @@ class BookingDataProcessor
|
|||||||
// Enrich services with data from travel (especially prices)
|
// Enrich services with data from travel (especially prices)
|
||||||
$this->enrichParticipantServicesFromTravel($participantData, $travel);
|
$this->enrichParticipantServicesFromTravel($participantData, $travel);
|
||||||
|
|
||||||
// Normalize loaded participant data so the edit flow sees canonical values.
|
|
||||||
$participantData->normalizeLoadedData();
|
|
||||||
|
|
||||||
$dto->participants[$index] = $participantData;
|
$dto->participants[$index] = $participantData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,11 +213,6 @@ class BookingDataProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich veg
|
|
||||||
if (null !== $participant->veg && isset($travel->additionalServices[$participant->veg->id])) {
|
|
||||||
$participant->veg = $travel->additionalServices[$participant->veg->id];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich rentals
|
// Enrich rentals
|
||||||
foreach ($participant->rentals as $key => $rental) {
|
foreach ($participant->rentals as $key => $rental) {
|
||||||
if (isset($travel->additionalServices[$rental->id])) {
|
if (isset($travel->additionalServices[$rental->id])) {
|
||||||
@@ -297,12 +284,12 @@ class BookingDataProcessor
|
|||||||
*
|
*
|
||||||
* @param BookingDto|null $formData The booking edit form data containing updated participant and service selections
|
* @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
|
public function createUpdateRequestPayload(?BookingDto $formData): array
|
||||||
{
|
{
|
||||||
// Apply bulk or family insurance if applicable (modifies DTO in place)
|
// Apply bulk insurance if enabled (modifies DTO in place)
|
||||||
$this->applyApplicantInsuranceToParticipants($formData);
|
$this->applyBulkInsuranceIfActive($formData);
|
||||||
|
|
||||||
$bookingData = $formData->booking;
|
$bookingData = $formData->booking;
|
||||||
$travelData = $formData->travel;
|
$travelData = $formData->travel;
|
||||||
@@ -358,50 +345,37 @@ class BookingDataProcessor
|
|||||||
* @param BookingDto $bookingDto The booking creation form data
|
* @param BookingDto $bookingDto The booking creation form data
|
||||||
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
|
* @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
|
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
|
||||||
{
|
{
|
||||||
// Apply bulk or family insurance if applicable (modifies DTO in place)
|
// Apply bulk insurance if enabled (modifies DTO in place)
|
||||||
$this->applyApplicantInsuranceToParticipants($bookingDto);
|
$this->applyBulkInsuranceIfActive($bookingDto);
|
||||||
|
|
||||||
return $this->payloadBuilder->buildCreatePayload($bookingDto, $bookingType);
|
return $this->payloadBuilder->buildCreatePayload($bookingDto, $bookingType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the applicant's insurance to other participants if their selection requires it.
|
* Applies bulk insurance assignment if the applicant has enabled it.
|
||||||
*
|
*
|
||||||
* Runs when either:
|
* When bulk insurance is active (applicant's bulkInsuranceBooking = true), this method
|
||||||
* - Bulk insurance is active (applicant's bulkInsuranceBooking = true): assigns the
|
* assigns the applicant's insurance TYPE to all dependent participants with automatic
|
||||||
* applicant's insurance TYPE to all dependent participants with automatic price tier
|
* price tier adjustment based on each participant's total cost.
|
||||||
* adjustment based on each participant's individual cost.
|
|
||||||
* - The applicant selected a family insurance: family insurance covers the whole family
|
|
||||||
* under a single policy, so dependents must never hold their own insurance, regardless
|
|
||||||
* of whether the bulk checkbox was checked.
|
|
||||||
*
|
*
|
||||||
* IMPORTANT: In edit mode, non-family bulk insurance only applies to participants who:
|
* IMPORTANT: In edit mode, bulk insurance only applies to participants who:
|
||||||
* 1. Currently have NO insurance assigned (insurance === null)
|
* 1. Currently have NO insurance assigned (insurance === null)
|
||||||
* 2. OR whose insurance was already assigned via previous bulk operation
|
* 2. OR whose insurance was already assigned via previous bulk operation
|
||||||
*
|
*
|
||||||
* This prevents overriding individually selected insurances that are locked. Family
|
* This prevents overriding individually selected insurances that are locked.
|
||||||
* insurance is exempt from this protection: dependents can never legitimately hold
|
|
||||||
* their own policy alongside it, so their insurance is always cleared, in edit mode too.
|
|
||||||
*
|
*
|
||||||
* @param BookingDto $bookingDto The booking DTO (create or edit flow)
|
* @param BookingDto $bookingDto The booking DTO (create or edit flow)
|
||||||
*/
|
*/
|
||||||
private function applyApplicantInsuranceToParticipants(BookingDto $bookingDto): void
|
private function applyBulkInsuranceIfActive(BookingDto $bookingDto): void
|
||||||
{
|
{
|
||||||
$applicant = $bookingDto->getParticipant(0);
|
$applicant = $bookingDto->getParticipant(0);
|
||||||
|
|
||||||
if (null === $applicant || null === $applicant->insurance) {
|
// Check if bulk insurance is enabled and applicant has selected an insurance
|
||||||
return;
|
if (null === $applicant || false === $applicant->bulkInsuranceBooking || null === $applicant->insurance) {
|
||||||
}
|
|
||||||
|
|
||||||
$isFamilyInsurance = true === $applicant->insurance->familyInsurance;
|
|
||||||
|
|
||||||
// Check if bulk insurance is enabled OR the applicant's selection is a family
|
|
||||||
// insurance (which always applies to dependents, independent of the bulk checkbox)
|
|
||||||
if (false === $applicant->bulkInsuranceBooking && false === $isFamilyInsurance) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +388,7 @@ class BookingDataProcessor
|
|||||||
$participantPrices[$index] = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index);
|
$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(
|
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
|
||||||
$selectableInsurances,
|
$selectableInsurances,
|
||||||
$applicant->insurance,
|
$applicant->insurance,
|
||||||
@@ -422,11 +396,10 @@ class BookingDataProcessor
|
|||||||
$participantPrices
|
$participantPrices
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply assignments to all participants; for non-family insurance skip the applicant
|
// Apply assignments to dependent participants (skip applicant at index 0)
|
||||||
// (they already hold the insurance they selected themselves)
|
|
||||||
foreach ($assignments as $index => $insurance) {
|
foreach ($assignments as $index => $insurance) {
|
||||||
if (0 === $index && false === $isFamilyInsurance) {
|
if (0 === $index) {
|
||||||
continue;
|
continue; // Skip applicant
|
||||||
}
|
}
|
||||||
|
|
||||||
$participant = $bookingDto->getParticipant($index);
|
$participant = $bookingDto->getParticipant($index);
|
||||||
@@ -434,10 +407,9 @@ class BookingDataProcessor
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In edit mode: non-family bulk assignment must not overwrite an individually
|
// In edit mode: only apply bulk insurance if participant has no insurance
|
||||||
// locked insurance. Family insurance always overrides, since dependents can
|
// This respects the rule that once assigned, insurance cannot be changed
|
||||||
// never legitimately keep their own policy alongside it.
|
if (BookingDto::MODE_EDIT === $bookingDto->getMode() && null !== $participant->insurance) {
|
||||||
if (BookingDto::MODE_EDIT === $bookingDto->getMode() && false === $isFamilyInsurance && null !== $participant->insurance) {
|
|
||||||
continue; // Skip participants with existing insurance assignment
|
continue; // Skip participants with existing insurance assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ namespace App\BusProNet\DataProcessor;
|
|||||||
|
|
||||||
use App\BusProNet\Constants;
|
use App\BusProNet\Constants;
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
|
use App\BusProNet\Service\ParticipantStatusRuleRegistry;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds API payload structures for BusProNet booking requests.
|
* Builds API payload structures for BusProNet booking requests.
|
||||||
@@ -17,10 +17,9 @@ use App\Form\Model\ParticipantDto;
|
|||||||
*/
|
*/
|
||||||
class BookingPayloadBuilder
|
class BookingPayloadBuilder
|
||||||
{
|
{
|
||||||
private const DEFAULT_PARTICIPANT_STATUS = 'F';
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ServiceMappingCollector $mappingCollector,
|
private readonly ServiceMappingCollector $mappingCollector,
|
||||||
|
private readonly ParticipantStatusRuleRegistry $statusRuleRegistry,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ class BookingPayloadBuilder
|
|||||||
*
|
*
|
||||||
* @param Booking $bookingData The booking data object
|
* @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
|
public function buildBasePayload(Booking $bookingData): array
|
||||||
{
|
{
|
||||||
@@ -67,8 +66,8 @@ class BookingPayloadBuilder
|
|||||||
*
|
*
|
||||||
* Bank account information is required for direct debit payments.
|
* Bank account information is required for direct debit payments.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $payload The payload array to modify
|
* @param array $payload The payload array to modify
|
||||||
* @param Booking $bookingData The booking data object
|
* @param Booking $bookingData The booking data object
|
||||||
*/
|
*/
|
||||||
public function addBankAccountToPayload(array &$payload, Booking $bookingData): void
|
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.
|
* Includes status, personal data, and wishes (room remarks, license plate) for each participant.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $payload The payload array to modify
|
* @param array $payload The payload array to modify
|
||||||
* @param Booking $bookingData The booking data object
|
* @param Booking $bookingData The booking data object
|
||||||
* @param array<int, ParticipantDto> $participantDtos The participant DTOs from the form (for wishes data)
|
* @param array $participantDtos The participant DTOs from the form (for wishes data)
|
||||||
*/
|
*/
|
||||||
public function buildParticipantPayload(array &$payload, Booking $bookingData, array $participantDtos): void
|
public function buildParticipantPayload(array &$payload, Booking $bookingData, array $participantDtos): void
|
||||||
{
|
{
|
||||||
@@ -142,8 +141,8 @@ class BookingPayloadBuilder
|
|||||||
* Includes additional services, transportation services, and accommodation details
|
* Includes additional services, transportation services, and accommodation details
|
||||||
* with participant mappings and quantities.
|
* with participant mappings and quantities.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $payload The payload array to modify
|
* @param array $payload The payload array to modify
|
||||||
* @param Booking $bookingData The booking data object
|
* @param Booking $bookingData The booking data object
|
||||||
*/
|
*/
|
||||||
public function buildServicesPayload(array &$payload, Booking $bookingData): void
|
public function buildServicesPayload(array &$payload, Booking $bookingData): void
|
||||||
{
|
{
|
||||||
@@ -171,8 +170,8 @@ class BookingPayloadBuilder
|
|||||||
'@idzimmer' => $room->id,
|
'@idzimmer' => $room->id,
|
||||||
'@kategorie' => $room->category,
|
'@kategorie' => $room->category,
|
||||||
'@idverpflegung' => $room->boardId,
|
'@idverpflegung' => $room->boardId,
|
||||||
'@anreise' => $room->dateFrom?->format('d.m.Y'),
|
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
||||||
'@abreise' => $room->dateTo?->format('d.m.Y'),
|
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
|
||||||
'@anzahl' => $room->totalCount,
|
'@anzahl' => $room->totalCount,
|
||||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
];
|
];
|
||||||
@@ -199,8 +198,8 @@ class BookingPayloadBuilder
|
|||||||
*
|
*
|
||||||
* Only included in payload if there are actual pickup assignments for bus transportation.
|
* Only included in payload if there are actual pickup assignments for bus transportation.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $payload The payload array to modify
|
* @param array $payload The payload array to modify
|
||||||
* @param Booking $bookingData The booking data object
|
* @param Booking $bookingData The booking data object
|
||||||
*/
|
*/
|
||||||
public function buildPickupPayload(array &$payload, Booking $bookingData): void
|
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.
|
* 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 array $payload The payload array to modify
|
||||||
* @param Booking $bookingData The booking data object
|
* @param Booking $bookingData The booking data object
|
||||||
*/
|
*/
|
||||||
public function buildDropOffPayload(array &$payload, Booking $bookingData): void
|
public function buildDropOffPayload(array &$payload, Booking $bookingData): void
|
||||||
{
|
{
|
||||||
@@ -250,7 +249,7 @@ class BookingPayloadBuilder
|
|||||||
* @param BookingDto $bookingDto The booking creation form data
|
* @param BookingDto $bookingDto The booking creation form data
|
||||||
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
|
* @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
|
public function buildCreatePayload(BookingDto $bookingDto, string $bookingType): array
|
||||||
{
|
{
|
||||||
@@ -303,7 +302,7 @@ class BookingPayloadBuilder
|
|||||||
foreach ($bookingDto->participants as $index => $participant) {
|
foreach ($bookingDto->participants as $index => $participant) {
|
||||||
$participantData = [
|
$participantData = [
|
||||||
'@id' => $index + 1,
|
'@id' => $index + 1,
|
||||||
'status' => self::DEFAULT_PARTICIPANT_STATUS,
|
'status' => $this->statusRuleRegistry->evaluateStatus($participant),
|
||||||
'name' => $participant->lastName,
|
'name' => $participant->lastName,
|
||||||
'vorname' => $participant->firstName,
|
'vorname' => $participant->firstName,
|
||||||
'geschlecht' => $participant->gender ?? '',
|
'geschlecht' => $participant->gender ?? '',
|
||||||
@@ -440,11 +439,11 @@ class BookingPayloadBuilder
|
|||||||
* Generic helper method that converts service ID => participant IDs mappings
|
* Generic helper method that converts service ID => participant IDs mappings
|
||||||
* into XML payload structure.
|
* into XML payload structure.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $payload The payload array to modify
|
* @param array $payload The payload array to modify
|
||||||
* @param string $sectionKey The section key (e.g., 'beförderungen', 'versicherungen')
|
* @param string $sectionKey The section key (e.g., 'beförderungen', 'versicherungen')
|
||||||
* @param string $itemKey The item key (e.g., 'beförderung', 'versicherung')
|
* @param string $itemKey The item key (e.g., 'beförderung', 'versicherung')
|
||||||
* @param string $idAttributeName The ID attribute name (e.g., '@idleistung', '@idversicherung')
|
* @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 $serviceMap Map of service ID to participant IDs
|
||||||
*/
|
*/
|
||||||
public function addServicesFromMap(
|
public function addServicesFromMap(
|
||||||
array &$payload,
|
array &$payload,
|
||||||
@@ -476,9 +475,9 @@ class BookingPayloadBuilder
|
|||||||
* - abreise (departure date)
|
* - abreise (departure date)
|
||||||
* - anzahl (number of rooms of this type booked)
|
* - anzahl (number of rooms of this type booked)
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $payload The payload array to modify
|
* @param array $payload The payload array to modify
|
||||||
* @param array<int|string, int[]> $roomMap Map of room ID to participant IDs
|
* @param array $roomMap Map of room ID to participant IDs
|
||||||
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
|
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
|
||||||
*/
|
*/
|
||||||
public function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingDto $bookingDto): void
|
public function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingDto $bookingDto): void
|
||||||
{
|
{
|
||||||
@@ -521,8 +520,8 @@ class BookingPayloadBuilder
|
|||||||
/**
|
/**
|
||||||
* Adds purchase vouchers to the payload.
|
* Adds purchase vouchers to the payload.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $payload The payload array to modify
|
* @param array $payload The payload array to modify
|
||||||
* @param BookingDto $bookingDto The booking data
|
* @param BookingDto $bookingDto The booking data
|
||||||
*/
|
*/
|
||||||
public function addPurchaseVouchersToPayload(array &$payload, BookingDto $bookingDto): void
|
public function addPurchaseVouchersToPayload(array &$payload, BookingDto $bookingDto): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\BusProNet\DataProcessor;
|
namespace App\BusProNet\DataProcessor;
|
||||||
|
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
use App\BusProNet\Model\Service;
|
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -121,7 +120,7 @@ class ParticipantServiceProcessor
|
|||||||
*
|
*
|
||||||
* @param ParticipantDto $participant The participant data
|
* @param ParticipantDto $participant The participant data
|
||||||
*
|
*
|
||||||
* @return array<Service> Array of services selected by this participant
|
* @return array<\App\BusProNet\Model\Service> Array of services selected by this participant
|
||||||
*/
|
*/
|
||||||
private function collectParticipantAdditionalServices(ParticipantDto $participant): array
|
private function collectParticipantAdditionalServices(ParticipantDto $participant): array
|
||||||
{
|
{
|
||||||
@@ -132,11 +131,6 @@ class ParticipantServiceProcessor
|
|||||||
...$participant->rentals,
|
...$participant->rentals,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Veg is encoded as an additional service in the edit/update payload.
|
|
||||||
if (null !== $participant->veg) {
|
|
||||||
$services[] = $participant->veg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ski pass if selected (single service, not an array)
|
// Add ski pass if selected (single service, not an array)
|
||||||
if (null !== $participant->skiPass) {
|
if (null !== $participant->skiPass) {
|
||||||
$services[] = $participant->skiPass;
|
$services[] = $participant->skiPass;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\BusProNet\DataProcessor;
|
|||||||
|
|
||||||
use App\BusProNet\Model\Address;
|
use App\BusProNet\Model\Address;
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
|
use App\BusProNet\Model\Communication;
|
||||||
use App\BusProNet\Model\PersonalData;
|
use App\BusProNet\Model\PersonalData;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
|
|
||||||
@@ -85,6 +86,9 @@ class PersonalDataSynchronizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($participant->email || $participant->mobile) {
|
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->email = $participant->email;
|
||||||
$bookingData->participants[$participant->index]->communication->mobile = $participant->mobile;
|
$bookingData->participants[$participant->index]->communication->mobile = $participant->mobile;
|
||||||
}
|
}
|
||||||
@@ -123,6 +127,10 @@ class PersonalDataSynchronizer
|
|||||||
$applicant->dateOfBirth = new \DateTimeImmutable('-20 years');
|
$applicant->dateOfBirth = new \DateTimeImmutable('-20 years');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (null === $applicant->communication) {
|
||||||
|
$applicant->communication = new Communication();
|
||||||
|
}
|
||||||
|
|
||||||
if (null === $applicant->communication->mobile || '' === $applicant->communication->mobile) {
|
if (null === $applicant->communication->mobile || '' === $applicant->communication->mobile) {
|
||||||
$applicant->communication->mobile = '12345';
|
$applicant->communication->mobile = '12345';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class ServiceMappingCollector
|
|||||||
*
|
*
|
||||||
* Groups participants by their assigned room ID.
|
* 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
|
public function collectRoomMappings(BookingDto $bookingDto): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class CountryDataProvider
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, Country> */
|
|
||||||
public function getAll(): array
|
public function getAll(): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
|||||||
use Symfony\Component\OptionsResolver\Options;
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
/** @extends AbstractType<string> */
|
|
||||||
class CountryType extends AbstractType
|
class CountryType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private readonly CountryDataProvider $countries)
|
public function __construct(private readonly CountryDataProvider $countries)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace App\BusProNet\Model;
|
namespace App\BusProNet\Model;
|
||||||
|
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use function Symfony\Component\String\u;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a physical address with street, postal code, city, and country information.
|
* Represents a physical address with street, postal code, city, and country information.
|
||||||
@@ -30,21 +29,6 @@ class Address
|
|||||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||||
public ?string $country = null;
|
public ?string $country = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes legacy string data loaded from API/session sources.
|
|
||||||
*
|
|
||||||
* Trims whitespace and converts empty strings to null so load-time audits
|
|
||||||
* and form hydration see canonical values.
|
|
||||||
*/
|
|
||||||
public function normalize(): void
|
|
||||||
{
|
|
||||||
$this->street = $this->normalizeNullableString($this->street);
|
|
||||||
$this->postCode = $this->normalizeNullableString($this->postCode);
|
|
||||||
$this->city = $this->normalizeNullableString($this->city);
|
|
||||||
$this->district = $this->normalizeNullableString($this->district);
|
|
||||||
$this->country = $this->normalizeNullableString($this->country);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the address to API payload format.
|
* Converts the address to API payload format.
|
||||||
*
|
*
|
||||||
@@ -63,15 +47,4 @@ class Address
|
|||||||
'land' => $this->country,
|
'land' => $this->country,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeNullableString(?string $value): ?string
|
|
||||||
{
|
|
||||||
if (null === $value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$trimmed = u($value)->trim()->toString();
|
|
||||||
|
|
||||||
return '' === $trimmed ? null : $trimmed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,6 @@ namespace App\BusProNet\Model;
|
|||||||
*/
|
*/
|
||||||
class AgeConstraintResult
|
class AgeConstraintResult
|
||||||
{
|
{
|
||||||
/** @param array<string, mixed> $metadata */
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $type,
|
public readonly string $type,
|
||||||
public readonly ?int $ageFrom = null,
|
public readonly ?int $ageFrom = null,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ namespace App\BusProNet\Model;
|
|||||||
*/
|
*/
|
||||||
class BaseData
|
class BaseData
|
||||||
{
|
{
|
||||||
/** @param array<array-key, mixed> $items */
|
|
||||||
public function __construct(private readonly array $items)
|
public function __construct(private readonly array $items)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -20,7 +19,7 @@ class BaseData
|
|||||||
/**
|
/**
|
||||||
* Retrieves all items in the base data collection.
|
* 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
|
public function getItems(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ use App\BusProNet\Constants;
|
|||||||
*/
|
*/
|
||||||
class Booking
|
class Booking
|
||||||
{
|
{
|
||||||
public const string STATUS_CANCELED = 'S';
|
|
||||||
|
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
public ?int $agencyId = null;
|
public ?int $agencyId = null;
|
||||||
public ?int $bookingNumber = null;
|
public ?int $bookingNumber = null;
|
||||||
@@ -38,50 +36,14 @@ class Booking
|
|||||||
public ?string $paymentType = null;
|
public ?string $paymentType = null;
|
||||||
public ?string $paymentLabel = null;
|
public ?string $paymentLabel = null;
|
||||||
public ?BankAccount $bankAccount = null;
|
public ?BankAccount $bankAccount = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
public array $participantsStatus = [];
|
public array $participantsStatus = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, PersonalData>
|
|
||||||
*/
|
|
||||||
public array $participants = [];
|
public array $participants = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, Service>
|
|
||||||
*/
|
|
||||||
public array $transportationServices = [];
|
public array $transportationServices = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, Service>
|
|
||||||
*/
|
|
||||||
public array $additionalServices = [];
|
public array $additionalServices = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, Room>
|
|
||||||
*/
|
|
||||||
public array $rooms = [];
|
public array $rooms = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, Pickup>
|
|
||||||
*/
|
|
||||||
public array $pickups = [];
|
public array $pickups = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, Pickup>
|
|
||||||
*/
|
|
||||||
public array $dropOffs = [];
|
public array $dropOffs = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, Surcharge>
|
|
||||||
*/
|
|
||||||
public array $surcharges = [];
|
public array $surcharges = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<string|int, Insurance>
|
|
||||||
*/
|
|
||||||
public array $insurances = [];
|
public array $insurances = [];
|
||||||
public ?int $invoiceNumber = null;
|
public ?int $invoiceNumber = null;
|
||||||
public ?float $totalPrice = null;
|
public ?float $totalPrice = null;
|
||||||
@@ -222,7 +184,7 @@ class Booking
|
|||||||
public function getInsuranceForParticipant(int $participantIndex): ?Insurance
|
public function getInsuranceForParticipant(int $participantIndex): ?Insurance
|
||||||
{
|
{
|
||||||
foreach ($this->insurances as $insurance) {
|
foreach ($this->insurances as $insurance) {
|
||||||
if (in_array($participantIndex, $insurance->mapping, true)) {
|
if (in_array($participantIndex, $insurance->mapping ?? [], true)) {
|
||||||
return $insurance;
|
return $insurance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ namespace App\BusProNet\Model;
|
|||||||
*/
|
*/
|
||||||
class BookingResponse
|
class BookingResponse
|
||||||
{
|
{
|
||||||
public const string BOOKING_POSSIBLE = 'möglich';
|
|
||||||
public const string BOOKING_IMPOSSIBLE = 'nicht möglich';
|
|
||||||
public const string BOOKING_PROCESSED = 'erfolgt';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $status Booking status (möglich|erfolgt)
|
* @param string $status Booking status (möglich|erfolgt)
|
||||||
* @param int|null $bookingNumber Booking number (BPN XML: vorgang)
|
* @param int|null $bookingNumber Booking number (BPN XML: vorgang)
|
||||||
@@ -41,7 +37,7 @@ class BookingResponse
|
|||||||
*/
|
*/
|
||||||
public function isInquiryValid(): bool
|
public function isInquiryValid(): bool
|
||||||
{
|
{
|
||||||
return self::BOOKING_POSSIBLE === $this->status;
|
return 'möglich' === $this->status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +45,7 @@ class BookingResponse
|
|||||||
*/
|
*/
|
||||||
public function isBookingSuccessful(): bool
|
public function isBookingSuccessful(): bool
|
||||||
{
|
{
|
||||||
return self::BOOKING_PROCESSED === $this->status;
|
return 'erfolgt' === $this->status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,23 +12,8 @@ namespace App\BusProNet\Model;
|
|||||||
*/
|
*/
|
||||||
class CrmAttributes
|
class CrmAttributes
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var array<int, CrmSelectionGroup>|null
|
|
||||||
*/
|
|
||||||
public ?array $selectionGroups = null;
|
public ?array $selectionGroups = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, CrmAction>|null
|
|
||||||
*/
|
|
||||||
public ?array $crmActions = null;
|
public ?array $crmActions = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
public array $hotelCodes = [];
|
public array $hotelCodes = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
public array $roles = [];
|
public array $roles = [];
|
||||||
}
|
}
|
||||||
@@ -15,15 +15,15 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
*/
|
*/
|
||||||
class CrmSelection
|
class CrmSelection
|
||||||
{
|
{
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $mutable = false;
|
public bool $mutable = false;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $selected = false;
|
public bool $selected = false;
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ class CrmSelectionGroup
|
|||||||
* Maps BusPro IDs of CRM selection groups representing
|
* Maps BusPro IDs of CRM selection groups representing
|
||||||
* included services.
|
* included services.
|
||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
public static array $includedServicesMapping = [
|
public static array $includedServicesMapping = [
|
||||||
7 => 'Skipass',
|
7 => 'Skipass',
|
||||||
@@ -32,16 +32,13 @@ class CrmSelectionGroup
|
|||||||
90 => 'Transport',
|
90 => 'Transport',
|
||||||
];
|
];
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
|
|
||||||
/**
|
#[Groups(['api:single'])]
|
||||||
* @var array<int, CrmSelection>|null
|
|
||||||
*/
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public ?array $selections = null;
|
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
|
class Guide
|
||||||
{
|
{
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?string $name = null;
|
public ?string $name = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?string $phone = null;
|
public ?string $phone = null;
|
||||||
}
|
}
|
||||||
@@ -15,24 +15,24 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
*/
|
*/
|
||||||
class Hotel
|
class Hotel
|
||||||
{
|
{
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $code = null;
|
public ?string $code = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $name = null;
|
public ?string $name = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $city = null;
|
public ?string $city = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $street = null;
|
public ?string $street = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $phone = null;
|
public ?string $phone = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $country = null;
|
public ?string $country = null;
|
||||||
}
|
}
|
||||||
@@ -56,9 +56,7 @@ class PersonalData
|
|||||||
#[Assert\Valid(groups: ['personal_data'])]
|
#[Assert\Valid(groups: ['personal_data'])]
|
||||||
public Communication $communication;
|
public Communication $communication;
|
||||||
|
|
||||||
/** @var list<string> */
|
|
||||||
public array $roles = [];
|
public array $roles = [];
|
||||||
/** @var list<string> */
|
|
||||||
public array $hotelCodes = [];
|
public array $hotelCodes = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\BusProNet\Model;
|
|
||||||
|
|
||||||
class PersonalDataUpdateResponse
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly ?int $addressId,
|
|
||||||
public readonly ?int $personId,
|
|
||||||
public readonly bool $changed,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,35 +17,34 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
*/
|
*/
|
||||||
class Pickup
|
class Pickup
|
||||||
{
|
{
|
||||||
#[Groups(['api:list', 'snapshot'])]
|
#[Groups(['api:list'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'snapshot'])]
|
#[Groups(['api:list'])]
|
||||||
public ?string $code = null;
|
public ?string $code = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'snapshot'])]
|
#[Groups(['api:list'])]
|
||||||
public ?string $city = null;
|
public ?string $city = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'snapshot'])]
|
#[Groups(['api:list'])]
|
||||||
public ?string $postalCode = null;
|
public ?string $postalCode = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'snapshot'])]
|
#[Groups(['api:list'])]
|
||||||
public ?string $street = null;
|
public ?string $street = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i'])]
|
||||||
public ?\DateTimeImmutable $time = null;
|
public ?\DateTimeImmutable $time = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?float $price = null;
|
public ?float $price = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?float $priceOutbound = null;
|
public ?float $priceOutbound = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?float $priceInbound = null;
|
public ?float $priceInbound = null;
|
||||||
|
|
||||||
/** @var list<int> */
|
|
||||||
#[Groups(['api:booking'])]
|
#[Groups(['api:booking'])]
|
||||||
public array $mapping = [];
|
public array $mapping = [];
|
||||||
|
|
||||||
|
|||||||
@@ -20,19 +20,19 @@ class Room
|
|||||||
public const SELECTION_TYPE_BY_PAX = 'by_pax';
|
public const SELECTION_TYPE_BY_PAX = 'by_pax';
|
||||||
public const SELECTION_TYPE_BY_ROOM = 'by_room';
|
public const SELECTION_TYPE_BY_ROOM = 'by_room';
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $category = null;
|
public ?string $category = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $boardId = null;
|
public ?int $boardId = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $code = null;
|
public ?string $code = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
|
|
||||||
#[Groups(['booking'])]
|
#[Groups(['booking'])]
|
||||||
@@ -46,35 +46,33 @@ class Room
|
|||||||
#[Groups(['booking'])]
|
#[Groups(['booking'])]
|
||||||
public ?int $totalCount = null;
|
public ?int $totalCount = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $minPax = null;
|
public ?int $minPax = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $maxPax = null;
|
public ?int $maxPax = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $nights = null;
|
public ?int $nights = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $status = null;
|
public ?string $status = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $available = null;
|
public ?int $available = null;
|
||||||
|
|
||||||
#[Groups(['booking'])]
|
#[Groups(['booking'])]
|
||||||
public ?string $board = null;
|
public ?string $board = null;
|
||||||
|
|
||||||
/** @var list<int> */
|
|
||||||
#[Groups(['booking'])]
|
#[Groups(['booking'])]
|
||||||
public array $mapping = [];
|
public array $mapping = [];
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?float $price = null;
|
public ?float $price = null;
|
||||||
|
|
||||||
#[Groups(['booking'])]
|
#[Groups(['booking'])]
|
||||||
public ?float $totalPrice = null;
|
public ?float $totalPrice = null;
|
||||||
|
|
||||||
/** @var array<int, float|null> */
|
|
||||||
#[Groups(['booking'])]
|
#[Groups(['booking'])]
|
||||||
public array $individualPrice = [];
|
public array $individualPrice = [];
|
||||||
|
|
||||||
|
|||||||
@@ -17,90 +17,84 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
*/
|
*/
|
||||||
class Service
|
class Service
|
||||||
{
|
{
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $category = null;
|
public ?string $category = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $status = null;
|
public ?string $status = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public bool $mandatory = false;
|
public bool $mandatory = false;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public bool $autoBook = false;
|
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
public ?\DateTimeImmutable $dateFrom = null;
|
public ?\DateTimeImmutable $dateFrom = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $timeFrom = null;
|
public ?string $timeFrom = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $dayTime = null;
|
public ?string $dayTime = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
public ?\DateTimeImmutable $dateTo = null;
|
public ?\DateTimeImmutable $dateTo = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $subType = null;
|
public ?string $subType = null;
|
||||||
|
|
||||||
#[Groups(['booking'])]
|
#[Groups(['booking'])]
|
||||||
public ?int $totalCount = null;
|
public ?int $totalCount = null;
|
||||||
|
|
||||||
/** @var list<int> */
|
#[Groups(['api:single'])]
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $mapping = [];
|
public array $mapping = [];
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?float $price = null;
|
public ?float $price = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?float $totalPrice = null;
|
public ?float $totalPrice = null;
|
||||||
|
|
||||||
/** @var array<int, float|null> */
|
#[Groups(['api:single'])]
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $individualPrice = [];
|
public array $individualPrice = [];
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?int $available = null;
|
public ?int $available = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $source = null;
|
public ?string $source = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $direction = null;
|
public ?string $direction = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?int $ageFrom = null;
|
public ?int $ageFrom = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?int $ageTo = null;
|
public ?int $ageTo = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?int $birthYearFrom = null;
|
public ?int $birthYearFrom = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?int $birthYearTo = null;
|
public ?int $birthYearTo = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $ageConstraintType = null;
|
public ?string $ageConstraintType = null;
|
||||||
|
|
||||||
/** @var array<string, mixed>|null */
|
|
||||||
#[Groups(['api:single'])]
|
#[Groups(['api:single'])]
|
||||||
public ?array $ageConstraintMetadata = null;
|
public ?array $ageConstraintMetadata = null;
|
||||||
|
|
||||||
#[Groups(['api:single'])]
|
#[Groups(['api:single'])]
|
||||||
public ?string $rawAgeConstraintData = null;
|
public ?string $rawAgeConstraintData = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $description = null;
|
public ?string $description = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,6 +104,6 @@ class Service
|
|||||||
* When false, this service's price is excluded when determining which insurance
|
* When false, this service's price is excluded when determining which insurance
|
||||||
* tier is appropriate for a participant.
|
* tier is appropriate for a participant.
|
||||||
*/
|
*/
|
||||||
#[Groups(['api:single', 'api:list', 'snapshot'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public bool $includeInInsuranceCalculation = true;
|
public bool $includeInInsuranceCalculation = true;
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,7 @@ namespace App\BusProNet\Model;
|
|||||||
class Surcharge
|
class Surcharge
|
||||||
{
|
{
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
/** @var list<int> */
|
|
||||||
public array $mapping = [];
|
public array $mapping = [];
|
||||||
public ?float $totalPrice = null;
|
public ?float $totalPrice = null;
|
||||||
/** @var array<int, float|null> */
|
|
||||||
public array $individualPrice = [];
|
public array $individualPrice = [];
|
||||||
}
|
}
|
||||||
@@ -20,43 +20,40 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|||||||
class Travel
|
class Travel
|
||||||
{
|
{
|
||||||
use SortByPriceTrait;
|
use SortByPriceTrait;
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $code = null;
|
public ?string $code = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $productCode = null;
|
public ?string $productCode = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $productId = null;
|
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?int $hotelId = null;
|
public ?int $hotelId = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?Hotel $hotel = null;
|
public ?Hotel $hotel = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
public ?\DateTimeImmutable $dateFrom = null;
|
public ?\DateTimeImmutable $dateFrom = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
public ?\DateTimeImmutable $dateTo = null;
|
public ?\DateTimeImmutable $dateTo = null;
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?string $type = null;
|
public ?string $type = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Travel booking status from BusProNet API.
|
* Travel booking status from BusProNet API.
|
||||||
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'.
|
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'.
|
||||||
*/
|
*/
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?string $status = null;
|
public ?string $status = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,71 +62,53 @@ class Travel
|
|||||||
*
|
*
|
||||||
* @var array<string>
|
* @var array<string>
|
||||||
*/
|
*/
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public array $allowedBookingStatus = [];
|
public array $allowedBookingStatus = [];
|
||||||
|
|
||||||
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
#[Groups(['api:list', 'api:single'])]
|
||||||
public ?float $priceFrom = null;
|
public ?float $priceFrom = null;
|
||||||
|
|
||||||
/**
|
#[Groups(['api:single'])]
|
||||||
* @var array<int, CrmSelectionGroup>
|
|
||||||
*/
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $selectionGroups = [];
|
public array $selectionGroups = [];
|
||||||
|
|
||||||
/**
|
#[Groups(['api:single'])]
|
||||||
* @var array<int, Service>
|
|
||||||
*/
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $additionalServices = [];
|
public array $additionalServices = [];
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $additionalServicesMutable = true;
|
public bool $additionalServicesMutable = true;
|
||||||
|
|
||||||
/**
|
#[Groups(['api:single'])]
|
||||||
* @var array<int, Service>
|
|
||||||
*/
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $transportationServices = [];
|
public array $transportationServices = [];
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $transportationServicesMutable = true;
|
public bool $transportationServicesMutable = true;
|
||||||
|
|
||||||
/**
|
#[Groups(['api:single'])]
|
||||||
* @var array<int, Pickup>
|
|
||||||
*/
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $pickups = [];
|
public array $pickups = [];
|
||||||
|
|
||||||
/**
|
#[Groups(['api:single'])]
|
||||||
* @var array<int, Pickup>
|
|
||||||
*/
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $dropOffs = [];
|
public array $dropOffs = [];
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $pickupsMutable = true;
|
public bool $pickupsMutable = true;
|
||||||
|
|
||||||
/**
|
#[Groups(['api:single'])]
|
||||||
* @var array<int, Room>
|
|
||||||
*/
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
|
||||||
public array $rooms = [];
|
public array $rooms = [];
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $roomsMutable = true;
|
public bool $roomsMutable = true;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $participantDataMutable = true;
|
public bool $participantDataMutable = true;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public bool $participantCountMutable = true;
|
public bool $participantCountMutable = true;
|
||||||
|
|
||||||
#[Groups(['api:single', 'snapshot'])]
|
#[Groups(['api:single'])]
|
||||||
public ?Guide $guide = null;
|
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'])]
|
#[Groups(['api:single'])]
|
||||||
public array $insurances = [];
|
public array $insurances = [];
|
||||||
@@ -203,7 +182,7 @@ class Travel
|
|||||||
* Maps selection group IDs to their corresponding services based on
|
* Maps selection group IDs to their corresponding services based on
|
||||||
* the predefined included services mapping.
|
* 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
|
public function getIncludedServices(): array
|
||||||
{
|
{
|
||||||
@@ -218,28 +197,6 @@ class Travel
|
|||||||
return $services;
|
return $services;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether any CRM selection group contains the given selection ID.
|
|
||||||
*
|
|
||||||
* This scans all parsed groups and their selections, regardless of group.
|
|
||||||
*/
|
|
||||||
public function hasSelectionId(int $selectionId): bool
|
|
||||||
{
|
|
||||||
foreach ($this->selectionGroups as $selectionGroup) {
|
|
||||||
if (null === $selectionGroup->selections) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($selectionGroup->selections as $selection) {
|
|
||||||
if ($selectionId === $selection->id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves rooms filtered by their IDs.
|
* Retrieves rooms filtered by their IDs.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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;
|
namespace App\BusProNet\Service\StatusRule;
|
||||||
|
|
||||||
use App\BusProNet\Model\Service;
|
use App\BusProNet\Model\Service;
|
||||||
use App\BusProNet\Service\Contract\BookingStatusRuleInterface;
|
use App\BusProNet\Service\Contract\ParticipantStatusRuleInterface;
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Model\ParticipantDto;
|
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
|
* Assigns 'Option' status to participants who have selected any service
|
||||||
* service containing 'Begleitperson' in its label.
|
* 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 STATUS = 'O';
|
||||||
private const PRIORITY = 100;
|
private const PRIORITY = 100;
|
||||||
private const SEARCH_TERM = 'begleitperson';
|
private const SEARCH_TERM = 'begleitperson';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see BookingStatusRuleInterface::evaluate()
|
* @see ParticipantStatusRuleInterface::evaluate()
|
||||||
*/
|
*/
|
||||||
public function evaluate(BookingDto $bookingDto): bool
|
public function evaluate(ParticipantDto $participant): bool
|
||||||
{
|
|
||||||
foreach ($bookingDto->participants as $participant) {
|
|
||||||
if (true === $this->participantHasChaperonService($participant)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function participantHasChaperonService(ParticipantDto $participant): bool
|
|
||||||
{
|
{
|
||||||
// Check array services
|
// Check array services
|
||||||
$arrayServices = [
|
$arrayServices = [
|
||||||
@@ -47,7 +36,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
|||||||
|
|
||||||
foreach ($arrayServices as $services) {
|
foreach ($arrayServices as $services) {
|
||||||
foreach ($services as $service) {
|
foreach ($services as $service) {
|
||||||
if (true === $this->containsSearchTerm($service->label)) {
|
if ($service instanceof Service && true === $this->containsSearchTerm($service->label)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +58,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see BookingStatusRuleInterface::getStatus()
|
* @see ParticipantStatusRuleInterface::getStatus()
|
||||||
*/
|
*/
|
||||||
public function getStatus(): string
|
public function getStatus(): string
|
||||||
{
|
{
|
||||||
@@ -77,7 +66,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see BookingStatusRuleInterface::getPriority()
|
* @see ParticipantStatusRuleInterface::getPriority()
|
||||||
*/
|
*/
|
||||||
public function getPriority(): int
|
public function getPriority(): int
|
||||||
{
|
{
|
||||||
@@ -85,7 +74,7 @@ class ChaperonServiceStatusRule implements BookingStatusRuleInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see BookingStatusRuleInterface::getDescription()
|
* @see ParticipantStatusRuleInterface::getDescription()
|
||||||
*/
|
*/
|
||||||
public function getDescription(): string
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\BusProNet\Service\StatusRule;
|
|
||||||
|
|
||||||
use App\BusProNet\Service\Contract\BookingStatusRuleInterface;
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status rule for travels containing selection 1473.
|
|
||||||
*
|
|
||||||
* Forces Option status when the travel contains the CRM selection
|
|
||||||
* "NICHT-automatisch bestätigen".
|
|
||||||
*/
|
|
||||||
class Selection1473StatusRule implements BookingStatusRuleInterface
|
|
||||||
{
|
|
||||||
private const STATUS = 'O';
|
|
||||||
private const PRIORITY = 200;
|
|
||||||
private const SELECTION_ID = 1473;
|
|
||||||
|
|
||||||
public function evaluate(BookingDto $bookingDto): bool
|
|
||||||
{
|
|
||||||
return $bookingDto->travel->hasSelectionId(self::SELECTION_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getStatus(): string
|
|
||||||
{
|
|
||||||
return self::STATUS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPriority(): int
|
|
||||||
{
|
|
||||||
return self::PRIORITY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Assigns Option status when selection 1473 is present';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,9 +28,9 @@ trait SortByPriceTrait
|
|||||||
* collections in the application. Items are sorted from lowest to highest price,
|
* collections in the application. Items are sorted from lowest to highest price,
|
||||||
* with null prices being treated as zero (appearing first).
|
* 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
|
protected function sortByPrice(array $items): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use Carbon\Exceptions\InvalidFormatException;
|
|||||||
|
|
||||||
trait TypeConversionTrait
|
trait TypeConversionTrait
|
||||||
{
|
{
|
||||||
/** @return list<string> */
|
|
||||||
protected function stringToArray(?string $string, string $separator = ','): array
|
protected function stringToArray(?string $string, string $separator = ','): array
|
||||||
{
|
{
|
||||||
if (true === empty($string)) {
|
if (true === empty($string)) {
|
||||||
@@ -19,7 +18,7 @@ trait TypeConversionTrait
|
|||||||
|
|
||||||
protected function stringToFloat(?string $string): ?float
|
protected function stringToFloat(?string $string): ?float
|
||||||
{
|
{
|
||||||
if (null === $string || '' === $string) {
|
if (true === empty($string)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\BusProNet\Traits;
|
||||||
|
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
trait XmlParserTrait
|
||||||
|
{
|
||||||
|
protected function getStringOrNullValue(Crawler $node): ?string
|
||||||
|
{
|
||||||
|
return 0 < $node->count() ? $node->text() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getIntOrNullValue(Crawler $node): ?int
|
||||||
|
{
|
||||||
|
return 0 < $node->count() ? (int) $node->text() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,27 +3,18 @@
|
|||||||
namespace App\BusProNet\XmlLoader;
|
namespace App\BusProNet\XmlLoader;
|
||||||
|
|
||||||
use App\BusProNet\Traits\TypeConversionTrait;
|
use App\BusProNet\Traits\TypeConversionTrait;
|
||||||
|
use App\BusProNet\Traits\XmlParserTrait;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\FilesystemOperator;
|
||||||
use Symfony\Component\DomCrawler\Crawler;
|
|
||||||
use Symfony\Contracts\Cache\CacheInterface;
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
|
||||||
abstract class AbstractLoader
|
abstract class AbstractLoader
|
||||||
{
|
{
|
||||||
use TypeConversionTrait;
|
use TypeConversionTrait;
|
||||||
|
use XmlParserTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected readonly CacheInterface $cache,
|
protected readonly CacheInterface $cache,
|
||||||
protected readonly FilesystemOperator $xmlExport,
|
protected readonly FilesystemOperator $xmlExport,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getStringOrNullValue(Crawler $node): ?string
|
|
||||||
{
|
|
||||||
return 0 < $node->count() ? $node->text() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getIntOrNullValue(Crawler $node): ?int
|
|
||||||
{
|
|
||||||
return 0 < $node->count() ? (int) $node->text() : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,11 @@ use Symfony\Contracts\Cache\ItemInterface;
|
|||||||
|
|
||||||
class HotelLoader extends AbstractLoader
|
class HotelLoader extends AbstractLoader
|
||||||
{
|
{
|
||||||
/** @return array<int, Hotel> */
|
|
||||||
public function loadAll(?string $filename = 'hotel.xml'): array
|
public function loadAll(?string $filename = 'hotel.xml'): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->cache->get('bpn_hotels', function (ItemInterface $item) use ($filename) {
|
return $this->cache->get('bpn_hotels', function (ItemInterface $item) use ($filename) {
|
||||||
$item->expiresAfter(3 * 60 * 60);
|
$item->expiresAfter(3 * 60 * 60);
|
||||||
$item->tag(['xml-sync']);
|
|
||||||
|
|
||||||
$hotels = [];
|
$hotels = [];
|
||||||
|
|
||||||
@@ -93,12 +91,6 @@ class HotelLoader extends AbstractLoader
|
|||||||
public function patchHotelDetails(Travel $travel): void
|
public function patchHotelDetails(Travel $travel): void
|
||||||
{
|
{
|
||||||
$hotelId = $travel->hotelId;
|
$hotelId = $travel->hotelId;
|
||||||
if (null === $hotelId) {
|
|
||||||
$travel->hotel = null;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$hotel = $this->loadById($hotelId);
|
$hotel = $this->loadById($hotelId);
|
||||||
$travel->hotel = $hotel;
|
$travel->hotel = $hotel;
|
||||||
|
|||||||
@@ -22,13 +22,11 @@ class InsuranceLoader extends AbstractLoader
|
|||||||
parent::__construct($cache, $xmlExport);
|
parent::__construct($cache, $xmlExport);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string|int, Insurance> */
|
|
||||||
public function loadAll(?string $filename = 'versicherungen.xml'): array
|
public function loadAll(?string $filename = 'versicherungen.xml'): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->cache->get('bpn_insurances', function (ItemInterface $item) use ($filename) {
|
return $this->cache->get('bpn_insurances', function (ItemInterface $item) use ($filename) {
|
||||||
$item->expiresAfter(3 * 60 * 60);
|
$item->expiresAfter(3 * 60 * 60);
|
||||||
$item->tag(['xml-sync']);
|
|
||||||
|
|
||||||
$crawler = $this->loadXml($filename);
|
$crawler = $this->loadXml($filename);
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ use Symfony\Contracts\Cache\ItemInterface;
|
|||||||
|
|
||||||
class PickupLoader extends AbstractLoader
|
class PickupLoader extends AbstractLoader
|
||||||
{
|
{
|
||||||
/** @return array<int|string, Pickup> */
|
|
||||||
public function loadAll(?string $filename = 'zustiege.xml'): array
|
public function loadAll(?string $filename = 'zustiege.xml'): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->cache->get('bpn_pickups', function (ItemInterface $item) use ($filename) {
|
return $this->cache->get('bpn_pickups', function (ItemInterface $item) use ($filename) {
|
||||||
$item->expiresAfter(3 * 60 * 60);
|
$item->expiresAfter(3 * 60 * 60);
|
||||||
$item->tag(['xml-sync']);
|
|
||||||
|
|
||||||
$crawler = $this->loadXml($filename);
|
$crawler = $this->loadXml($filename);
|
||||||
$pickupNodes = $crawler->filterXPath('//zustiege/zustieg');
|
$pickupNodes = $crawler->filterXPath('//zustiege/zustieg');
|
||||||
@@ -67,11 +65,6 @@ class PickupLoader extends AbstractLoader
|
|||||||
$travel->dropOffs = $this->patchAndOrderDropOffs($travel->dropOffs, $travel->pickups);
|
$travel->dropOffs = $this->patchAndOrderDropOffs($travel->dropOffs, $travel->pickups);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<int, Pickup> $pickups
|
|
||||||
*
|
|
||||||
* @return array<int, Pickup>
|
|
||||||
*/
|
|
||||||
private function patchAndSortPickups(array $pickups): array
|
private function patchAndSortPickups(array $pickups): array
|
||||||
{
|
{
|
||||||
$this->enrichPickupDetails($pickups);
|
$this->enrichPickupDetails($pickups);
|
||||||
@@ -144,9 +137,6 @@ class PickupLoader extends AbstractLoader
|
|||||||
{
|
{
|
||||||
foreach ($pickups as $pickup) {
|
foreach ($pickups as $pickup) {
|
||||||
$pickupData = $this->loadById($pickup->id);
|
$pickupData = $this->loadById($pickup->id);
|
||||||
if (null === $pickupData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pickup->code = $pickupData->code;
|
$pickup->code = $pickupData->code;
|
||||||
$pickup->postalCode = $pickupData->postalCode;
|
$pickup->postalCode = $pickupData->postalCode;
|
||||||
|
|||||||
@@ -51,14 +51,13 @@ class TravelLoader extends AbstractLoader
|
|||||||
* including ID, code, label, dates, and associated hotels. The mapping is
|
* including ID, code, label, dates, and associated hotels. The mapping is
|
||||||
* cached for 3 hours to improve performance.
|
* 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
|
public function generateFilesMap(): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->cache->get('bpn_travels_mapping', function (ItemInterface $item) {
|
return $this->cache->get('bpn_travels_mapping', function (ItemInterface $item) {
|
||||||
$item->expiresAfter(3 * 60 * 60);
|
$item->expiresAfter(3 * 60 * 60);
|
||||||
$item->tag(['xml-sync']);
|
|
||||||
|
|
||||||
$xmlFiles = $this
|
$xmlFiles = $this
|
||||||
->xmlExport
|
->xmlExport
|
||||||
@@ -75,7 +74,7 @@ class TravelLoader extends AbstractLoader
|
|||||||
$travelDataNodes = $crawler->filterXPath('//reisen/reise/termin');
|
$travelDataNodes = $crawler->filterXPath('//reisen/reise/termin');
|
||||||
|
|
||||||
$travelDataNodes->each(function (Crawler $node) use (&$mapping, $hotels, $file) {
|
$travelDataNodes->each(function (Crawler $node) use (&$mapping, $hotels, $file) {
|
||||||
$travelId = (int) $node->attr('idbuspro');
|
$travelId = $node->attr('idbuspro');
|
||||||
|
|
||||||
$mapping[$travelId] = [
|
$mapping[$travelId] = [
|
||||||
'id' => $travelId,
|
'id' => $travelId,
|
||||||
@@ -103,7 +102,7 @@ class TravelLoader extends AbstractLoader
|
|||||||
return $mapping;
|
return $mapping;
|
||||||
});
|
});
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
throw new \RuntimeException('Cache key error in generateFilesMap', 0, $e);
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +173,6 @@ class TravelLoader extends AbstractLoader
|
|||||||
*
|
*
|
||||||
* @throws TravelNotFoundException When travel ID is not found
|
* @throws TravelNotFoundException When travel ID is not found
|
||||||
* @throws HotelNotInTravelException When hotel ID exists but not for this travel
|
* @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
|
public function loadById(int $dateId, ?int $hotelId = null, ?string $filename = null): Travel
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,15 +9,6 @@ abstract class AbstractParser
|
|||||||
{
|
{
|
||||||
use TypeConversionTrait;
|
use TypeConversionTrait;
|
||||||
|
|
||||||
protected function getIntOrNullAttribute(?string $value): ?int
|
|
||||||
{
|
|
||||||
if (null === $value || '' === $value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getStringOrNullValue(Crawler $node): ?string
|
protected function getStringOrNullValue(Crawler $node): ?string
|
||||||
{
|
{
|
||||||
return 0 < $node->count() ? $node->text() : null;
|
return 0 < $node->count() ? $node->text() : null;
|
||||||
@@ -48,7 +39,6 @@ abstract class AbstractParser
|
|||||||
return 0 < $node->count() && $this->stringToBool($node->text());
|
return 0 < $node->count() && $this->stringToBool($node->text());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return list<string> */
|
|
||||||
protected function getArrayValue(Crawler $node, string $separator = ','): array
|
protected function getArrayValue(Crawler $node, string $separator = ','): array
|
||||||
{
|
{
|
||||||
if (0 === $node->count()) {
|
if (0 === $node->count()) {
|
||||||
@@ -57,23 +47,4 @@ abstract class AbstractParser
|
|||||||
|
|
||||||
return $this->stringToArray($node->text(), $separator);
|
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);
|
return $this->mergeConstraintResults($results, $constraintData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param list<AgeConstraintResult> $results */
|
|
||||||
private function mergeConstraintResults(array $results, string $rawData): AgeConstraintResult
|
private function mergeConstraintResults(array $results, string $rawData): AgeConstraintResult
|
||||||
{
|
{
|
||||||
if (empty($results)) {
|
if (empty($results)) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ class ApiResponseParser extends AbstractParser
|
|||||||
/**
|
/**
|
||||||
* @throws ResponseParserException
|
* @throws ResponseParserException
|
||||||
*/
|
*/
|
||||||
/** @param array<string, mixed> $additionalArgs */
|
|
||||||
public function parseXmlString(string $type, string $xml, array $additionalArgs = []): mixed
|
public function parseXmlString(string $type, string $xml, array $additionalArgs = []): mixed
|
||||||
{
|
{
|
||||||
$crawler = new Crawler($xml);
|
$crawler = new Crawler($xml);
|
||||||
@@ -26,70 +25,56 @@ class ApiResponseParser extends AbstractParser
|
|||||||
|
|
||||||
$resultNode = $crawler->filterXPath('//ergebnis');
|
$resultNode = $crawler->filterXPath('//ergebnis');
|
||||||
|
|
||||||
try {
|
switch ($responseType) {
|
||||||
switch ($responseType) {
|
case ApiClient::TYPE_NOTIFICATION:
|
||||||
case ApiClient::TYPE_NOTIFICATION:
|
return (new NotificationParser())->parse($crawler);
|
||||||
return (new NotificationParser())->parse($crawler);
|
case ApiClient::TYPE_CUSTOMER_DATA:
|
||||||
case ApiClient::TYPE_CUSTOMER_DATA:
|
$subType = $resultNode->filterXPath('//art')->text();
|
||||||
$subType = $this->getStringOrNullValue($resultNode->filterXPath('//art'));
|
|
||||||
if (null === $subType || '' === $subType) {
|
|
||||||
throw new ResponseParserException('Unable to determine customer data subtype from XML response');
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($subType) {
|
switch ($subType) {
|
||||||
case 'Adressdaten':
|
case 'Adressdaten':
|
||||||
case 'Newsletter':
|
case 'Adressdaten_Ändern':
|
||||||
return (new PersonalDataParser())->parse($resultNode);
|
case 'Newsletter':
|
||||||
case 'Adressdaten_Ändern':
|
return (new PersonalDataParser())->parse($resultNode);
|
||||||
return (new PersonalDataUpdateParser())->parse($resultNode);
|
case 'Adresse_Neu':
|
||||||
case 'Adresse_Neu':
|
return (new ContactFormResponseParser())->parse($resultNode);
|
||||||
return (new ContactFormResponseParser())->parse($resultNode);
|
case 'SelektionCRM':
|
||||||
case 'SelektionCRM':
|
case 'SelektionCRM_Ändern':
|
||||||
case 'SelektionCRM_Ändern':
|
return (new CrmAttributesResponseParser())->parse($resultNode);
|
||||||
return (new CrmAttributesResponseParser())->parse($resultNode);
|
case 'Vorgänge':
|
||||||
case 'Vorgänge':
|
return (new BookingsParser())->parse($resultNode);
|
||||||
return (new BookingsParser())->parse($resultNode);
|
case 'Vorgang_Details':
|
||||||
case 'Vorgang_Details':
|
return (new BookingParser())->parse($resultNode);
|
||||||
return (new BookingParser())->parse($resultNode);
|
case 'Dokumentdruck':
|
||||||
case 'Dokumentdruck':
|
return (new DocumentsParser())->parseDocuments($resultNode);
|
||||||
return (new DocumentsParser())->parseDocuments($resultNode);
|
case 'Vorgangdruck':
|
||||||
case 'Vorgangdruck':
|
return (new DocumentsParser())->parseConfirmation($resultNode);
|
||||||
return (new DocumentsParser())->parseConfirmation($resultNode);
|
}
|
||||||
}
|
break;
|
||||||
break;
|
case ApiClient::TYPE_BASE_DATA_COUNTRIES:
|
||||||
case ApiClient::TYPE_BASE_DATA_COUNTRIES:
|
return (new CountriesParser())->parse($resultNode);
|
||||||
return (new CountriesParser())->parse($resultNode);
|
case ApiClient::TYPE_MUTABLE_DATA:
|
||||||
case ApiClient::TYPE_MUTABLE_DATA:
|
return (new MutableDataParser())->parse($resultNode);
|
||||||
return (new MutableDataParser())->parse($resultNode);
|
case ApiClient::TYPE_AVAILABILITY:
|
||||||
case ApiClient::TYPE_AVAILABILITY:
|
return (new AvailabilitiesParser())->parseServices($resultNode);
|
||||||
return (new AvailabilitiesParser())->parseServices($resultNode);
|
case ApiClient::TYPE_AVAILABILITY_HOTEL:
|
||||||
case ApiClient::TYPE_AVAILABILITY_EXTENDED:
|
return (new AvailabilitiesParser())->parseRooms($resultNode);
|
||||||
return (new ExtendedAvailabilitiesParser())->parseServices($resultNode);
|
case ApiClient::TYPE_BOOKING_UPDATE:
|
||||||
case ApiClient::TYPE_AVAILABILITY_HOTEL:
|
return (new BookingUpdateParser())->parse($resultNode);
|
||||||
return (new AvailabilitiesParser())->parseRooms($resultNode);
|
case ApiClient::TYPE_BOOKING:
|
||||||
case ApiClient::TYPE_BOOKING_UPDATE:
|
return (new BookingResponseParser())->parse($resultNode);
|
||||||
return (new BookingUpdateParser())->parse($resultNode);
|
case ApiClient::TYPE_PRODUCTS:
|
||||||
case ApiClient::TYPE_BOOKING:
|
return (new ProductsParser())->parse($resultNode);
|
||||||
return (new BookingResponseParser())->parse($resultNode);
|
case ApiClient::TYPE_PRODUCT_DATA:
|
||||||
case ApiClient::TYPE_PRODUCTS:
|
$travelNode = $crawler->filterXPath('//reise/termin');
|
||||||
return (new ProductsParser())->parse($resultNode);
|
|
||||||
case ApiClient::TYPE_PRODUCT_DATA:
|
|
||||||
$travelNode = $crawler->filterXPath('//reise/termin');
|
|
||||||
|
|
||||||
if (0 === $travelNode->count()) {
|
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
|
||||||
throw new ResponseParserException('No travel data node found in product data response');
|
case ApiClient::TYPE_AGENCIES:
|
||||||
}
|
return (new AgencyParser())->parse($resultNode);
|
||||||
|
case ApiClient::TYPE_PURCHASE_VOUCHER:
|
||||||
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
|
return (new PurchaseVoucherParser())->parse($resultNode);
|
||||||
case ApiClient::TYPE_AGENCIES:
|
case ApiClient::TYPE_PROMO_VOUCHER:
|
||||||
return (new AgencyParser())->parse($resultNode);
|
return (new PromoVoucherParser())->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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ResponseParserException('Unable to parse XML response');
|
throw new ResponseParserException('Unable to parse XML response');
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use Symfony\Component\DomCrawler\Crawler;
|
|||||||
*/
|
*/
|
||||||
class BookingInsurancesParser extends AbstractParser
|
class BookingInsurancesParser extends AbstractParser
|
||||||
{
|
{
|
||||||
/** @return array<string, Insurance> */
|
|
||||||
public function parse(Crawler $result): array
|
public function parse(Crawler $result): array
|
||||||
{
|
{
|
||||||
$insurances = [];
|
$insurances = [];
|
||||||
|
|||||||
Loaded 100 of 438 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user