Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f620cae4f9 | ||
|
|
6c4ea07e6e | ||
|
|
b95944701e | ||
|
|
2f8701c2e0 | ||
|
|
f47142c465 | ||
|
|
3105f2e7a0 | ||
|
|
0ea43d0c6b | ||
|
|
99b6876ce2 | ||
|
|
4e26b8acc0 | ||
|
|
0b2a16d117 | ||
|
|
b41b19d4c6 | ||
|
|
f9142e3080 | ||
|
|
9660149c67 | ||
|
|
2b957e1887 | ||
|
|
57a2503da2 | ||
|
|
f5f84ad98e | ||
|
|
de37186706 | ||
|
|
4d3fc69faa | ||
|
|
022819cbe6 | ||
|
|
1b0d7ce1a9 | ||
|
|
a6ed60462d | ||
|
|
a8d2852313 | ||
|
|
8907233ef5 | ||
|
|
7a3d3a3a96 | ||
|
|
23a3430ea5 | ||
|
|
a15c7578c3 | ||
|
|
08f6633507 | ||
|
|
dbe33ec760 | ||
|
|
145f7de781 | ||
|
|
287ec30cf7 | ||
|
|
c29634b0d2 | ||
|
|
ffa2038acb | ||
|
|
febbf8dd1d | ||
|
|
09610ab3cd | ||
|
|
33328074b0 | ||
|
|
1dc14a193a | ||
|
|
2fc7ffe458 | ||
|
|
52ee6b26b1 | ||
|
|
d77f92b6f0 | ||
|
|
e65cd3a96e | ||
|
|
80569e0977 | ||
|
|
66d0f511df | ||
|
|
93201fbd20 | ||
|
|
b2fccae431 | ||
|
|
8c99fba6fa | ||
|
|
d207bc74cb | ||
|
|
bcf75ce3c3 | ||
|
|
d08d9dbb9f | ||
|
|
33f0cb04e0 | ||
|
|
3fb95cab92 | ||
|
|
b768af15fe | ||
|
|
00889318ee | ||
|
|
f023bd9772 | ||
|
|
4ec1935eef | ||
|
|
ee8c6a6545 | ||
|
|
02b1cbfd6c | ||
|
|
97f88d5eeb | ||
|
|
5561a24050 | ||
|
|
bdf8cffd04 | ||
|
|
fa7e5c2091 | ||
|
|
f1a5d478f3 | ||
|
|
de4dadb9e5 | ||
|
|
b18b09d4a5 | ||
|
|
3f66cb89e6 | ||
|
|
495c130e79 | ||
|
|
7dfbc06bd1 | ||
|
|
132dfc6860 | ||
|
|
e647b6c1f0 | ||
|
|
8b3ef0df95 | ||
|
|
224673a2c4 | ||
|
|
eaff989bdd | ||
|
|
8ac62d2a02 | ||
|
|
1fa8f88a6d | ||
|
|
d211425ff2 | ||
|
|
5b16a515f3 | ||
|
|
259cbd02c5 | ||
|
|
014a057164 | ||
|
|
9ebfdb3d95 | ||
|
|
4f6b82daa8 | ||
|
|
77ad96baf9 | ||
|
|
68900dd4ea | ||
|
|
c0c8ad515c | ||
|
|
287c299c41 | ||
|
|
a2bcedafc4 | ||
|
|
daf89f9189 | ||
|
|
2f96af9254 | ||
|
|
18a97394e8 | ||
|
|
e4ef2f7adc | ||
|
|
df7885ca1c | ||
|
|
d5dfb6c550 | ||
|
|
9b84893d88 | ||
|
|
a1f68ec11d | ||
|
|
b9c6ace1da | ||
|
|
c1c7466dc7 | ||
|
|
02a1640b51 | ||
|
|
083e68ee4d | ||
|
|
4f7cd2c025 | ||
|
|
80969d1da0 | ||
|
|
5a0bf11c81 | ||
|
|
ca6e38cb9c | ||
|
|
ca5a604a3e | ||
|
|
0fcecc9c58 | ||
|
|
4e925171b3 | ||
|
|
41e2fe19bc | ||
|
|
a927d6bf59 | ||
|
|
c0cc183c59 | ||
|
|
b68c4ff9b4 | ||
|
|
132329361f | ||
|
|
4d86629315 | ||
|
|
d1c92f2957 | ||
|
|
0d2cc5b998 | ||
|
|
fdfd5d56c9 | ||
|
|
059c520a50 | ||
|
|
d8818dc435 | ||
|
|
2baa147afa | ||
|
|
e179bbe259 | ||
|
|
16cab4b768 | ||
|
|
6ab34fa87c | ||
|
|
fcfd366dec | ||
|
|
98dd685679 | ||
|
|
2597318488 | ||
|
|
aa997826f8 | ||
|
|
69705052b3 | ||
|
|
f903f17ebd | ||
|
|
95d50e9833 | ||
|
|
63e8a068b3 | ||
|
|
dbc0b4acee | ||
|
|
f31ac660d2 | ||
|
|
8b72f8c277 | ||
|
|
ca052682bb | ||
|
|
1b0e479e49 | ||
|
|
f22ceae41c | ||
|
|
346256f895 | ||
|
|
a0cbd5a23f | ||
|
|
803b0608cc | ||
|
|
7eb1cbdabd | ||
|
|
ca873a99f4 | ||
|
|
8d666baf28 | ||
|
|
be0c9d67bb | ||
|
|
f0594ba09c | ||
|
|
fce6d1615a | ||
|
|
a264526127 | ||
|
|
fbfffbb31a | ||
|
|
bca3610cff | ||
|
|
facd91375c | ||
|
|
96321005e0 | ||
|
|
6f07678f04 | ||
|
|
3b6ee90a7e | ||
|
|
909e8a1d8e | ||
|
|
8f2baca339 | ||
|
|
637166bfd6 | ||
|
|
45af3a21b7 | ||
|
|
7e686cef6b | ||
|
|
be43d30cc7 | ||
|
|
77b16f287a | ||
|
|
bc7fb794bf | ||
|
|
c1ab6d8687 | ||
|
|
ccfafc5da4 | ||
|
|
502281e219 | ||
|
|
b81cc58d5e | ||
|
|
05ac07d656 | ||
|
|
3234c78f0d | ||
|
|
3849ae306c | ||
|
|
301b14f518 | ||
|
|
df8566e1b5 | ||
|
|
45d05786da | ||
|
|
6a6bebb2d1 | ||
|
|
ea76c808de | ||
|
|
f9069540a9 | ||
|
|
8a9868b644 | ||
|
|
d1e2bf3e7d | ||
|
|
66c55f0722 | ||
|
|
ca7d0b6965 | ||
|
|
02ff28acdd | ||
|
|
cb56a4fe46 | ||
|
|
752e6752b4 | ||
|
|
00df76269a | ||
|
|
99483b3506 | ||
|
|
5619a6f22f | ||
|
|
c510cbf590 | ||
|
|
6d9265de0f | ||
|
|
49ea562322 | ||
|
|
1f087b6c08 | ||
|
|
37c839600e | ||
|
|
cef2e4fa49 | ||
|
|
11ff2b9084 | ||
|
|
69f16b59cb | ||
|
|
0e644260b6 | ||
|
|
33609ab534 | ||
|
|
1cc7a025a1 | ||
|
|
709b0ff61e | ||
|
|
0d7d90a442 | ||
|
|
5cbdc5eaf2 | ||
|
|
a7c3064df6 | ||
|
|
f63d52c9d6 | ||
|
|
b752256bda | ||
|
|
474d171779 | ||
|
|
51ce9b6e45 | ||
|
|
9cef24f827 | ||
|
|
e6880b7fed | ||
|
|
e505eba111 | ||
|
|
4e067d13b3 | ||
|
|
40c0042158 | ||
|
|
6abfa122e4 | ||
|
|
032540a712 | ||
|
|
cf95048f2b | ||
|
|
29a1ce7de7 | ||
|
|
1670b8b8d2 | ||
|
|
3644b935fd | ||
|
|
20162cceef | ||
|
|
44a9de7db7 | ||
|
|
f99a2d99a2 | ||
|
|
f795a3ea21 | ||
|
|
f8a56b7de2 | ||
|
|
8a667ccaba | ||
|
|
6ddd0b00bd | ||
|
|
c597cfda51 | ||
|
|
f673bc59d0 | ||
|
|
78b925daa6 | ||
|
|
4e05c94912 | ||
|
|
acb599b760 | ||
|
|
c7b70dbd56 | ||
|
|
b4f7299f27 | ||
|
|
6fd5a218a3 | ||
|
|
a1518c06fb | ||
|
|
c517a0b41f | ||
|
|
ded904fac8 | ||
|
|
abea6bff7b | ||
|
|
06f53ba6eb | ||
|
|
eda0919f52 | ||
|
|
1f27c744d2 | ||
|
|
bbc011ddb9 | ||
|
|
c2054a1ef6 | ||
|
|
f9d420fecd | ||
|
|
3e1d719512 | ||
|
|
f685bd18ab | ||
|
|
f46cc59041 | ||
|
|
380b1ed329 | ||
|
|
1056b4fba6 | ||
|
|
19031dba99 | ||
|
|
028db7a7ee | ||
|
|
a1764faaeb | ||
|
|
8529bb0e21 | ||
|
|
9eefdd1858 | ||
|
|
e5803860df | ||
|
|
2754d734aa | ||
|
|
3487ca4822 | ||
|
|
02ebddc34a | ||
|
|
3957f6bd09 | ||
|
|
11ab98b3d8 | ||
|
|
e24403949c | ||
|
|
288f6b91d0 | ||
|
|
8ca105966d | ||
|
|
592cd517ff | ||
|
|
586b410a66 | ||
|
|
a444940db2 | ||
|
|
afe5aef9f7 | ||
|
|
21a3e8fafc | ||
|
|
26bbba5a2e | ||
|
|
b68d5c9398 | ||
|
|
c1425f0562 | ||
|
|
e1f1b60608 | ||
|
|
ea1c95528c | ||
|
|
0cac2969ae | ||
|
|
b856c51389 | ||
|
|
aa3b23ecb8 | ||
|
|
1e653e213a | ||
|
|
7d91dcfb70 | ||
|
|
8e7e6266f0 | ||
|
|
167ccf5151 | ||
|
|
c582e0820a | ||
|
|
7d3a63d744 | ||
|
|
0a0987873f | ||
|
|
18d60c8781 | ||
|
|
7708ea6a0f | ||
|
|
71a46c6b63 | ||
|
|
667324df69 | ||
|
|
957e20d763 | ||
|
|
029a87c6ca | ||
|
|
baa4609102 | ||
|
|
875943e4da | ||
|
|
10e674cdd0 | ||
|
|
0bf78b0b81 | ||
|
|
c90b93556a | ||
|
|
f45718ea06 | ||
|
|
6d418ee8cc | ||
|
|
4851b1b72d | ||
|
|
dfb6f88be9 | ||
|
|
f28227384d | ||
|
|
cef9be92f3 | ||
|
|
f849d67a79 | ||
|
|
fba3becb06 | ||
|
|
a23ff6d119 | ||
|
|
384744c958 | ||
|
|
70c6617e88 | ||
|
|
9afe3b334e | ||
|
|
ad67d4c7ae | ||
|
|
b7eaed3ac3 | ||
|
|
136e2d68a2 | ||
|
|
bdcd54687d | ||
|
|
99190b0ef1 | ||
|
|
c7702b0c37 | ||
|
|
9b88a2c14d | ||
|
|
6a945e6a08 | ||
|
|
9c7cf0778f | ||
|
|
dd0f1a25d0 | ||
|
|
7f4cf3060e | ||
|
|
30496a4875 | ||
|
|
027508f495 | ||
|
|
b5e2b2a2ad | ||
|
|
9c9db05b86 | ||
|
|
1fa17a7df7 | ||
|
|
e09e5858d8 | ||
|
|
6ede062a66 | ||
|
|
b351b4042f | ||
|
|
95d0ce33d4 | ||
|
|
75366167e8 | ||
|
|
7fa7e4be45 | ||
|
|
d14dafbade | ||
|
|
e222f448bb | ||
|
|
f55ed3dc4c | ||
|
|
fedfcc247e | ||
|
|
8186adfaeb | ||
|
|
00304facba | ||
|
|
ff241f437f | ||
|
|
4a6b9cd64d | ||
|
|
8646dfbf02 | ||
|
|
d6f2681c61 | ||
|
|
5a47ca6053 | ||
|
|
d7fd273475 | ||
|
|
ce828a6ef5 | ||
|
|
15dbd72f93 | ||
|
|
e3bdd8737f | ||
|
|
7942656026 | ||
|
|
1fe8a2cb17 | ||
|
|
ab48b1f867 | ||
|
|
3043842254 | ||
|
|
249e326de4 | ||
|
|
b3cace3b43 | ||
|
|
ad578179d8 | ||
|
|
c57351f8b5 | ||
|
|
c5934e4c5b | ||
|
|
06e64bd3b8 | ||
|
|
48572812a9 | ||
|
|
46b610370a | ||
|
|
96d4b8ed45 | ||
|
|
c18ba3d815 | ||
|
|
fd81079247 | ||
|
|
e5781fbabc | ||
|
|
8887b06583 | ||
|
|
9955ff7df9 | ||
|
|
db74b6049c | ||
|
|
0b5c7af4be | ||
|
|
721016a00e | ||
|
|
563522d261 | ||
|
|
04f4455491 | ||
|
|
b976ce480a | ||
|
|
6d5f7cfd22 | ||
|
|
9baed0987e | ||
|
|
b76d17d35c | ||
|
|
4e182612e8 | ||
|
|
645729a518 | ||
|
|
9ee4c1cd5c | ||
|
|
02742aef5b | ||
|
|
87af2c3855 | ||
|
|
ea1ce4a029 | ||
|
|
2fe9185a6f | ||
|
|
ef191c55be | ||
|
|
abd74a127a | ||
|
|
52ac269b8d | ||
|
|
643f65ce3b | ||
|
|
465ce3e69a | ||
|
|
207b648f36 | ||
|
|
7f2d52a5ce | ||
|
|
f1eaa95c44 | ||
|
|
e203de172d | ||
|
|
79b7e2aaa5 | ||
|
|
3c2ade9299 | ||
|
|
f3f2dc6cec | ||
|
|
12b69bd681 | ||
|
|
8f542ff448 | ||
|
|
80c81c11fb | ||
|
|
e3809bd930 | ||
|
|
d755e29459 | ||
|
|
105eb5a9cd | ||
|
|
65f8436aa7 | ||
|
|
4afdd79252 | ||
|
|
d1e807b11f | ||
|
|
ee84690cd9 | ||
|
|
c7a27f1dc7 | ||
|
|
1fd9f8d1f8 | ||
|
|
b57d849ecd | ||
|
|
654ae4dc0b | ||
|
|
3984a623bf | ||
|
|
96619c6cec | ||
|
|
3dd92ccdb4 | ||
|
|
b111a615a9 | ||
|
|
2372958713 | ||
|
|
82a8541af1 | ||
|
|
ea162972fa | ||
|
|
094d6858d1 | ||
|
|
65398d59d4 | ||
|
|
3df9659ce2 | ||
|
|
c4f4f53fd6 | ||
|
|
b567b6a840 | ||
|
|
ab18e5f134 | ||
|
|
929115c02b | ||
|
|
e26884c099 | ||
|
|
077bcdcaf5 | ||
|
|
685ac5a94a | ||
|
|
3d1b75b591 | ||
|
|
e1bfe9b93c | ||
|
|
5b75a165ce | ||
|
|
e12fbc85d4 | ||
|
|
6bbef0a975 | ||
|
|
bc175ca438 | ||
|
|
ef15772b97 | ||
|
|
da5b913757 | ||
|
|
6b5a51f652 | ||
|
|
af2620c8b3 | ||
|
|
15fdfdc42e | ||
|
|
ce88e1f97b | ||
|
|
a67e3a5eac | ||
|
|
cc747f0690 | ||
|
|
a6d2661562 | ||
|
|
4b95b12a7b | ||
|
|
cca8554e3d | ||
|
|
28c426ca3c | ||
|
|
be83a4295a | ||
|
|
87439ee509 | ||
|
|
36ef54d541 | ||
|
|
400407358d | ||
|
|
502b121e43 | ||
|
|
61447ced4f | ||
|
|
1e107c68b4 | ||
|
|
602fb31f27 | ||
|
|
2a8531a025 | ||
|
|
980589e190 | ||
|
|
4a2d1f4a02 | ||
|
|
39e6cc4d68 | ||
|
|
e5018432cb | ||
|
|
dbf0b03bac | ||
|
|
01060035bc | ||
|
|
fa0cf5a654 | ||
|
|
4f1a1cad8e | ||
|
|
8c8aae9a1e | ||
|
|
230c53875f | ||
|
|
2e40cbc7c7 | ||
|
|
cd1ffc3e9a | ||
|
|
4db2cd7eda | ||
|
|
f230b36382 | ||
|
|
b872f2a5db | ||
|
|
c6678d16a6 | ||
|
|
b73a02048e | ||
|
|
1f2f2751e4 | ||
|
|
85998534fe | ||
|
|
8e7b8bd75f | ||
|
|
fb983f23dd | ||
|
|
7df99521c4 | ||
|
|
a8c0a1aa61 | ||
|
|
fc05350ae4 | ||
|
|
ba7de8fa01 | ||
|
|
778cf80e6b | ||
|
|
f01bb7d271 | ||
|
|
6a031e5a71 | ||
|
|
37924841e1 | ||
|
|
be90248fc7 | ||
|
|
cb38beb32c | ||
|
|
cc8500d0f4 | ||
|
|
61d4e36f1f | ||
|
|
5c5f164aa2 | ||
|
|
5275e6c732 | ||
|
|
f3ea6734a1 | ||
|
|
bb20176c76 | ||
|
|
beee44a10e | ||
|
|
43823dbda1 | ||
|
|
ff39bf365c | ||
|
|
5d69e4a8aa | ||
|
|
142db19627 | ||
|
|
7b84ecbb56 | ||
|
|
dc6de1734a | ||
|
|
1e464cef0a | ||
|
|
4decaa6789 | ||
|
|
a057088808 | ||
|
|
e46053a7b7 | ||
|
|
91d9e82d84 | ||
|
|
608f5ed3e9 | ||
|
|
9b2e209b4f | ||
|
|
bae1582631 | ||
|
|
c3fe76d78c | ||
|
|
8abab0e379 | ||
|
|
d31204188b | ||
|
|
a74f34b559 | ||
|
|
37cda348b0 | ||
|
|
38656e73c5 | ||
|
|
03ac430be4 | ||
|
|
89979d6ce4 | ||
|
|
54a3145710 | ||
|
|
7aea149b6c | ||
|
|
f50aa55e8b | ||
|
|
6c0d7c30c6 | ||
|
|
af163d597e | ||
|
|
d0c8302d21 | ||
|
|
44e2d76525 | ||
|
|
8f274bbb44 | ||
|
|
8436124cbc | ||
|
|
647fa8e212 | ||
|
|
6cc5886073 | ||
|
|
513c889926 | ||
|
|
9c6f4d59b3 | ||
|
|
a0d5768752 | ||
|
|
4f951cdf8a | ||
|
|
619e2ba1ee | ||
|
|
7d6a80eefa | ||
|
|
2403f8fb29 | ||
|
|
b485112ed3 | ||
|
|
1f5c835f8a | ||
|
|
a3d7ee63d3 | ||
|
|
a8cf67728b | ||
|
|
c830fc7841 | ||
|
|
5ef3bb9cbf | ||
|
|
b882175f36 | ||
|
|
d20215929d | ||
|
|
ea312b9986 | ||
|
|
0559341f31 | ||
|
|
1f4dc2d4a2 | ||
|
|
14ca0f5c59 | ||
|
|
e51c4843c5 | ||
|
|
32a9fac9ed | ||
|
|
de8cbf6178 | ||
|
|
33e1aea80c | ||
|
|
83589ee677 | ||
|
|
023ecdebc9 | ||
|
|
2d9cb2c0b9 | ||
|
|
5c0814ef9b | ||
|
|
28831a3b06 | ||
|
|
0d073a36a3 | ||
|
|
069600d602 | ||
|
|
d70a27f2fd | ||
|
|
e330626b03 | ||
|
|
0f4b16c194 | ||
|
|
c89bda86b7 | ||
|
|
56fa5355fd | ||
|
|
7ea52670b9 | ||
|
|
ee264e88d4 | ||
|
|
3f7db4e772 | ||
|
|
3d9fc18a83 | ||
|
|
9eeed7f992 | ||
|
|
ba0a86cbea | ||
|
|
41d8d65d82 | ||
|
|
57843b8a6f | ||
|
|
d6258d0ca1 | ||
|
|
66ccebb02b | ||
|
|
e3081e56f8 | ||
|
|
201b223813 | ||
|
|
08d534e39b | ||
|
|
4489169553 | ||
|
|
16c239716b | ||
|
|
f5913eb947 | ||
|
|
f2681af9ee | ||
|
|
66dd51d75a | ||
|
|
5f78361533 | ||
|
|
3c1192298b | ||
|
|
0bd9d79bbe | ||
|
|
a2e055f313 | ||
|
|
ca1d5c4303 | ||
|
|
e6eebc00a3 | ||
|
|
29bdf794f3 | ||
|
|
4ba81b8f03 | ||
|
|
8af7f0e797 | ||
|
|
c67fa18ad3 | ||
|
|
6c9da6537e | ||
|
|
241785b428 | ||
|
|
411166ffba | ||
|
|
203c9ebbca | ||
|
|
ede37d5d0f | ||
|
|
4f02529b15 | ||
|
|
0f6b73340e | ||
|
|
96c53cc173 | ||
|
|
7ce3bc18b2 | ||
|
|
82b47e314b | ||
|
|
9d2474c1e3 | ||
|
|
787f69fc46 | ||
|
|
054d4e52b2 | ||
|
|
0d1ef6d40e | ||
|
|
ead448b8d4 | ||
|
|
7ff10e6792 | ||
|
|
3d6c94e7c5 | ||
|
|
7673e64738 | ||
|
|
4230c53812 | ||
|
|
3198e03cc4 | ||
|
|
1e48e107fc | ||
|
|
ed7c889c4f | ||
|
|
84f4481ef0 | ||
|
|
b84997d058 | ||
|
|
3063a7c05f | ||
|
|
36fe4c9e9a | ||
|
|
5c5134b69f | ||
|
|
e7fb764158 | ||
|
|
fe624cfd1c | ||
|
|
81b8237570 | ||
|
|
a019c7dae0 | ||
|
|
552dd029fa | ||
|
|
bd5c6359fe | ||
|
|
6bd483d768 | ||
|
|
7003c0d81b | ||
|
|
b038d9a41f | ||
|
|
89c42cf7e4 | ||
|
|
cd1e5ad3ad | ||
|
|
f36c761c28 | ||
|
|
0f7d95d31b | ||
|
|
4b92f56ed7 | ||
|
|
b54fb07e84 | ||
|
|
11825191cf | ||
|
|
d719b17aec | ||
|
|
65a33922cf | ||
|
|
727ee9b014 | ||
|
|
4d51f0334f | ||
|
|
86e4866d6a | ||
|
|
e75276ed88 | ||
|
|
bfdf4c0453 | ||
|
|
f90902c5e3 | ||
|
|
d0d6844fbd | ||
|
|
cd7a724e62 | ||
|
|
1f5877460b | ||
|
|
8da77ee13b | ||
|
|
d205cd09ef | ||
|
|
b44d8505b2 | ||
|
|
9d99fe9272 | ||
|
|
431839d36d | ||
|
|
cce1f688b9 |
No files matched your search
@@ -1,11 +0,0 @@
|
|||||||
name: opencode
|
|
||||||
repository: /Users/fromme/Development/Misc/ddev-opencode
|
|
||||||
version: ""
|
|
||||||
install_date: "2026-01-05T17:21:57+01:00"
|
|
||||||
project_files:
|
|
||||||
- web-build/Dockerfile.opencode
|
|
||||||
- commands/web/opencode
|
|
||||||
- config.opencode.yaml
|
|
||||||
- opencode/.gitignore
|
|
||||||
global_files: []
|
|
||||||
removal_actions: []
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#ddev-generated
|
|
||||||
|
|
||||||
## Description: OpenCode AI coding assistant
|
|
||||||
## Usage: opencode
|
|
||||||
## Example: ddev opencode [...]
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
opencode $@
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#ddev-generated
|
|
||||||
webimage_extra_packages:
|
|
||||||
- xdg-utils
|
|
||||||
hooks:
|
|
||||||
post-start:
|
|
||||||
- exec: "mkdir -p /var/www/html/.ddev/opencode/.opencode && ln -sf /var/www/html/.ddev/opencode/.opencode ~/.opencode"
|
|
||||||
@@ -1,44 +1,51 @@
|
|||||||
type: php
|
type: php
|
||||||
docroot: public
|
docroot: public
|
||||||
php_version: "8.2"
|
php_version: "8.3"
|
||||||
webserver_type: apache-fpm
|
webserver_type: apache-fpm
|
||||||
xdebug_enabled: false
|
xdebug_enabled: false
|
||||||
additional_hostnames: []
|
additional_hostnames:
|
||||||
|
- mysnz
|
||||||
|
- mysbw
|
||||||
|
- myser
|
||||||
additional_fqdns: []
|
additional_fqdns: []
|
||||||
database:
|
database:
|
||||||
type: mariadb
|
type: mysql
|
||||||
version: "10.11"
|
version: "8.4"
|
||||||
use_dns_when_possible: true
|
use_dns_when_possible: true
|
||||||
composer_version: "2"
|
composer_version: "2"
|
||||||
web_environment: []
|
web_environment: []
|
||||||
corepack_enable: false
|
corepack_enable: false
|
||||||
|
nodejs_version: "22"
|
||||||
|
|
||||||
# Key features of DDEV's config.yaml:
|
# Key features of DDEV's config.yaml:
|
||||||
|
|
||||||
# name: <projectname> # Name of the project, automatically provides
|
# name: <projectname> # Name of the project, automatically provides
|
||||||
# http://projectname.ddev.site and https://projectname.ddev.site
|
# http://projectname.ddev.site and https://projectname.ddev.site
|
||||||
|
# If the name is omitted, the project will take the name of the enclosing directory,
|
||||||
|
# which is useful if you want to have a copy of the project side by side with this one.
|
||||||
|
|
||||||
# type: <projecttype> # backdrop, craftcms, django4, drupal, drupal6, drupal7, laravel, magento, magento2, php, python, shopware6, silverstripe, typo3, wordpress
|
# type: <projecttype> # backdrop, cakephp, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, generic, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress
|
||||||
# See https://ddev.readthedocs.io/en/stable/users/quickstart/ for more
|
# See https://docs.ddev.com/en/stable/users/quickstart/ for more
|
||||||
# information on the different project types
|
# information on the different project types
|
||||||
# "drupal" covers recent Drupal 8+
|
|
||||||
|
|
||||||
# docroot: <relative_path> # Relative path to the directory containing index.php.
|
# docroot: <relative_path> # Relative path to the directory containing index.php.
|
||||||
|
|
||||||
# php_version: "8.2" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"
|
# php_version: "8.3" # PHP version to use, "5.6" through "8.5"
|
||||||
|
|
||||||
# You can explicitly specify the webimage but this
|
# You can explicitly specify the webimage but this
|
||||||
# is not recommended, as the images are often closely tied to DDEV's' behavior,
|
# is not recommended, as the images are often closely tied to DDEV's' behavior,
|
||||||
# so this can break upgrades.
|
# so this can break upgrades.
|
||||||
|
|
||||||
# webimage: <docker_image> # nginx/php docker image.
|
# webimage: <docker_image>
|
||||||
|
# It’s unusual to change this option, and we don’t recommend it without Docker experience and a good reason.
|
||||||
|
# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.*
|
||||||
|
|
||||||
# database:
|
# database:
|
||||||
# type: <dbtype> # mysql, mariadb, postgres
|
# type: <dbtype> # mysql, mariadb, postgres
|
||||||
# version: <version> # database version, like "10.11" or "8.0"
|
# version: <version> # database version, like "10.11" or "8.0"
|
||||||
# MariaDB versions can be 5.5-10.8, 10.11, and 11.4.
|
# MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8
|
||||||
# MySQL versions can be 5.5-8.0.
|
# MySQL versions can be 5.5-8.0, 8.4
|
||||||
# PostgreSQL versions can be 9-17.
|
# PostgreSQL versions can be 9-18
|
||||||
|
|
||||||
# router_http_port: <port> # Port to be used for http (defaults to global configuration, usually 80)
|
# router_http_port: <port> # Port to be used for http (defaults to global configuration, usually 80)
|
||||||
# router_https_port: <port> # Port for https (defaults to global configuration, usually 443)
|
# router_https_port: <port> # Port for https (defaults to global configuration, usually 443)
|
||||||
@@ -48,12 +55,21 @@ corepack_enable: false
|
|||||||
# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better,
|
# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better,
|
||||||
# as leaving Xdebug enabled all the time is a big performance hit.
|
# as leaving Xdebug enabled all the time is a big performance hit.
|
||||||
|
|
||||||
# xhprof_enabled: false # Set to true to enable Xhprof and "ddev start" or "ddev restart"
|
# xhgui_http_port: "8143"
|
||||||
# Note that for most people the commands
|
# xhgui_https_port: "8142"
|
||||||
# "ddev xhprof" to enable Xhprof and "ddev xhprof off" to disable it work better,
|
# The XHGui ports can be changed from the default 8143 and 8142
|
||||||
# as leaving Xhprof enabled all the time is a big performance hit.
|
# Very rarely used
|
||||||
|
|
||||||
# webserver_type: nginx-fpm, apache-fpm, or nginx-gunicorn
|
# host_xhgui_port: "8142"
|
||||||
|
# Can be used to change the host binding port of the XHGui
|
||||||
|
# application. Rarely used; only when port conflict and
|
||||||
|
# bind_all_ports is used (normally with router disabled)
|
||||||
|
|
||||||
|
# xhprof_mode: [prepend|xhgui|global]
|
||||||
|
# Set to "xhgui" to enable XHGui features
|
||||||
|
# "xhgui" will become default in a future major release
|
||||||
|
|
||||||
|
# webserver_type: nginx-fpm, apache-fpm, generic
|
||||||
|
|
||||||
# timezone: Europe/Berlin
|
# timezone: Europe/Berlin
|
||||||
# If timezone is unset, DDEV will attempt to derive it from the host system timezone
|
# If timezone is unset, DDEV will attempt to derive it from the host system timezone
|
||||||
@@ -79,9 +95,9 @@ corepack_enable: false
|
|||||||
# Alternatively, an explicit Composer version may be specified, for example "2.2.18".
|
# Alternatively, an explicit Composer version may be specified, for example "2.2.18".
|
||||||
# To reinstall Composer after the image was built, run "ddev debug rebuild".
|
# To reinstall Composer after the image was built, run "ddev debug rebuild".
|
||||||
|
|
||||||
# nodejs_version: "20"
|
# nodejs_version: "22"
|
||||||
# change from the default system Node.js version to any other version.
|
# change from the default system Node.js version to any other version.
|
||||||
# See https://ddev.readthedocs.io/en/stable/users/configuration/config/#nodejs_version for more information
|
# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information
|
||||||
# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation,
|
# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation,
|
||||||
# Note that using of 'ddev nvm' is discouraged because "nodejs_version" is much easier to use,
|
# Note that using of 'ddev nvm' is discouraged because "nodejs_version" is much easier to use,
|
||||||
# can specify any version, and is more robust than using 'nvm'.
|
# can specify any version, and is more robust than using 'nvm'.
|
||||||
@@ -117,7 +133,7 @@ corepack_enable: false
|
|||||||
|
|
||||||
# ddev_version_constraint: ""
|
# ddev_version_constraint: ""
|
||||||
# Example:
|
# Example:
|
||||||
# ddev_version_constraint: ">= 1.22.4"
|
# ddev_version_constraint: ">= 1.24.8"
|
||||||
# This will enforce that the running ddev version is within this constraint.
|
# This will enforce that the running ddev version is within this constraint.
|
||||||
# See https://github.com/Masterminds/semver#checking-version-constraints for
|
# See https://github.com/Masterminds/semver#checking-version-constraints for
|
||||||
# supported constraint formats
|
# supported constraint formats
|
||||||
@@ -146,8 +162,8 @@ corepack_enable: false
|
|||||||
# - "mutagen": enables Mutagen for this project.
|
# - "mutagen": enables Mutagen for this project.
|
||||||
# - "nfs": enables NFS for this project.
|
# - "nfs": enables NFS for this project.
|
||||||
#
|
#
|
||||||
# See https://ddev.readthedocs.io/en/stable/users/install/performance/#nfs
|
# See https://docs.ddev.com/en/stable/users/install/performance/#nfs
|
||||||
# See https://ddev.readthedocs.io/en/stable/users/install/performance/#mutagen
|
# See https://docs.ddev.com/en/stable/users/install/performance/#mutagen
|
||||||
|
|
||||||
# fail_on_hook_fail: False
|
# fail_on_hook_fail: False
|
||||||
# Decide whether 'ddev start' should be interrupted by a failing hook
|
# Decide whether 'ddev start' should be interrupted by a failing hook
|
||||||
@@ -196,7 +212,7 @@ corepack_enable: false
|
|||||||
|
|
||||||
# ngrok_args: --basic-auth username:pass1234
|
# ngrok_args: --basic-auth username:pass1234
|
||||||
# Provide extra flags to the "ngrok http" command, see
|
# Provide extra flags to the "ngrok http" command, see
|
||||||
# https://ngrok.com/docs/ngrok-agent/config or run "ngrok http -h"
|
# https://ngrok.com/docs/agent/config/v3/#agent-configuration or run "ngrok http -h"
|
||||||
|
|
||||||
# disable_settings_management: false
|
# disable_settings_management: false
|
||||||
# If true, DDEV will not create CMS-specific settings files like
|
# If true, DDEV will not create CMS-specific settings files like
|
||||||
@@ -274,7 +290,7 @@ corepack_enable: false
|
|||||||
# Many DDEV commands can be extended to run tasks before or after the
|
# Many DDEV commands can be extended to run tasks before or after the
|
||||||
# DDEV command is executed, for example "post-start", "post-import-db",
|
# DDEV command is executed, for example "post-start", "post-import-db",
|
||||||
# "pre-composer", "post-composer"
|
# "pre-composer", "post-composer"
|
||||||
# See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more
|
# See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more
|
||||||
# information on the commands that can be extended and the tasks you can define
|
# information on the commands that can be extended and the tasks you can define
|
||||||
# for them. Example:
|
# for them. Example:
|
||||||
#hooks:
|
#hooks:
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#ddev-generated
|
|
||||||
|
|
||||||
# OpenCode internal config
|
|
||||||
/.opencode/
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#ddev-generated
|
|
||||||
|
|
||||||
RUN npm install -g opencode-ai
|
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ APP_SECRET=642ead390b8a38f0a017ec84d5362129
|
|||||||
#
|
#
|
||||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
||||||
# DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
# DATABASE_URL="mysql://app:[email protected]:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=10.11.11-MariaDB&charset=utf8mb4"
|
DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=8.4.0&charset=utf8mb4"
|
||||||
# DATABASE_URL="postgresql://app:[email protected]:5432/app?serverVersion=16&charset=utf8"
|
# DATABASE_URL="postgresql://app:[email protected]:5432/app?serverVersion=16&charset=utf8"
|
||||||
###< doctrine/doctrine-bundle ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
@@ -40,26 +40,53 @@ 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=
|
||||||
APP_BPN_PORT=
|
APP_BPN_PORTS=
|
||||||
APP_BPN_DEBUG=false
|
APP_BPN_DEBUG=false
|
||||||
APP_BPN_CONNECTION_TIMEOUT=5
|
APP_BPN_CONNECTION_TIMEOUT=5
|
||||||
APP_BPN_STREAM_TIMEOUT=30
|
APP_BPN_STREAM_TIMEOUT=30
|
||||||
APP_BPN_TOTAL_TIMEOUT=45
|
APP_BPN_TOTAL_TIMEOUT=45
|
||||||
|
|
||||||
APP_TRAVEL_INFO_BASE_URL="https://www.ep-reisen.de/reiseinformationen/"
|
APP_TRAVEL_INFO_BASE_URL="https://www.ep-reisen.de/reiseinformationen/"
|
||||||
|
APP_LEGAL_NOTICE_URL="https://www.ep-reisen.de/unternehmen/impressum-skireiseveranstalter/"
|
||||||
|
APP_TERMS_AND_CONDITIONS_URL="https://www.ep-reisen.de/skireisen/infos/allgemeine-infos/agb/"
|
||||||
|
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]"
|
||||||
|
|
||||||
# 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
|
||||||
|
# Status for new bookings: 'F' (Fixed/Final), 'O' (Option - requires agency confirmation)
|
||||||
|
# Use 'O' during beta phase, switch to 'F' for production
|
||||||
|
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=
|
||||||
@@ -67,6 +94,11 @@ 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,3 +4,4 @@ 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
|
||||||
@@ -31,3 +31,9 @@ yarn-error.log
|
|||||||
/.php-cs-fixer.php
|
/.php-cs-fixer.php
|
||||||
/.php-cs-fixer.cache
|
/.php-cs-fixer.cache
|
||||||
###< friendsofphp/php-cs-fixer ###
|
###< friendsofphp/php-cs-fixer ###
|
||||||
|
|
||||||
|
/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/SBW131225
|
GET {{base_url}}/api/travels/DPWMP140326
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ Authorization: Bearer {{$auth.token("oauth2_api")}}
|
|||||||
|
|
||||||
### API hotel availability remote
|
### API hotel availability remote
|
||||||
# @no-cookie-jar
|
# @no-cookie-jar
|
||||||
GET {{base_url}}/api/travels/11603/152546/2026-01-03/availability
|
GET {{base_url}}/api/travels/11606/152546/2026-01-24/availability
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|
||||||
@@ -90,14 +90,35 @@ Content-Type: application/json
|
|||||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"lastName": "Duschen",
|
"lastName": "{{$random.name.lastName}}",
|
||||||
"firstName": "Isolde",
|
"firstName": "{{$random.name.firstName}}",
|
||||||
"gender": "f",
|
"gender": "d",
|
||||||
"street": "Fooroad 1",
|
"street": "{{$random.address.streetAddress}}",
|
||||||
"zipCode": "12345",
|
"zipCode": "{{$random.address.zipCode}}",
|
||||||
"city": "Bartown",
|
"city": "{{$random.address.city}}",
|
||||||
"email": "[email protected]",
|
"email": "{{$random.email}}",
|
||||||
"phone": "12345"
|
"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
|
||||||
|
|||||||
@@ -13,3 +13,12 @@ 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
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitors birthday input fields (day, month, year) and dispatches a custom event
|
||||||
|
* only when all three fields are filled with valid values.
|
||||||
|
*
|
||||||
|
* This prevents premature HTMX refreshes when users are still entering the date,
|
||||||
|
* improving UX by waiting until the complete date is entered.
|
||||||
|
*/
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ['day', 'month', 'year']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if all birthday fields are filled and dispatches a completion event.
|
||||||
|
*
|
||||||
|
* Called on input/change events from the day, month, and year fields.
|
||||||
|
* Only dispatches the 'birthday:complete' event when all three fields
|
||||||
|
* contain valid values that could form a complete date.
|
||||||
|
*/
|
||||||
|
check() {
|
||||||
|
const day = this.dayTarget.value.trim()
|
||||||
|
const month = this.monthTarget.value.trim()
|
||||||
|
const year = this.yearTarget.value.trim()
|
||||||
|
|
||||||
|
if (false === this.isComplete(day, month, year)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.element.dispatchEvent(new CustomEvent('birthday:complete', { bubbles: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that all date components are filled with plausible values.
|
||||||
|
*
|
||||||
|
* @param {string} day The day value
|
||||||
|
* @param {string} month The month value
|
||||||
|
* @param {string} year The year value
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if all fields contain complete values
|
||||||
|
*/
|
||||||
|
isComplete(day, month, year) {
|
||||||
|
// Day: 1-2 digits
|
||||||
|
if (0 === day.length || day.length > 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Month: 1-2 digits
|
||||||
|
if (0 === month.length || month.length > 2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Year: exactly 4 digits for a complete year
|
||||||
|
if (4 !== year.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus';
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = {
|
||||||
|
payload: Object
|
||||||
|
};
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (typeof window.dataLayer !== 'undefined') {
|
||||||
|
window.dataLayer.push(this.payloadValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,24 +24,15 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleNotifications(event) {
|
handleNotifications(event) {
|
||||||
console.log('[VOUCHER DEBUG] showNotifications event received:', event)
|
|
||||||
console.log('[VOUCHER DEBUG] event.detail:', event.detail)
|
|
||||||
|
|
||||||
// HTMX wraps the trigger value in an object with 'value' and 'elt' properties
|
// HTMX wraps the trigger value in an object with 'value' and 'elt' properties
|
||||||
// event.detail.value contains the actual array from the HX-Trigger header
|
// event.detail.value contains the actual array from the HX-Trigger header
|
||||||
const notifications = event.detail?.value || event.detail || []
|
const notifications = event.detail?.value || event.detail || []
|
||||||
|
|
||||||
console.log('[VOUCHER DEBUG] notifications array:', notifications)
|
|
||||||
console.log('[VOUCHER DEBUG] notifications count:', Array.isArray(notifications) ? notifications.length : 'not an array')
|
|
||||||
|
|
||||||
if (Array.isArray(notifications)) {
|
if (Array.isArray(notifications)) {
|
||||||
notifications.forEach((notification, index) => {
|
notifications.forEach((notification, index) => {
|
||||||
console.log(`[VOUCHER DEBUG] Processing notification ${index}:`, notification)
|
|
||||||
const className = this.getClassForType(notification.type)
|
const className = this.getClassForType(notification.type)
|
||||||
this.showToast(notification.message, className)
|
this.showToast(notification.message, className)
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
console.error('[VOUCHER DEBUG] Notifications is not an array:', notifications)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,4 +57,4 @@ export default class extends Controller {
|
|||||||
|
|
||||||
return typeMap[type] || 'toastify--info'
|
return typeMap[type] || 'toastify--info'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg viewBox="0 0 130 130" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(3, 28)">
|
||||||
|
<path d="M107,0.3c-36.4,5.5 -81.6,19.1 -81.6,38.2c0,17.7 41.8,13.2 41.8,13.2c0,0 -67,22.3 -67,-7c0,-33.2 84.5,-44.4 106.8,-44.4Z" fill="#0084d4"/>
|
||||||
|
<path d="M17,73.5c36.4,-5.4 81.6,-19.1 81.6,-38.2c0,-17.7 -41.8,-13.2 -41.8,-13.2c0,0 67,-22.3 67,7c0,33.3 -84.5,44.4 -106.8,44.4Z" fill="#f8c62f"/>
|
||||||
|
<path d="M60.7,34.3l-12.8,4l12.2,0.8l-0.4,-2.4l1,-2.4Z" fill="#0084d4"/>
|
||||||
|
<path d="M75.9,35.2l-9.2,2.9l-3.5,9.7l-1.7,-11.3l4,-10.7l1.7,7.9l8.7,1.5Z" fill="#f8c62f"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,244 @@
|
|||||||
|
<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-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>
|
||||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
@@ -5,6 +5,8 @@ const DEBOUNCE_MS = 200
|
|||||||
|
|
||||||
let debounceTimeout = null
|
let debounceTimeout = null
|
||||||
let historyRestoreGracePeriod = false
|
let historyRestoreGracePeriod = false
|
||||||
|
let savedScrollPosition = null
|
||||||
|
let activeRequests = 0
|
||||||
|
|
||||||
function getIndicator() {
|
function getIndicator() {
|
||||||
return document.getElementById(INDICATOR_ID)
|
return document.getElementById(INDICATOR_ID)
|
||||||
@@ -28,12 +30,20 @@ function hide() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBeforeRequest() {
|
function handleBeforeRequest(event) {
|
||||||
// Ignore requests triggered during history restore grace period
|
// Ignore requests triggered during history restore grace period
|
||||||
if (true === historyRestoreGracePeriod) {
|
if (true === historyRestoreGracePeriod) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeRequests++
|
||||||
|
|
||||||
|
// Save scroll position of the target container before HTMX request
|
||||||
|
const target = event.detail?.target || document.getElementById('main-content')
|
||||||
|
if (target && typeof target.scrollTop === 'number') {
|
||||||
|
savedScrollPosition = { element: target, top: target.scrollTop }
|
||||||
|
}
|
||||||
|
|
||||||
// Don't start a new debounce if already pending
|
// Don't start a new debounce if already pending
|
||||||
if (debounceTimeout) {
|
if (debounceTimeout) {
|
||||||
return
|
return
|
||||||
@@ -45,7 +55,29 @@ function handleBeforeRequest() {
|
|||||||
}, DEBOUNCE_MS)
|
}, DEBOUNCE_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRequestEnd() {
|
||||||
|
// Counter may already be 0 if a force-reset (history save, page show) fired
|
||||||
|
// before this afterRequest — ignore the stale completion in that case
|
||||||
|
if (0 === activeRequests) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeRequests--
|
||||||
|
if (0 === activeRequests) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAfterSwap() {
|
||||||
|
// Restore scroll position after HTMX swaps content
|
||||||
|
if (savedScrollPosition !== null && savedScrollPosition.element) {
|
||||||
|
savedScrollPosition.element.scrollTop = savedScrollPosition.top
|
||||||
|
savedScrollPosition = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleHistoryRestore() {
|
function handleHistoryRestore() {
|
||||||
|
activeRequests = 0
|
||||||
hide()
|
hide()
|
||||||
|
|
||||||
// Set grace period to ignore change events triggered by history restore
|
// Set grace period to ignore change events triggered by history restore
|
||||||
@@ -56,23 +88,29 @@ function handleHistoryRestore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTimeout() {
|
function handleTimeout() {
|
||||||
hide()
|
// Don't decrement here — htmx:afterRequest always fires alongside htmx:timeout
|
||||||
|
// and handles the counter. This handler only shows the alert.
|
||||||
alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.')
|
alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBeforeHistorySave() {
|
function handleBeforeHistorySave() {
|
||||||
// Hide indicator before HTMX saves the page to history cache
|
// Hide indicator before HTMX saves the page to history cache
|
||||||
|
activeRequests = 0
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageShow() {
|
||||||
|
activeRequests = 0
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize event listeners
|
// Initialize event listeners
|
||||||
document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest)
|
document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest)
|
||||||
document.body.addEventListener('htmx:afterRequest', hide)
|
document.body.addEventListener('htmx:afterRequest', handleRequestEnd)
|
||||||
|
document.body.addEventListener('htmx:afterSwap', handleAfterSwap)
|
||||||
document.body.addEventListener('htmx:timeout', handleTimeout)
|
document.body.addEventListener('htmx:timeout', handleTimeout)
|
||||||
document.body.addEventListener('htmx:historyRestore', handleHistoryRestore)
|
document.body.addEventListener('htmx:historyRestore', handleHistoryRestore)
|
||||||
document.body.addEventListener('htmx:sendError', hide)
|
|
||||||
document.body.addEventListener('htmx:responseError', hide)
|
|
||||||
document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave)
|
document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave)
|
||||||
|
|
||||||
window.addEventListener('pageshow', hide)
|
window.addEventListener('pageshow', handlePageShow)
|
||||||
window.addEventListener('popstate', handleHistoryRestore)
|
window.addEventListener('popstate', handleHistoryRestore)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
@import "components/typography.css";
|
|
||||||
@import "components/forms.css";
|
|
||||||
@import "components/button.css";
|
@import "components/button.css";
|
||||||
@import "components/tooltip.css";
|
@import "components/datatable.css";
|
||||||
@import "components/toast.css";
|
@import "components/forms.css";
|
||||||
@import "components/pagination.css";
|
@import "components/pagination.css";
|
||||||
|
@import "components/toast.css";
|
||||||
|
@import "components/tooltip.css";
|
||||||
|
@import "components/typography.css";
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.button {
|
.button {
|
||||||
@apply inline-flex items-center justify-center space-x-2 h-10 leading-10 px-4 md:px-8 cursor-pointer;
|
@apply inline-flex items-center justify-center space-x-2 h-8 leading-8 md:h-10 md:leading-10 px-4 md:px-8 cursor-pointer;
|
||||||
@apply uppercase leading-none text-center;
|
@apply uppercase leading-none text-center text-sm md:text-base;
|
||||||
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-light;
|
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-dark;
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,42 +20,52 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button--secondary {
|
.button--secondary {
|
||||||
@apply bg-none bg-primary-dark text-white;
|
@apply bg-none bg-primary-light text-white;
|
||||||
@apply hover:bg-primary-light/80;
|
@apply hover:bg-primary-light/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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/80;
|
@apply hover:bg-none hover:bg-sbw-primary/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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/80;
|
@apply hover:bg-none hover:bg-sbw-primary/50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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/80;
|
@apply hover:bg-none hover:bg-snz-primary/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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/80;
|
@apply hover:bg-none hover:bg-snz-primary/50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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/80;
|
@apply hover:bg-none hover:bg-ser-primary/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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/80;
|
@apply hover:bg-none hover:bg-ser-primary/50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -14,12 +14,17 @@
|
|||||||
"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",
|
||||||
|
"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",
|
||||||
"nelexa/zip": "^4.0",
|
"nelexa/zip": "^4.0",
|
||||||
"nesbot/carbon": "^3.8",
|
"nesbot/carbon": "^3.8",
|
||||||
"phpdocumentor/reflection-docblock": "^5.6",
|
"phpdocumentor/reflection-docblock": "^5.6",
|
||||||
|
"phpoffice/phpspreadsheet": "^5.4",
|
||||||
"phpstan/phpdoc-parser": "^2.0",
|
"phpstan/phpdoc-parser": "^2.0",
|
||||||
"spatie/crypto": "^2.1",
|
"spatie/crypto": "^2.1",
|
||||||
"symfony/apache-pack": "^1.0",
|
"symfony/apache-pack": "^1.0",
|
||||||
@@ -58,6 +63,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -69,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
"sort-packages": true,
|
"sort-packages": true,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "8.2.28"
|
"php": "8.3.30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -92,6 +98,8 @@
|
|||||||
"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"
|
||||||
@@ -116,9 +124,11 @@
|
|||||||
"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",
|
||||||
"spatie/ray": "^1.43",
|
"rector/rector": "^2.4",
|
||||||
"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.*",
|
||||||
|
|||||||
@@ -16,4 +16,7 @@ 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],
|
||||||
|
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
|
||||||
|
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
@@ -15,5 +15,7 @@ framework:
|
|||||||
#app: cache.adapter.apcu
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
# Namespaced pools use the above "app" backend by default
|
# Namespaced pools use the above "app" backend by default
|
||||||
#pools:
|
pools:
|
||||||
#my.dedicated.cache: null
|
bpn.cache:
|
||||||
|
adapter: cache.adapter.filesystem
|
||||||
|
tags: true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
flagception:
|
||||||
|
features: {}
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
flysystem:
|
flysystem:
|
||||||
storages:
|
storages:
|
||||||
xml_export.storage:
|
xml_export.storage:
|
||||||
adapter: 'local'
|
local:
|
||||||
options:
|
|
||||||
directory: '%env(resolve:XML_EXPORT_PATH)%'
|
directory: '%env(resolve:XML_EXPORT_PATH)%'
|
||||||
xml_source.storage:
|
xml_source.storage:
|
||||||
adapter: 'sftp'
|
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:
|
||||||
adapter: 'local'
|
local:
|
||||||
options:
|
|
||||||
directory: '%kernel.project_dir%/var/bpn'
|
directory: '%kernel.project_dir%/var/bpn'
|
||||||
json_export.storage:
|
json_export.storage:
|
||||||
adapter: 'local'
|
local:
|
||||||
options:
|
|
||||||
directory: '%env(resolve:JSON_EXPORT_PATH)%'
|
directory: '%env(resolve:JSON_EXPORT_PATH)%'
|
||||||
@@ -9,9 +9,8 @@ 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: auto
|
cookie_secure: true
|
||||||
# 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
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
knp_menu:
|
||||||
|
twig:
|
||||||
|
template: _partials/_menu.html.twig
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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,9 +3,10 @@ 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: false
|
enable_refresh_token_grant: true
|
||||||
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,9 +21,10 @@ framework:
|
|||||||
messenger.bus.default: []
|
messenger.bus.default: []
|
||||||
|
|
||||||
routing:
|
routing:
|
||||||
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
|
Symfony\Component\Mailer\Messenger\SendEmailMessage: sync
|
||||||
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,6 +4,7 @@ monolog:
|
|||||||
- bpn
|
- bpn
|
||||||
- core
|
- core
|
||||||
- auth
|
- auth
|
||||||
|
- admin
|
||||||
|
|
||||||
when@dev:
|
when@dev:
|
||||||
monolog:
|
monolog:
|
||||||
@@ -27,11 +28,16 @@ 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"]
|
channels: ["!event", "!bpn", "!core", "!auth", "!admin"]
|
||||||
# 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:
|
||||||
@@ -59,7 +65,7 @@ when@test:
|
|||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||||
level: debug
|
level: debug
|
||||||
|
|
||||||
when@prod:
|
when@prod: &prod_config
|
||||||
monolog:
|
monolog:
|
||||||
handlers:
|
handlers:
|
||||||
database:
|
database:
|
||||||
@@ -84,13 +90,19 @@ when@prod:
|
|||||||
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"]
|
channels: ["!bpn", "!core", "!auth", "!admin"]
|
||||||
nested:
|
nested:
|
||||||
type: rotating_file
|
type: rotating_file
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.framework.log"
|
path: "%kernel.logs_dir%/%kernel.environment%.framework.log"
|
||||||
@@ -105,3 +117,5 @@ when@prod:
|
|||||||
channels: [deprecation]
|
channels: [deprecation]
|
||||||
path: php://stderr
|
path: php://stderr
|
||||||
formatter: monolog.formatter.json
|
formatter: monolog.formatter.json
|
||||||
|
|
||||||
|
when@staging: *prod_config
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
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:
|
||||||
@@ -8,6 +9,12 @@ 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)/
|
||||||
@@ -19,7 +26,15 @@ 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
|
||||||
@@ -32,8 +47,10 @@ 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: '^/', roles: PUBLIC_ACCESS, requires_channel: https }
|
- { path: ^/admin, roles: ROLE_ADMIN, requires_channel: https }
|
||||||
|
- { path: ^/, roles: PUBLIC_ACCESS, requires_channel: https }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
@@ -42,6 +59,7 @@ 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,9 +1,15 @@
|
|||||||
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)%'
|
||||||
|
customer_service_email: '%env(APP_CUSTOMER_SERVICE_EMAIL)%'
|
||||||
|
legal_notice_url: '%env(APP_LEGAL_NOTICE_URL)%'
|
||||||
|
terms_and_conditions_url: '%env(APP_TERMS_AND_CONDITIONS_URL)%'
|
||||||
|
privacy_policy_url: '%env(APP_PRIVACY_POLICY_URL)%'
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
twig:
|
twig:
|
||||||
|
|||||||
@@ -9,21 +9,52 @@ zenstruck_schedule:
|
|||||||
|
|
||||||
schedule_extensions:
|
schedule_extensions:
|
||||||
email_on_failure:
|
email_on_failure:
|
||||||
to: [email protected]
|
enabled: true
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- task: app:cleanup:xml-dumps
|
- task: app:cleanup:xml-dumps
|
||||||
frequency: "0 1 * * *"
|
frequency: "0 1 * * *"
|
||||||
description: "Removes outdated XML dumps of requests/responses to BPN API for debugging"
|
description: "Removes outdated XML dumps of requests/responses to BPN API for debugging"
|
||||||
|
|
||||||
- task: app:bpn:xml-sync
|
|
||||||
frequency: "*/15 8-19 * * *"
|
|
||||||
description: "Sync BusPro XML data every 15 min. during peak hours"
|
|
||||||
|
|
||||||
- task: app:bpn:xml-sync
|
|
||||||
frequency: "0 20-23,0-7 * * *"
|
|
||||||
description: "Sync BusPro XML data hourly outside peak hours"
|
|
||||||
|
|
||||||
- task: app:cleanup:log-entries
|
- task: app:cleanup:log-entries
|
||||||
frequency: "30 1 * * *"
|
frequency: "15 1 * * *"
|
||||||
description: "Removes outdated log entries with a retention period of 6 months"
|
description: "Removes outdated log entries with a retention period of 6 months"
|
||||||
|
|
||||||
|
- task: app:draft:cleanup
|
||||||
|
frequency: "30 1 * * *"
|
||||||
|
description: "Removes expired drafts"
|
||||||
|
|
||||||
|
- task: app:cleanup:newsletter-opt-in-requests
|
||||||
|
frequency: "45 1 * * *"
|
||||||
|
description: "Removes expired pending newsletter double opt-in requests"
|
||||||
|
|
||||||
|
- task: league:oauth2-server:clear-expired-tokens
|
||||||
|
frequency: "0 * * * *"
|
||||||
|
description: "Removes expired oauth2 tokens hourly"
|
||||||
|
|
||||||
|
- task: app:bpn:refresh-travel-snapshot --refresh-after 60
|
||||||
|
frequency: "10 8-19 * * *"
|
||||||
|
description: "Syncs travel data snapshots with BusPro XML data hourly during peak hours"
|
||||||
|
|
||||||
|
- task: app:bpn:refresh-travel-snapshot --refresh-after 180
|
||||||
|
frequency: "10 20-23,0-7 * * *"
|
||||||
|
description: "Syncs travel data snapshots with BusPro XML data every 3 hours outside peak hours"
|
||||||
|
|
||||||
|
when@staging:
|
||||||
|
zenstruck_schedule:
|
||||||
|
mailer:
|
||||||
|
subject_prefix: "[MyE&P STAGING]"
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
zenstruck_schedule:
|
||||||
|
mailer:
|
||||||
|
subject_prefix: "[MyE&P PROD]"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- task: app:bpn:xml-sync
|
||||||
|
frequency: "*/15 8-19 * * *"
|
||||||
|
description: "Syncs BusPro XML data every 15 min. during peak hours"
|
||||||
|
|
||||||
|
- task: app:bpn:xml-sync
|
||||||
|
frequency: "0 20-23,0-7 * * *"
|
||||||
|
description: "Syncs BusPro XML data hourly outside peak hours"
|
||||||
@@ -5,28 +5,62 @@
|
|||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
travel_info_base_url: '%env(APP_TRAVEL_INFO_BASE_URL)%'
|
travel_info_base_url: '%env(APP_TRAVEL_INFO_BASE_URL)%'
|
||||||
|
terms_and_conditions_url: '%env(APP_TERMS_AND_CONDITIONS_URL)%'
|
||||||
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)%'
|
||||||
|
newsletter_confirmation_ttl_hours: '%env(int:APP_NEWSLETTER_CONFIRMATION_TTL_HOURS)%'
|
||||||
|
travel_snapshot_retention_buffer_days: '%env(int:APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS)%'
|
||||||
|
default_email_from: '%env(APP_DEFAULT_EMAIL_FROM)%'
|
||||||
|
default_email_to: '%env(APP_DEFAULT_EMAIL_TO)%'
|
||||||
|
|
||||||
# Body dimensions choices for BodyDimensionsType
|
# MailJet list ids and their labels
|
||||||
body_dimensions.height_choices:
|
mailjet_lists:
|
||||||
'bis 148cm': '-148'
|
10321569: 'E&P Newsletter'
|
||||||
'149 - 157cm': '149-157'
|
10321382: 'Reisen-Alert Ahrntal'
|
||||||
'158 - 166cm': '158-166'
|
10321383: 'Reisen-Alert Davos'
|
||||||
'167 - 178cm': '167-178'
|
10321391: 'Reisen-Alert Familienreisen'
|
||||||
'179 - 185cm': '179-185'
|
10321384: 'Reisen-Alert Arosa-Lenzerheide'
|
||||||
'186 - 194cm': '186-194'
|
10321385: 'Reisen-Alert Les Deux Alpes'
|
||||||
'195cm oder mehr': '195cm+'
|
10321386: 'Reisen-Alert Montafon'
|
||||||
body_dimensions.shoe_size_min: 36
|
10321387: 'Reisen-Alert Portes du Soleil'
|
||||||
body_dimensions.shoe_size_max: 48
|
10321388: 'Reisen-Alert Saalbach-Hinterglemm'
|
||||||
body_dimensions.weight_choices:
|
10538809: 'Reisen-Alert Scuol'
|
||||||
'42 - 48kg': '42-48'
|
10321389: 'Reisen-Alert Stubaital'
|
||||||
'49 - 57kg': '49-57'
|
10554990: 'Reisen-Alert Ski & Boarderweek'
|
||||||
'58 - 66kg': '58-66'
|
|
||||||
'67 - 78kg': '67-78'
|
# MailJet contact metadata names for name synchronization
|
||||||
'79 - 85kg': '79-85'
|
mailjet_contact_metadata_fields:
|
||||||
'86 - 94kg': '86-94'
|
firstName: 'vorname'
|
||||||
'95kg oder mehr': '95k+'
|
lastName: 'nachname'
|
||||||
|
|
||||||
|
# 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_config:
|
||||||
|
ep-reisen.de:
|
||||||
|
theme: base
|
||||||
|
gtm_id: GTM-56LWRF
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.ep-reisen.de-28eabc35-7085-4d8f-b450-522bd1a5247c/consent.js
|
||||||
|
ski-boarderweek.de:
|
||||||
|
theme: sbw
|
||||||
|
gtm_id: GTM-MS2C8CH
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.ski-boarderweek.de-d3641c34-8183-4b8d-b56e-c052e7a9223e/consent.js
|
||||||
|
semesterende-skireisen.de:
|
||||||
|
theme: ser
|
||||||
|
gtm_id: GTM-MNKDKL9
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.semesterende-skireisen.de-c9b3422d-eb62-4ce4-b8f7-91e9b509e26f/consent.js
|
||||||
|
snowzone.net:
|
||||||
|
theme: snz
|
||||||
|
gtm_id: GTM-M96CKL
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.snowzone.net-8e6aea0c-7d93-42a0-a822-29fff2aa5613/consent.js
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
@@ -39,6 +73,7 @@ services:
|
|||||||
$xmlDump: '@xml_dump.storage'
|
$xmlDump: '@xml_dump.storage'
|
||||||
$jsonExport: '@json_export.storage'
|
$jsonExport: '@json_export.storage'
|
||||||
$travelInfoBaseUrl: '%env(APP_TRAVEL_INFO_BASE_URL)%'
|
$travelInfoBaseUrl: '%env(APP_TRAVEL_INFO_BASE_URL)%'
|
||||||
|
$termsAndConditionsUrl: '%terms_and_conditions_url%'
|
||||||
$logger: '@monolog.logger.core'
|
$logger: '@monolog.logger.core'
|
||||||
$environment: '%kernel.environment%'
|
$environment: '%kernel.environment%'
|
||||||
|
|
||||||
@@ -59,7 +94,7 @@ services:
|
|||||||
bpn_username: '%env(APP_BPN_USER)%'
|
bpn_username: '%env(APP_BPN_USER)%'
|
||||||
bpn_password: '%env(APP_BPN_PASSWORD)%'
|
bpn_password: '%env(APP_BPN_PASSWORD)%'
|
||||||
bpn_api_ip: '%env(APP_BPN_IP)%'
|
bpn_api_ip: '%env(APP_BPN_IP)%'
|
||||||
bpn_api_port: '%env(APP_BPN_PORT)%'
|
bpn_api_ports: '%env(csv:APP_BPN_PORTS)%'
|
||||||
debug: '%env(bool:APP_BPN_DEBUG)%'
|
debug: '%env(bool:APP_BPN_DEBUG)%'
|
||||||
connection_timeout: '%env(int:APP_BPN_CONNECTION_TIMEOUT)%'
|
connection_timeout: '%env(int:APP_BPN_CONNECTION_TIMEOUT)%'
|
||||||
stream_timeout: '%env(int:APP_BPN_STREAM_TIMEOUT)%'
|
stream_timeout: '%env(int:APP_BPN_STREAM_TIMEOUT)%'
|
||||||
@@ -73,17 +108,65 @@ 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\TravelDataService:
|
App\Service\TravelSnapshotManager:
|
||||||
|
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\VoucherValidationService:
|
App\Service\ParticipantFormSupport:
|
||||||
|
arguments:
|
||||||
|
$bodyDimensionRanges: '%body_dimension_ranges%'
|
||||||
|
|
||||||
|
App\Service\BookingEditDataLoader:
|
||||||
|
arguments:
|
||||||
|
$bpnCache: '@bpn.cache'
|
||||||
|
|
||||||
|
App\BusProNet\XmlLoader\TravelLoader:
|
||||||
|
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'
|
||||||
@@ -100,28 +183,81 @@ 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'
|
||||||
- 'App\Form\Service\ParticipantTransportationInboundFieldHandler'
|
- 'App\Form\Service\ParticipantTransportationInboundFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantPickupFieldHandler'
|
- 'App\Form\Service\ParticipantPickupFieldHandler'
|
||||||
|
- 'App\Form\Service\ParticipantDropOffFieldHandler'
|
||||||
- 'App\Form\Service\ParticipantParkingFieldHandler'
|
- 'App\Form\Service\ParticipantParkingFieldHandler'
|
||||||
- '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'
|
||||||
|
|
||||||
App\Service\CmsDataService:
|
# Booking Status Rules
|
||||||
|
App\BusProNet\Service\StatusRule\Selection1473StatusRule: ~
|
||||||
|
App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule: ~
|
||||||
|
|
||||||
|
# Booking Status Rule Registry
|
||||||
|
App\BusProNet\Service\BookingStatusRuleRegistry:
|
||||||
|
arguments:
|
||||||
|
$rules:
|
||||||
|
- '@App\BusProNet\Service\StatusRule\Selection1473StatusRule'
|
||||||
|
- '@App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule'
|
||||||
|
|
||||||
|
App\Service\CmsDataProvider:
|
||||||
arguments:
|
arguments:
|
||||||
$httpClient: '@typo3.client'
|
$httpClient: '@typo3.client'
|
||||||
$apiKey: 'AbcAbc123'# dummy key, not yet implemented
|
$apiKey: 'AbcAbc123'# dummy key, not yet implemented
|
||||||
|
|
||||||
|
App\Logger\RequestIdProcessor:
|
||||||
|
tags:
|
||||||
|
- { 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:
|
||||||
|
arguments:
|
||||||
|
$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%'
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
parameters:
|
||||||
|
# domain mapping for theme, gtm id and cmp url
|
||||||
|
domain_config:
|
||||||
|
myep.ddev.site:
|
||||||
|
theme: base
|
||||||
|
gtm_id: GTM-56LWRF
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.ep-reisen.de-28eabc35-7085-4d8f-b450-522bd1a5247c/consent.js
|
||||||
|
myswb.ddev.site:
|
||||||
|
theme: sbw
|
||||||
|
gtm_id: GTM-MS2C8CH
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.ski-boarderweek.de-d3641c34-8183-4b8d-b56e-c052e7a9223e/consent.js
|
||||||
|
myser.ddev.site:
|
||||||
|
theme: ser
|
||||||
|
gtm_id: GTM-MNKDKL9
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.semesterende-skireisen.de-c9b3422d-eb62-4ce4-b8f7-91e9b509e26f/consent.js
|
||||||
|
mysnz.ddev.site:
|
||||||
|
theme: snz
|
||||||
|
gtm_id: GTM-M96CKL
|
||||||
|
cmp_url: https://consent.cookiefirst.com/sites/my.snowzone.net-8e6aea0c-7d93-42a0-a822-29fff2aa5613/consent.js
|
||||||
@@ -17,7 +17,6 @@ add('shared_dirs', [
|
|||||||
|
|
||||||
add('shared_files', [
|
add('shared_files', [
|
||||||
'public/.htaccess',
|
'public/.htaccess',
|
||||||
'.env.local.php',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
set('keep_releases', 3);
|
set('keep_releases', 3);
|
||||||
@@ -33,6 +32,7 @@ $rsyncOptions = [
|
|||||||
'.ddev',
|
'.ddev',
|
||||||
'.git',
|
'.git',
|
||||||
'.github',
|
'.github',
|
||||||
|
'.jj',
|
||||||
'node_modules',
|
'node_modules',
|
||||||
'.editorconfig',
|
'.editorconfig',
|
||||||
'.env.dev.local',
|
'.env.dev.local',
|
||||||
@@ -46,14 +46,17 @@ $rsyncOptions = [
|
|||||||
'.phpunit.result.cache',
|
'.phpunit.result.cache',
|
||||||
'config/secret/',
|
'config/secret/',
|
||||||
'CLAUDE.md',
|
'CLAUDE.md',
|
||||||
|
'cypress*',
|
||||||
'deploy.php',
|
'deploy.php',
|
||||||
|
'docs',
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
'*.http',
|
'*.http',
|
||||||
'http-client.private.env.json',
|
'http-client*.json',
|
||||||
'postcss.config.js',
|
'postcss.config.js',
|
||||||
'phpunit.xml.dist',
|
'phpunit.xml.dist',
|
||||||
'public/.htaccess',
|
'public/.htaccess',
|
||||||
|
'ray.php',
|
||||||
'tailwind.config.js',
|
'tailwind.config.js',
|
||||||
'webpack.config.js',
|
'webpack.config.js',
|
||||||
'bin/.phpunit',
|
'bin/.phpunit',
|
||||||
@@ -74,31 +77,31 @@ $rsyncOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
host('prod')
|
host('prod')
|
||||||
->setHostname('92.205.212.39')
|
->setHostname('185.243.135.29')
|
||||||
->setRemoteUser('ssh-53857-root')
|
->setRemoteUser('p704161')
|
||||||
->setForwardAgent(true)
|
->setForwardAgent(true)
|
||||||
->setSshMultiplexing(true)
|
->setSshMultiplexing(true)
|
||||||
->setDeployPath('/kunden/tiefschnee.de/myep')
|
->setDeployPath('/home/www/p704161/html/myep')
|
||||||
->set('writable_mode', 'chmod')
|
|
||||||
->set('http_user', 'ssh-53857-root')
|
|
||||||
->set('bin/php', '/usr/bin/php82')
|
|
||||||
->set('rsync_src', __DIR__)
|
|
||||||
->set('rsync', $rsyncOptions)
|
|
||||||
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{release_or_current_path}}/public/ --web-url=https://my.ep-reisen.de')
|
|
||||||
;
|
|
||||||
|
|
||||||
host('staging')
|
|
||||||
->setHostname('185.243.132.170')
|
|
||||||
->setRemoteUser('p546473')
|
|
||||||
->setForwardAgent(true)
|
|
||||||
->setSshMultiplexing(true)
|
|
||||||
->setDeployPath('/home/www/p546473/html/myep')
|
|
||||||
->set('writable_mode', 'chmod')
|
->set('writable_mode', 'chmod')
|
||||||
|
->set('http_user', 'p704161')
|
||||||
->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('keep_releases', 5)
|
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/public/ --web-url=https://my.ep-reisen.de')
|
||||||
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{release_or_current_path}}/public/ --web-url=https://myep.dreipunktnull.net')
|
;
|
||||||
|
|
||||||
|
host('staging')
|
||||||
|
->setHostname('185.243.135.29')
|
||||||
|
->setRemoteUser('p704161')
|
||||||
|
->setForwardAgent(true)
|
||||||
|
->setSshMultiplexing(true)
|
||||||
|
->setDeployPath('/home/www/p704161/html/myep-staging')
|
||||||
|
->set('writable_mode', 'chmod')
|
||||||
|
->set('http_user', 'p704161')
|
||||||
|
->set('bin/php', '/usr/local/bin/php')
|
||||||
|
->set('rsync_src', __DIR__)
|
||||||
|
->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')
|
||||||
;
|
;
|
||||||
|
|
||||||
task('deploy', [
|
task('deploy', [
|
||||||
@@ -110,14 +113,16 @@ task('deploy', [
|
|||||||
'rsync',
|
'rsync',
|
||||||
'deploy:shared',
|
'deploy:shared',
|
||||||
'deploy:writable',
|
'deploy:writable',
|
||||||
|
'deploy:cache:clear',
|
||||||
'database:migrate',
|
'database:migrate',
|
||||||
'deploy:publish',
|
|
||||||
'cachetool:clear:opcache',
|
'cachetool:clear:opcache',
|
||||||
|
'deploy:publish',
|
||||||
'deploy:stop-workers',
|
'deploy:stop-workers',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
task('deploy:assets', function () {
|
task('deploy:assets', function () {
|
||||||
runLocally('npm i && npm run build');
|
runLocally('ddev exec npm ci');
|
||||||
|
runLocally('ddev exec npm run build');
|
||||||
});
|
});
|
||||||
|
|
||||||
task('deploy:stop-workers', function () {
|
task('deploy:stop-workers', function () {
|
||||||
|
|||||||
@@ -1,475 +0,0 @@
|
|||||||
# Symfony 7.4 Upgrade Plan
|
|
||||||
|
|
||||||
**Project:** MyEP Next Booking
|
|
||||||
**Current Version:** Symfony 6.4.31
|
|
||||||
**Target Version:** Symfony 7.4 (LTS)
|
|
||||||
**Date:** 2026-01-05
|
|
||||||
**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.2.29)
|
|
||||||
- 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.2.29 (compatible)
|
|
||||||
- OAuth2 Server Bundle: 1.1.0 (compatible)
|
|
||||||
- Stimulus Bundle: 2.32.0 (compatible)
|
|
||||||
- KnpMenuBundle: Removed (no longer in use)
|
|
||||||
- Doctrine ORM: 3.6.0 (compatible)
|
|
||||||
- All 312 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) |
|
|
||||||
|
|
||||||
### Items No Longer Relevant
|
|
||||||
| Item | Reason |
|
|
||||||
|------|--------|
|
|
||||||
| KnpMenuBundle upgrade | **Bundle removed from project** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current State Analysis
|
|
||||||
|
|
||||||
### PHP Version Compatibility
|
|
||||||
|
|
||||||
- **Current Runtime:** PHP 8.2.29
|
|
||||||
- **Composer Platform:** PHP 8.2.28
|
|
||||||
- **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 |
|
|
||||||
|
|
||||||
### 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 312 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/stopwatch": "7.4.*",
|
|
||||||
"symfony/web-profiler-bundle": "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.*"`
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
### 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 (312 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 (312 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:** 5.0
|
|
||||||
**Previous Version:** 4.0 (dated 2025-12-06)
|
|
||||||
**Last Updated:** 2026-01-05
|
|
||||||
**Status:** Ready for Symfony 7.4 Upgrade
|
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# 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.
|
||||||
@@ -7,8 +7,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://myep-next-booking.ddev.site/authorize",
|
"Auth URL": "https://myep.ddev.site/authorize",
|
||||||
"Token URL": "https://myep-next-booking.ddev.site/token",
|
"Token URL": "https://myep.ddev.site/token",
|
||||||
"Redirect URL": "https://myep-team.ddev.site/auth/check",
|
"Redirect URL": "https://myep-team.ddev.site/auth/check",
|
||||||
"Scope": "email"
|
"Scope": "email"
|
||||||
},
|
},
|
||||||
@@ -17,8 +17,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://myep-next-booking.ddev.site/authorize",
|
"Auth URL": "https://myep.ddev.site/authorize",
|
||||||
"Token URL": "https://myep-next-booking.ddev.site/token",
|
"Token URL": "https://myep.ddev.site/token",
|
||||||
"Redirect URL": "https://myep-team.ddev.site/auth/check",
|
"Redirect URL": "https://myep-team.ddev.site/auth/check",
|
||||||
"Scope": "email profile"
|
"Scope": "email profile"
|
||||||
},
|
},
|
||||||
@@ -27,8 +27,61 @@
|
|||||||
"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://myep-next-booking.ddev.site/authorize",
|
"Auth URL": "https://myep.ddev.site/authorize",
|
||||||
"Token URL": "https://myep-next-booking.ddev.site/token",
|
"Token URL": "https://myep.ddev.site/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://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"
|
"Scope": "api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,8 +95,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://myep.ep-reisen.de/authorize",
|
"Auth URL": "https://my.ep-reisen.de/authorize",
|
||||||
"Token URL": "https://myep.ep-reisen.de/token",
|
"Token URL": "https://my.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"
|
||||||
},
|
},
|
||||||
@@ -52,8 +105,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://myep.ep-reisen.de/authorize",
|
"Auth URL": "https://my.ep-reisen.de/authorize",
|
||||||
"Token URL": "https://myep.ep-reisen.de/token",
|
"Token URL": "https://my.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"
|
||||||
},
|
},
|
||||||
@@ -62,8 +115,17 @@
|
|||||||
"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://myep.ep-reisen.de/authorize",
|
"Auth URL": "https://my.ep-reisen.de/authorize",
|
||||||
"Token URL": "https://myep.ep-reisen.de/token",
|
"Token URL": "https://my.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
###
|
||||||
|
# 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}}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?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 Version20260106091117 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create booking_edit_draft table for persisting user edits during booking edit flow';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE booking_edit_draft (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, booking_id INT NOT NULL, form_data JSON NOT NULL COMMENT \'(DC2Type:json)\', created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_E6548098A76ED395 (user_id), UNIQUE INDEX user_booking_unique (user_id, booking_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft ADD CONSTRAINT FK_E6548098A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft DROP FOREIGN KEY FK_E6548098A76ED395');
|
||||||
|
$this->addSql('DROP TABLE booking_edit_draft');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?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 Version20260109081315 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 ADD travel_date DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\'');
|
||||||
|
$this->addSql('CREATE INDEX IDX_DRAFT_TRAVEL_DATE ON booking_edit_draft (travel_date)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('DROP INDEX IDX_DRAFT_TRAVEL_DATE ON booking_edit_draft');
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft DROP travel_date');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260110105253 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add booking_number column to booking_edit_draft table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft ADD booking_number INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft DROP booking_number');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20260113140000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add date_id and hotel_id columns to booking_edit_draft table for export functionality';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft ADD date_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft ADD hotel_id INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft DROP date_id');
|
||||||
|
$this->addSql('ALTER TABLE booking_edit_draft DROP hotel_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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 Version20260119174522 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add profile_complete flag to user table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE user ADD profile_complete TINYINT(1) NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE user DROP profile_complete');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?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`');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": "^5.0.0",
|
"@symfony/webpack-encore": "^6.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,8 +20,7 @@
|
|||||||
"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": "^5.1.0",
|
"webpack-cli": "^6.0.1"
|
||||||
"webpack-notifier": "^1.15.0"
|
|
||||||
},
|
},
|
||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 124 74" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><g><path d="M107,0.3c-36.4,5.5 -81.6,19.1 -81.6,38.2c0,17.7 41.8,13.2 41.8,13.2c0,0 -67,22.3 -67,-7c0,-33.2 84.5,-44.4 106.8,-44.4Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M17,73.5c36.4,-5.4 81.6,-19.1 81.6,-38.2c0,-17.7 -41.8,-13.2 -41.8,-13.2c0,0 67,-22.3 67,7c0,33.3 -84.5,44.4 -106.8,44.4Z" style="fill:#f8c62f;fill-rule:nonzero;"/><g><path d="M60.7,34.3l-12.8,4l12.2,0.8l-0.4,-2.4l1,-2.4Z" style="fill:#0084d4;fill-rule:nonzero;"/><path d="M75.9,35.2l-9.2,2.9l-3.5,9.7l-1.7,-11.3l4,-10.7l1.7,7.9l8.7,1.5Z" style="fill:#f8c62f;fill-rule:nonzero;"/></g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 995 B |
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"icons": [
|
|
||||||
{ "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
|
|
||||||
{ "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
return [
|
|
||||||
'host' => 'host.orb.internal',
|
|
||||||
'port' => 23517,
|
|
||||||
'remote_path' => '/var/www/html',
|
|
||||||
'local_path' => '/Users/fromme/Development/Symfony/myep-next-booking',
|
|
||||||
];
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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');
|
||||||
@@ -4,39 +4,44 @@ namespace App\BusProNet;
|
|||||||
|
|
||||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||||
use App\BusProNet\Exception\ApiClientException;
|
use App\BusProNet\Exception\ApiClientException;
|
||||||
|
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||||
use App\BusProNet\Exception\ResponseParserException;
|
use App\BusProNet\Exception\ResponseParserException;
|
||||||
|
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\ContactFormResponse;
|
|
||||||
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\ServiceAvailabilityResponse;
|
use App\BusProNet\Model\ServiceAvailabilityResponse;
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
use App\BusProNet\Traits\ApiClientTrait;
|
|
||||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\RegistrationDto;
|
use App\Form\Model\RegistrationDto;
|
||||||
|
use App\Service\RequestIdGenerator;
|
||||||
use League\Flysystem\FilesystemException;
|
use League\Flysystem\FilesystemException;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\FilesystemOperator;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
use Symfony\Component\Serializer\Encoder\XmlEncoder;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
class ApiClient
|
class ApiClient
|
||||||
{
|
{
|
||||||
use ApiClientTrait;
|
|
||||||
|
|
||||||
public const TYPE_NOTIFICATION = 'HINWEIS';
|
public const TYPE_NOTIFICATION = 'HINWEIS';
|
||||||
public const TYPE_CUSTOMER_DATA = 'KUNDENKONTO';
|
public const TYPE_CUSTOMER_DATA = 'KUNDENKONTO';
|
||||||
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';
|
||||||
@@ -46,14 +51,21 @@ 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 int $selectedPort;
|
||||||
|
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,
|
||||||
private readonly FilesystemOperator $xmlDump,
|
private readonly FilesystemOperator $xmlDump,
|
||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
private readonly BookingDataProcessor $bookingDataProcessor,
|
private readonly BookingDataProcessor $bookingDataProcessor,
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly RequestIdGenerator $requestIdGenerator,
|
||||||
array $options,
|
array $options,
|
||||||
) {
|
) {
|
||||||
$this->config = $this->resolveOptions($options);
|
$this->config = $this->resolveOptions($options);
|
||||||
@@ -79,21 +91,14 @@ class ApiClient
|
|||||||
/**
|
/**
|
||||||
* @throws ApiClientException
|
* @throws ApiClientException
|
||||||
*/
|
*/
|
||||||
public function register(RegistrationDto $registrationData): Notification
|
public function register(RegistrationDto $registrationData): Notification|RegistrationResponse
|
||||||
{
|
{
|
||||||
$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),
|
||||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||||
'art' => 'Adresse_Neu',
|
'art' => 'Adresse_Neu',
|
||||||
'adressdaten' => [
|
'adressdaten' => $registrationData->toPayload(),
|
||||||
'geschlecht' => $registrationData->gender,
|
|
||||||
'vorname' => $registrationData->firstName,
|
|
||||||
'name' => $registrationData->name,
|
|
||||||
'kommunikation' => [
|
|
||||||
'email' => $registrationData->email,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||||
@@ -123,7 +128,7 @@ class ApiClient
|
|||||||
string $password,
|
string $password,
|
||||||
PersonalData $personalData,
|
PersonalData $personalData,
|
||||||
bool $debug = false,
|
bool $debug = false,
|
||||||
): Notification|PersonalData {
|
): Notification|PersonalDataUpdateResponse {
|
||||||
$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),
|
||||||
@@ -144,13 +149,13 @@ class ApiClient
|
|||||||
public function createAddress(
|
public function createAddress(
|
||||||
PersonalData $personalData,
|
PersonalData $personalData,
|
||||||
bool $debug = false,
|
bool $debug = false,
|
||||||
): ContactFormResponse|Notification {
|
): RegistrationResponse|Notification {
|
||||||
$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),
|
||||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||||
'art' => 'Adresse_Neu',
|
'art' => 'Adresse_Neu',
|
||||||
'adressdaten' => $personalData->toPayload(),
|
'adressdaten' => $personalData->toPayload(false),
|
||||||
'ohnemailversand' => 'True',
|
'ohnemailversand' => 'True',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -316,6 +321,21 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -397,6 +417,30 @@ class ApiClient
|
|||||||
return $this->sendRequest(static::TYPE_PRODUCT_DATA, $data, ['hotelId' => $hotelId]);
|
return $this->sendRequest(static::TYPE_PRODUCT_DATA, $data, ['hotelId' => $hotelId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches raw XML travel data from the BusProNet API.
|
||||||
|
*
|
||||||
|
* Returns the unprocessed XML response for direct storage in the XML export directory.
|
||||||
|
* The response format matches the XML export structure from BusPro.
|
||||||
|
*
|
||||||
|
* @param int $travelId The travel product ID to fetch
|
||||||
|
*
|
||||||
|
* @return string The raw XML response
|
||||||
|
*
|
||||||
|
* @throws ApiClientException If the API request fails
|
||||||
|
*/
|
||||||
|
public function getTravelDataXml(int $travelId): string
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'user' => $this->config['bpn_username'],
|
||||||
|
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_PRODUCT_DATA),
|
||||||
|
'satz' => ['@typ' => static::TYPE_PRODUCT_DATA],
|
||||||
|
'idprodukt' => $travelId,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->sendRequestRaw($data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws ApiClientException
|
* @throws ApiClientException
|
||||||
*/
|
*/
|
||||||
@@ -489,10 +533,11 @@ class ApiClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends raw XML to the BPN API with key regeneration.
|
* Sends raw XML to the BPN API with key regeneration and automatic retry.
|
||||||
*
|
*
|
||||||
* Parses the XML to extract the request type, regenerates the authentication key
|
* Parses the XML to extract the request type, regenerates the authentication key
|
||||||
* with the current date, and sends the request. Returns the raw XML response.
|
* with the current date, and sends the request. Returns the raw XML response.
|
||||||
|
* Automatically retries on immediate connection close (server busy).
|
||||||
*
|
*
|
||||||
* @param string $xml The raw XML request body
|
* @param string $xml The raw XML request body
|
||||||
* @param bool $debug Enable debug mode (XML dumps)
|
* @param bool $debug Enable debug mode (XML dumps)
|
||||||
@@ -503,7 +548,18 @@ class ApiClient
|
|||||||
*/
|
*/
|
||||||
public function sendRawXml(string $xml, bool $debug = false): string
|
public function sendRawXml(string $xml, bool $debug = false): string
|
||||||
{
|
{
|
||||||
$requestId = date(DATE_ATOM).uniqid();
|
return $this->executeWithRetry(fn () => $this->doSendRawXml($xml, $debug));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the actual raw XML request to the BPN API.
|
||||||
|
*
|
||||||
|
* @throws ApiClientException
|
||||||
|
* @throws ImmediateConnectionCloseException
|
||||||
|
*/
|
||||||
|
private function doSendRawXml(string $xml, bool $debug = false): string
|
||||||
|
{
|
||||||
|
$requestId = $this->getRequestId();
|
||||||
|
|
||||||
$doc = new \DOMDocument();
|
$doc = new \DOMDocument();
|
||||||
if (false === @$doc->loadXML($xml)) {
|
if (false === @$doc->loadXML($xml)) {
|
||||||
@@ -534,25 +590,19 @@ class ApiClient
|
|||||||
|
|
||||||
$body = $doc->saveXML();
|
$body = $doc->saveXML();
|
||||||
|
|
||||||
$this->logger->info('Sending raw XML request to BPN API', [
|
|
||||||
'requestId' => $requestId,
|
|
||||||
'type' => $type,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (true === $debug || true === $this->config['debug']) {
|
if (true === $debug || true === $this->config['debug']) {
|
||||||
$this->dumpXmlToFile('request', $requestId, $body);
|
$this->dumpXmlToFile('request', $requestId, $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
$socket = $this->connect(
|
$socket = $this->connect();
|
||||||
$this->config['bpn_api_ip'],
|
|
||||||
$this->config['bpn_api_port'],
|
$this->logger->info('Sending raw XML request to BPN API', [
|
||||||
$this->config['max_retries'],
|
'requestId' => $requestId,
|
||||||
$this->config['connection_timeout'],
|
'type' => $type,
|
||||||
$this->config['stream_timeout'],
|
'port' => $this->selectedPort,
|
||||||
$this->config['total_timeout']
|
]);
|
||||||
);
|
$this->send($socket, $body);
|
||||||
$this->send($socket, $body, $this->config['total_timeout']);
|
$response = $this->receive($socket);
|
||||||
$response = $this->receive($socket, $this->config['total_timeout']);
|
|
||||||
$this->disconnect($socket);
|
$this->disconnect($socket);
|
||||||
|
|
||||||
$responseXml = substr($response, 10);
|
$responseXml = substr($response, 10);
|
||||||
@@ -567,9 +617,81 @@ 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
|
||||||
{
|
{
|
||||||
$requestId = date(DATE_ATOM).uniqid();
|
return $this->executeWithRetry(fn () => $this->doSendRequest($type, $data, $additionalArgs, $debug), $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ApiClientException
|
||||||
|
*/
|
||||||
|
/** @param array<string, mixed> $data */
|
||||||
|
private function sendRequestRaw(array $data): string
|
||||||
|
{
|
||||||
|
return $this->executeWithRetry(fn () => $this->doSendRequestRaw($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes an operation with automatic retry on immediate connection close.
|
||||||
|
*
|
||||||
|
* When the BPN server is busy, it may close connections immediately without responding.
|
||||||
|
* This method detects such conditions and automatically retries after a short delay.
|
||||||
|
*
|
||||||
|
* @throws ImmediateConnectionCloseException If all retry attempts fail
|
||||||
|
*/
|
||||||
|
private function executeWithRetry(callable $operation, ?string $type = null): mixed
|
||||||
|
{
|
||||||
|
$maxAttempts = $this->config['busy_retry_attempts'];
|
||||||
|
$retryDelay = $this->config['busy_retry_delay'];
|
||||||
|
$lastException = null;
|
||||||
|
|
||||||
|
for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
|
||||||
|
try {
|
||||||
|
return $operation();
|
||||||
|
} catch (ImmediateConnectionCloseException $e) {
|
||||||
|
$lastException = $e;
|
||||||
|
|
||||||
|
if ($attempt < $maxAttempts) {
|
||||||
|
$context = [
|
||||||
|
'attempt' => $attempt,
|
||||||
|
'maxAttempts' => $maxAttempts,
|
||||||
|
'retryDelay' => $retryDelay,
|
||||||
|
];
|
||||||
|
if (null !== $type) {
|
||||||
|
$context['type'] = $type;
|
||||||
|
}
|
||||||
|
$this->logger->warning('BPN server busy, retrying request', $context);
|
||||||
|
sleep($retryDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = ['attempts' => $maxAttempts];
|
||||||
|
if (null !== $type) {
|
||||||
|
$context['type'] = $type;
|
||||||
|
}
|
||||||
|
$this->logger->error('BPN server busy after all retry attempts', $context);
|
||||||
|
|
||||||
|
throw $lastException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the actual request to the BPN API.
|
||||||
|
*
|
||||||
|
* @throws ApiClientException
|
||||||
|
* @throws ImmediateConnectionCloseException
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
* @param array<string, mixed> $additionalArgs
|
||||||
|
*/
|
||||||
|
private function doSendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed
|
||||||
|
{
|
||||||
|
$requestId = $this->getRequestId();
|
||||||
|
|
||||||
$body = $this
|
$body = $this
|
||||||
->serializer
|
->serializer
|
||||||
@@ -579,30 +701,33 @@ class ApiClient
|
|||||||
])
|
])
|
||||||
;
|
;
|
||||||
|
|
||||||
$this->logger->info('Sending request to BPN API', [
|
|
||||||
'requestId' => $requestId,
|
|
||||||
'type' => $data['satz']['@typ'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (true === $debug || true === $this->config['debug']) {
|
if (true === $debug || true === $this->config['debug']) {
|
||||||
$this->dumpXmlToFile('request', $requestId, $body);
|
$this->dumpXmlToFile('request', $requestId, $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
$socket = $this->connect(
|
$socket = $this->connect();
|
||||||
$this->config['bpn_api_ip'],
|
|
||||||
$this->config['bpn_api_port'],
|
$this->logger->info('Sending request to BPN API', [
|
||||||
$this->config['max_retries'],
|
'requestId' => $requestId,
|
||||||
$this->config['connection_timeout'],
|
'type' => $data['satz']['@typ'],
|
||||||
$this->config['stream_timeout'],
|
'port' => $this->selectedPort,
|
||||||
$this->config['total_timeout']
|
]);
|
||||||
);
|
|
||||||
$this->send($socket, $body, $this->config['total_timeout']);
|
$this->send($socket, $body);
|
||||||
$response = $this->receive($socket, $this->config['total_timeout']);
|
$response = $this->receive($socket);
|
||||||
$this->disconnect($socket);
|
$this->disconnect($socket);
|
||||||
|
|
||||||
// message length (10 bytes) is prepended to actual message
|
// message length (10 bytes) is prepended to actual message
|
||||||
$xml = substr($response, 10);
|
$xml = substr($response, 10);
|
||||||
|
|
||||||
|
if ('' === $xml) {
|
||||||
|
$this->logger->error('Empty response body received from API (header-only response)', [
|
||||||
|
'request_id' => $requestId,
|
||||||
|
'raw_length' => strlen($response),
|
||||||
|
]);
|
||||||
|
throw new ApiClientException('Empty response body received from API');
|
||||||
|
}
|
||||||
|
|
||||||
if (true === $debug || true === $this->config['debug']) {
|
if (true === $debug || true === $this->config['debug']) {
|
||||||
$this->dumpXmlToFile('response', $requestId, $xml);
|
$this->dumpXmlToFile('response', $requestId, $xml);
|
||||||
}
|
}
|
||||||
@@ -620,6 +745,46 @@ class ApiClient
|
|||||||
throw new ApiClientException('Unexpected response received from API');
|
throw new ApiClientException('Unexpected response received from API');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*
|
||||||
|
* @throws ApiClientException
|
||||||
|
* @throws ImmediateConnectionCloseException
|
||||||
|
*/
|
||||||
|
private function doSendRequestRaw(array $data): string
|
||||||
|
{
|
||||||
|
$requestId = $this->getRequestId();
|
||||||
|
|
||||||
|
$body = $this->serializer->serialize($data, 'xml', [
|
||||||
|
XmlEncoder::ROOT_NODE_NAME => 'anfrage',
|
||||||
|
XmlEncoder::ENCODING => 'UTF-8',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (true === $this->config['debug']) {
|
||||||
|
$this->dumpXmlToFile('request', $requestId, $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
$socket = $this->connect();
|
||||||
|
|
||||||
|
$this->logger->info('Sending raw request to BPN API', [
|
||||||
|
'requestId' => $requestId,
|
||||||
|
'type' => $data['satz']['@typ'],
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->send($socket, $body);
|
||||||
|
$response = $this->receive($socket);
|
||||||
|
$this->disconnect($socket);
|
||||||
|
|
||||||
|
$xml = substr($response, 10);
|
||||||
|
|
||||||
|
if (true === $this->config['debug']) {
|
||||||
|
$this->dumpXmlToFile('response', $requestId, $xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $xml;
|
||||||
|
}
|
||||||
|
|
||||||
private function dumpXmlToFile(string $type, string $requestId, string $body): void
|
private function dumpXmlToFile(string $type, string $requestId, string $body): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -628,6 +793,14 @@ class ApiClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getRequestId(): string
|
||||||
|
{
|
||||||
|
$baseId = $this->requestStack->getMainRequest()?->attributes->get('request_id')
|
||||||
|
?? $this->requestIdGenerator->generateBaseId();
|
||||||
|
|
||||||
|
return $baseId.'_'.++$this->requestCounter;
|
||||||
|
}
|
||||||
|
|
||||||
private function createKey(string $username, string $password, string $type): string
|
private function createKey(string $username, string $password, string $type): string
|
||||||
{
|
{
|
||||||
$date = (new \DateTimeImmutable())->format('Ymd');
|
$date = (new \DateTimeImmutable())->format('Ymd');
|
||||||
@@ -635,6 +808,175 @@ class ApiClient
|
|||||||
return md5($username.$password.$date.$type);
|
return md5($username.$password.$date.$type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return resource
|
||||||
|
*
|
||||||
|
* @throws ApiClientException
|
||||||
|
* @throws TimeoutException
|
||||||
|
*/
|
||||||
|
private function connect()
|
||||||
|
{
|
||||||
|
// Randomly select a port from the available ports for load balancing
|
||||||
|
$this->selectedPort = $this->config['bpn_api_ports'][array_rand($this->config['bpn_api_ports'])];
|
||||||
|
$this->operationStartTime = microtime(true);
|
||||||
|
|
||||||
|
$tries = 1;
|
||||||
|
$errNo = $errStr = '';
|
||||||
|
$errorCodesForRetry = [
|
||||||
|
SOCKET_ECONNREFUSED,
|
||||||
|
SOCKET_EBADF,
|
||||||
|
];
|
||||||
|
|
||||||
|
$openSocket = function (&$errNo, &$errStr) {
|
||||||
|
return @fsockopen(
|
||||||
|
$this->config['bpn_api_ip'],
|
||||||
|
$this->selectedPort,
|
||||||
|
$errNo,
|
||||||
|
$errStr,
|
||||||
|
$this->config['connection_timeout']
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
$socket = $openSocket($errNo, $errStr);
|
||||||
|
|
||||||
|
while (false === $socket && true === in_array($errNo, $errorCodesForRetry) && $this->config['max_retries'] > $tries) {
|
||||||
|
// Check if we've exceeded total timeout during retries
|
||||||
|
if (microtime(true) - $this->operationStartTime > $this->config['total_timeout']) {
|
||||||
|
$this->logger->error('Connection retry timeout exceeded', [
|
||||||
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
||||||
|
'total_timeout' => $this->config['total_timeout'],
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
throw new TimeoutException('Connection timeout exceeded during retries');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->warning('Could not connect to socket, retrying', [
|
||||||
|
'error_message' => $errStr,
|
||||||
|
'error_number' => $errNo,
|
||||||
|
'attempt' => $tries,
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
++$tries;
|
||||||
|
sleep(1);
|
||||||
|
$socket = $openSocket($errNo, $errStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false !== $socket) {
|
||||||
|
stream_set_timeout($socket, $this->config['stream_timeout']);
|
||||||
|
} else {
|
||||||
|
$this->logger->error('Unable to open socket', [
|
||||||
|
'error_message' => $errStr,
|
||||||
|
'error_number' => $errNo,
|
||||||
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
throw new ApiClientException('Unable to open socket');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resource $socket
|
||||||
|
*
|
||||||
|
* @throws TimeoutException
|
||||||
|
*/
|
||||||
|
private function send($socket, string $data): void
|
||||||
|
{
|
||||||
|
// Check total timeout before sending
|
||||||
|
if (microtime(true) - $this->operationStartTime > $this->config['total_timeout']) {
|
||||||
|
$this->logger->error('Total timeout exceeded before send', [
|
||||||
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
throw new TimeoutException('Total operation timeout exceeded before send');
|
||||||
|
}
|
||||||
|
|
||||||
|
// message length is prepended to actual message
|
||||||
|
$send = sprintf('%010s', strlen($data)).$data;
|
||||||
|
fwrite($socket, $send);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resource $socket
|
||||||
|
*
|
||||||
|
* @throws TimeoutException
|
||||||
|
* @throws ImmediateConnectionCloseException
|
||||||
|
*/
|
||||||
|
private function receive($socket): string
|
||||||
|
{
|
||||||
|
$response = '';
|
||||||
|
$readAttempts = 0;
|
||||||
|
$totalTimeout = $this->config['total_timeout'];
|
||||||
|
|
||||||
|
while (false === feof($socket)) {
|
||||||
|
// Check total timeout before each read
|
||||||
|
$elapsedTime = microtime(true) - $this->operationStartTime;
|
||||||
|
if ($elapsedTime > $totalTimeout) {
|
||||||
|
$this->logger->error('Total timeout exceeded during receive', [
|
||||||
|
'elapsed_time' => $elapsedTime,
|
||||||
|
'total_timeout' => $totalTimeout,
|
||||||
|
'bytes_received' => strlen($response),
|
||||||
|
'read_attempts' => $readAttempts,
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
throw new TimeoutException('Total operation timeout exceeded while receiving data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunk = fread($socket, 4096);
|
||||||
|
++$readAttempts;
|
||||||
|
|
||||||
|
// Check if stream timed out on this specific read
|
||||||
|
$metadata = stream_get_meta_data($socket);
|
||||||
|
if (true === $metadata['timed_out']) {
|
||||||
|
// Check if this is an immediate rejection (0 bytes, < 1 second)
|
||||||
|
// This indicates the server is busy rather than a true timeout
|
||||||
|
if (0 === strlen($response) && $elapsedTime < 1.0) {
|
||||||
|
$this->logger->warning('Server closed connection immediately', [
|
||||||
|
'elapsed_time' => $elapsedTime,
|
||||||
|
'bytes_received' => 0,
|
||||||
|
'read_attempts' => $readAttempts,
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
throw new ImmediateConnectionCloseException('Server closed connection immediately - server may be busy');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->logger->error('Stream read timeout detected', [
|
||||||
|
'elapsed_time' => $elapsedTime,
|
||||||
|
'bytes_received' => strlen($response),
|
||||||
|
'read_attempts' => $readAttempts,
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
throw new TimeoutException('Stream timeout while reading from socket');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response .= $chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === strlen($response)) {
|
||||||
|
$elapsedTime = microtime(true) - $this->operationStartTime;
|
||||||
|
$this->logger->warning('Server closed connection without sending data', [
|
||||||
|
'elapsed_time' => $elapsedTime,
|
||||||
|
'read_attempts' => $readAttempts,
|
||||||
|
'port' => $this->selectedPort,
|
||||||
|
]);
|
||||||
|
throw new ImmediateConnectionCloseException('Server closed connection without sending data');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param resource $socket */
|
||||||
|
private function disconnect($socket): void
|
||||||
|
{
|
||||||
|
@fclose($socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $options
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
private function resolveOptions(array $options): array
|
private function resolveOptions(array $options): array
|
||||||
{
|
{
|
||||||
$optionsResolver = new OptionsResolver();
|
$optionsResolver = new OptionsResolver();
|
||||||
@@ -642,14 +984,17 @@ class ApiClient
|
|||||||
'bpn_username',
|
'bpn_username',
|
||||||
'bpn_password',
|
'bpn_password',
|
||||||
'bpn_api_ip',
|
'bpn_api_ip',
|
||||||
'bpn_api_port',
|
'bpn_api_ports',
|
||||||
]);
|
]);
|
||||||
|
$optionsResolver->setAllowedTypes('bpn_api_ports', ['array']);
|
||||||
$optionsResolver->setDefaults([
|
$optionsResolver->setDefaults([
|
||||||
'max_retries' => 25,
|
'max_retries' => 25,
|
||||||
'debug' => false,
|
'debug' => false,
|
||||||
'connection_timeout' => 5,
|
'connection_timeout' => 5,
|
||||||
'stream_timeout' => 30,
|
'stream_timeout' => 30,
|
||||||
'total_timeout' => 45,
|
'total_timeout' => 45,
|
||||||
|
'busy_retry_attempts' => 3,
|
||||||
|
'busy_retry_delay' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $optionsResolver->resolve($options);
|
return $optionsResolver->resolve($options);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final class Constants
|
|||||||
public const TOKEN_SKI_PASS = 'SPA';
|
public const TOKEN_SKI_PASS = 'SPA';
|
||||||
public const TOKEN_ADDITIONAL = 'SON';
|
public const TOKEN_ADDITIONAL = 'SON';
|
||||||
public const TOKEN_BOARD = 'VPF';
|
public const TOKEN_BOARD = 'VPF';
|
||||||
|
public const TOKEN_VEG = 'VEG';
|
||||||
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
|
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
|
||||||
public const TOKEN_RENTAL_INSURANCE = 'LVS';
|
public const TOKEN_RENTAL_INSURANCE = 'LVS';
|
||||||
public const TOKEN_INSURANCES = ['RRV', 'PAK', 'OHN', 'PKG'];
|
public const TOKEN_INSURANCES = ['RRV', 'PAK', 'OHN', 'PKG'];
|
||||||
@@ -37,6 +38,7 @@ final class Constants
|
|||||||
self::TOKEN_SKI_PASS => 'Skipässe',
|
self::TOKEN_SKI_PASS => 'Skipässe',
|
||||||
self::TOKEN_ADDITIONAL => 'Zusatzleistungen',
|
self::TOKEN_ADDITIONAL => 'Zusatzleistungen',
|
||||||
self::TOKEN_BOARD => 'Verpflegung',
|
self::TOKEN_BOARD => 'Verpflegung',
|
||||||
|
self::TOKEN_VEG => 'Verpflegungswunsch',
|
||||||
self::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung',
|
self::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung',
|
||||||
self::TOKEN_PARKING => 'Parkplatz',
|
self::TOKEN_PARKING => 'Parkplatz',
|
||||||
self::GROUP_TRANSPORTATION => 'Beförderung',
|
self::GROUP_TRANSPORTATION => 'Beförderung',
|
||||||
@@ -55,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_OPEN = 'O'; // Optionsbuchung - not relevant for current implementation
|
public const BOOKING_STATUS_OPTION = 'O'; // Optionsbuchung
|
||||||
|
|
||||||
// Payment methods
|
// Payment methods
|
||||||
public const PAYMENT_METHOD_TRANSFER = 'transfer';
|
public const PAYMENT_METHOD_TRANSFER = 'transfer';
|
||||||
@@ -68,4 +70,7 @@ final class Constants
|
|||||||
// Baby room constraints
|
// Baby room constraints
|
||||||
public const BABY_MAX_AGE = 2;
|
public const BABY_MAX_AGE = 2;
|
||||||
public const BABY_ROOM_CODE = 'Baby';
|
public const BABY_ROOM_CODE = 'Baby';
|
||||||
|
|
||||||
|
// Pseudo price room - used for website display only, not bookable
|
||||||
|
public const PSEUDO_PRICE_ROOM_CODE = 'PDGS';
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,14 @@ 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\BookingPriceCalculatorService;
|
use App\Service\BookingPriceCalculator;
|
||||||
use App\Service\InsuranceService;
|
use App\Service\InsuranceManager;
|
||||||
|
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.
|
||||||
@@ -25,8 +27,8 @@ use App\Service\InsuranceService;
|
|||||||
class BookingDataProcessor
|
class BookingDataProcessor
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly InsuranceService $insuranceService,
|
private readonly InsuranceManager $insuranceService,
|
||||||
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
private readonly BookingPriceCalculator $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,
|
||||||
@@ -42,11 +44,12 @@ 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
|
||||||
* - First participant's address is NOT auto-populated from applicant
|
* - For normal bookings, participant[0]'s missing address/dimensions are filled from the
|
||||||
* - Template uses placeholders to show applicant's address as hints
|
* applicant (same person in normal bookings, but BPN may omit fields in <teilnehmer>)
|
||||||
* - This prevents address data from being inadvertently modified during form binding
|
* - For internal agency bookings, this copy is skipped: the applicant is a customer, not
|
||||||
|
* the first participant, so copying applicant data into participant[0] would be incorrect
|
||||||
*/
|
*/
|
||||||
public function createBookingDtoFromBooking(Booking $booking, Travel $travel): BookingDto
|
public function createBookingDtoFromBooking(Booking $booking, Travel $travel, bool $isInternalAgencyBooking = false): BookingDto
|
||||||
{
|
{
|
||||||
$dto = new BookingDto($travel, $booking->hotelId);
|
$dto = new BookingDto($travel, $booking->hotelId);
|
||||||
|
|
||||||
@@ -68,19 +71,18 @@ class BookingDataProcessor
|
|||||||
$participantData = ParticipantDto::fromPersonalData($participant);
|
$participantData = ParticipantDto::fromPersonalData($participant);
|
||||||
$participantData->index = $index;
|
$participantData->index = $index;
|
||||||
|
|
||||||
// First participant (applicant): copy data from applicant if participant data is empty
|
// For normal bookings, applicant === participant[0]: copy address and body dimensions
|
||||||
// BPN API may return full data only in <anmelder> but minimal/empty data in <teilnehmer id="1">
|
// from applicant when BPN returns full data only in <anmelder> and minimal data in
|
||||||
if (0 === $index) {
|
// <teilnehmer id="1">. Skipped for internal agency bookings where the applicant is
|
||||||
|
// 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)
|
||||||
// This allows applicant and first participant to be different people with different addresses
|
$isEmpty = null === $participantData->address
|
||||||
if (null !== $booking->applicant->address) {
|
|| null === $participantData->address->street
|
||||||
$isEmpty = null === $participantData->address
|
|| u($participantData->address->street)->trim()->isEmpty();
|
||||||
|| 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
|
||||||
@@ -107,6 +109,9 @@ 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);
|
||||||
|
|
||||||
@@ -124,9 +129,19 @@ class BookingDataProcessor
|
|||||||
// Pickup location
|
// Pickup location
|
||||||
$participantData->pickup = $booking->getPickupForParticipant($index);
|
$participantData->pickup = $booking->getPickupForParticipant($index);
|
||||||
|
|
||||||
// Parking (stored in form data, not in booking entity - needs special handling)
|
// Drop-off location
|
||||||
// For now, leave as false - may need to extract from transportation services
|
$participantData->dropOff = $booking->getDropOffForParticipant($index);
|
||||||
$participantData->parking = false;
|
$participantData->differentDropOff = null !== $participantData->dropOff;
|
||||||
|
|
||||||
|
// Parking service (for self-organized PKW transportation)
|
||||||
|
$parkingServices = $booking->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_PARKING);
|
||||||
|
if (false === empty($parkingServices)) {
|
||||||
|
$participantData->parking = true;
|
||||||
|
$participantData->parkingService = reset($parkingServices);
|
||||||
|
} else {
|
||||||
|
$participantData->parking = false;
|
||||||
|
$participantData->parkingService = null;
|
||||||
|
}
|
||||||
|
|
||||||
// License plate (not stored in booking entity)
|
// License plate (not stored in booking entity)
|
||||||
$participantData->licensePlate = null;
|
$participantData->licensePlate = null;
|
||||||
@@ -141,6 +156,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +174,8 @@ class BookingDataProcessor
|
|||||||
$roomSelection->label = $room->label;
|
$roomSelection->label = $room->label;
|
||||||
$roomSelection->price = $room->price;
|
$roomSelection->price = $room->price;
|
||||||
$roomSelection->quantity = $bookingRoom->totalCount; // Actual quantity from booking XML (anzahl)
|
$roomSelection->quantity = $bookingRoom->totalCount; // Actual quantity from booking XML (anzahl)
|
||||||
|
$roomSelection->capacity = $room->minPax ?? 1;
|
||||||
|
$roomSelection->maxQuantity = $bookingRoom->totalCount + ($room->available ?? 0);
|
||||||
$dto->roomSelections[] = $roomSelection;
|
$dto->roomSelections[] = $roomSelection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,6 +221,11 @@ 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])) {
|
||||||
@@ -213,6 +238,11 @@ class BookingDataProcessor
|
|||||||
$participant->rentalInsurance = $travel->additionalServices[$participant->rentalInsurance->id];
|
$participant->rentalInsurance = $travel->additionalServices[$participant->rentalInsurance->id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enrich parking service
|
||||||
|
if (null !== $participant->parkingService && isset($travel->additionalServices[$participant->parkingService->id])) {
|
||||||
|
$participant->parkingService = $travel->additionalServices[$participant->parkingService->id];
|
||||||
|
}
|
||||||
|
|
||||||
// Enrich transportation services
|
// Enrich transportation services
|
||||||
if (null !== $participant->transportationOutbound && isset($travel->transportationServices[$participant->transportationOutbound->id])) {
|
if (null !== $participant->transportationOutbound && isset($travel->transportationServices[$participant->transportationOutbound->id])) {
|
||||||
$participant->transportationOutbound = $travel->transportationServices[$participant->transportationOutbound->id];
|
$participant->transportationOutbound = $travel->transportationServices[$participant->transportationOutbound->id];
|
||||||
@@ -222,9 +252,33 @@ class BookingDataProcessor
|
|||||||
$participant->transportationInbound = $travel->transportationServices[$participant->transportationInbound->id];
|
$participant->transportationInbound = $travel->transportationServices[$participant->transportationInbound->id];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich pickup
|
// Enrich pickup with data from travel (includes both outbound and inbound prices)
|
||||||
if (null !== $participant->pickup && isset($travel->pickupsOutbound[$participant->pickup->id])) {
|
// The pickups already have inbound prices merged in from TravelParser
|
||||||
$participant->pickup = $travel->pickupsOutbound[$participant->pickup->id];
|
if (null !== $participant->pickup && isset($travel->pickups[$participant->pickup->id])) {
|
||||||
|
$enrichedPickup = $travel->pickups[$participant->pickup->id];
|
||||||
|
|
||||||
|
// Copy all properties from enriched pickup to participant's pickup
|
||||||
|
$participant->pickup->price = $enrichedPickup->price;
|
||||||
|
$participant->pickup->priceOutbound = $enrichedPickup->priceOutbound;
|
||||||
|
$participant->pickup->priceInbound = $enrichedPickup->priceInbound;
|
||||||
|
$participant->pickup->time = $enrichedPickup->time;
|
||||||
|
$participant->pickup->city = $enrichedPickup->city;
|
||||||
|
$participant->pickup->street = $enrichedPickup->street;
|
||||||
|
$participant->pickup->postalCode = $enrichedPickup->postalCode;
|
||||||
|
$participant->pickup->code = $enrichedPickup->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich drop-off with data from travel
|
||||||
|
if (null !== $participant->dropOff && isset($travel->dropOffs[$participant->dropOff->id])) {
|
||||||
|
$enrichedDropOff = $travel->dropOffs[$participant->dropOff->id];
|
||||||
|
$participant->dropOff->price = $enrichedDropOff->price;
|
||||||
|
$participant->dropOff->priceOutbound = $enrichedDropOff->priceOutbound;
|
||||||
|
$participant->dropOff->priceInbound = $enrichedDropOff->priceInbound;
|
||||||
|
$participant->dropOff->time = $enrichedDropOff->time;
|
||||||
|
$participant->dropOff->city = $enrichedDropOff->city;
|
||||||
|
$participant->dropOff->street = $enrichedDropOff->street;
|
||||||
|
$participant->dropOff->postalCode = $enrichedDropOff->postalCode;
|
||||||
|
$participant->dropOff->code = $enrichedDropOff->code;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich insurance
|
// Enrich insurance
|
||||||
@@ -243,7 +297,7 @@ 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 The structured payload array ready for BusProNet API submission
|
* @return array<string, mixed> The structured payload array ready for BusProNet API submission
|
||||||
*/
|
*/
|
||||||
public function createUpdateRequestPayload(?BookingDto $formData): array
|
public function createUpdateRequestPayload(?BookingDto $formData): array
|
||||||
{
|
{
|
||||||
@@ -266,13 +320,18 @@ class BookingDataProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->serviceProcessor->removeUnusedServices($bookingData);
|
$this->serviceProcessor->removeUnusedServices($bookingData);
|
||||||
$this->personalDataSynchronizer->updateParticipantPersonalData($formData->participants, $bookingData);
|
$this->personalDataSynchronizer->updateParticipantPersonalData(
|
||||||
|
$formData->participants,
|
||||||
|
$bookingData,
|
||||||
|
$formData->isInternalAgencyBooking()
|
||||||
|
);
|
||||||
|
|
||||||
$payload = $this->payloadBuilder->buildBasePayload($bookingData);
|
$payload = $this->payloadBuilder->buildBasePayload($bookingData);
|
||||||
$this->payloadBuilder->addBankAccountToPayload($payload, $bookingData);
|
$this->payloadBuilder->addBankAccountToPayload($payload, $bookingData);
|
||||||
$this->payloadBuilder->buildParticipantPayload($payload, $bookingData, $formData->participants);
|
$this->payloadBuilder->buildParticipantPayload($payload, $bookingData, $formData->participants);
|
||||||
$this->payloadBuilder->buildServicesPayload($payload, $bookingData);
|
$this->payloadBuilder->buildServicesPayload($payload, $bookingData);
|
||||||
$this->payloadBuilder->buildPickupPayload($payload, $bookingData);
|
$this->payloadBuilder->buildPickupPayload($payload, $bookingData);
|
||||||
|
$this->payloadBuilder->buildDropOffPayload($payload, $bookingData);
|
||||||
|
|
||||||
// Add purchase vouchers if any exist
|
// Add purchase vouchers if any exist
|
||||||
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($formData);
|
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($formData);
|
||||||
@@ -299,7 +358,7 @@ 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 The structured payload array for BusProNet API submission
|
* @return array<string, mixed> The structured payload array for BusProNet API submission
|
||||||
*/
|
*/
|
||||||
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
|
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
|
||||||
{
|
{
|
||||||
@@ -342,7 +401,7 @@ class BookingDataProcessor
|
|||||||
$participantPrices[$index] = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index);
|
$participantPrices[$index] = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use InsuranceService for proper type-based assignment with price tier matching
|
// Use InsuranceManager for proper type-based assignment with price tier matching
|
||||||
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
|
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
|
||||||
$selectableInsurances,
|
$selectableInsurances,
|
||||||
$applicant->insurance,
|
$applicant->insurance,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace App\BusProNet\DataProcessor;
|
|||||||
use App\BusProNet\Constants;
|
use App\BusProNet\Constants;
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
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.
|
||||||
@@ -16,6 +17,8 @@ use App\Form\Model\BookingDto;
|
|||||||
*/
|
*/
|
||||||
class BookingPayloadBuilder
|
class BookingPayloadBuilder
|
||||||
{
|
{
|
||||||
|
private const DEFAULT_PARTICIPANT_STATUS = 'F';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ServiceMappingCollector $mappingCollector,
|
private readonly ServiceMappingCollector $mappingCollector,
|
||||||
) {
|
) {
|
||||||
@@ -28,7 +31,7 @@ class BookingPayloadBuilder
|
|||||||
*
|
*
|
||||||
* @param Booking $bookingData The booking data object
|
* @param Booking $bookingData The booking data object
|
||||||
*
|
*
|
||||||
* @return array The base payload structure
|
* @return array<string, mixed> The base payload structure
|
||||||
*/
|
*/
|
||||||
public function buildBasePayload(Booking $bookingData): array
|
public function buildBasePayload(Booking $bookingData): array
|
||||||
{
|
{
|
||||||
@@ -64,8 +67,8 @@ class BookingPayloadBuilder
|
|||||||
*
|
*
|
||||||
* Bank account information is required for direct debit payments.
|
* Bank account information is required for direct debit payments.
|
||||||
*
|
*
|
||||||
* @param array $payload The payload array to modify
|
* @param array<string, mixed> $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
|
||||||
{
|
{
|
||||||
@@ -84,9 +87,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 $payload The payload array to modify
|
* @param array<string, mixed> $payload The payload array to modify
|
||||||
* @param Booking $bookingData The booking data object
|
* @param Booking $bookingData The booking data object
|
||||||
* @param array $participantDtos The participant DTOs from the form (for wishes data)
|
* @param array<int, ParticipantDto> $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
|
||||||
{
|
{
|
||||||
@@ -139,36 +142,39 @@ 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 $payload The payload array to modify
|
* @param array<string, mixed> $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
|
||||||
{
|
{
|
||||||
foreach ($bookingData->additionalServices as $service) {
|
foreach ($bookingData->additionalServices as $service) {
|
||||||
|
$uniqueMapping = array_unique($service->mapping);
|
||||||
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
||||||
'@idleistung' => $service->id,
|
'@idleistung' => $service->id,
|
||||||
'@anzahl' => count($service->mapping),
|
'@anzahl' => count($uniqueMapping),
|
||||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($bookingData->transportationServices as $service) {
|
foreach ($bookingData->transportationServices as $service) {
|
||||||
|
$uniqueMapping = array_unique($service->mapping);
|
||||||
$payload['beförderungen']['beförderung'][] = [
|
$payload['beförderungen']['beförderung'][] = [
|
||||||
'@idleistung' => $service->id,
|
'@idleistung' => $service->id,
|
||||||
'@anzahl' => count($service->mapping),
|
'@anzahl' => count($uniqueMapping),
|
||||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($bookingData->rooms as $room) {
|
foreach ($bookingData->rooms as $room) {
|
||||||
|
$uniqueMapping = array_unique($room->mapping);
|
||||||
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
||||||
'@idzimmer' => $room->id,
|
'@idzimmer' => $room->id,
|
||||||
'@kategorie' => $room->category,
|
'@kategorie' => $room->category,
|
||||||
'@idverpflegung' => $room->boardId,
|
'@idverpflegung' => $room->boardId,
|
||||||
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
'@anreise' => $room->dateFrom?->format('d.m.Y'),
|
||||||
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
|
'@abreise' => $room->dateTo?->format('d.m.Y'),
|
||||||
'@anzahl' => $room->totalCount,
|
'@anzahl' => $room->totalCount,
|
||||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $room->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +182,12 @@ class BookingPayloadBuilder
|
|||||||
if (false === empty($bookingData->insurances)) {
|
if (false === empty($bookingData->insurances)) {
|
||||||
$payload['versicherungen']['versicherung'] = [];
|
$payload['versicherungen']['versicherung'] = [];
|
||||||
foreach ($bookingData->insurances as $insurance) {
|
foreach ($bookingData->insurances as $insurance) {
|
||||||
if (count($insurance->mapping) > 0) {
|
$uniqueMapping = array_unique($insurance->mapping);
|
||||||
|
if (count($uniqueMapping) > 0) {
|
||||||
$payload['versicherungen']['versicherung'][] = [
|
$payload['versicherungen']['versicherung'][] = [
|
||||||
'@idversicherung' => $insurance->id,
|
'@idversicherung' => $insurance->id,
|
||||||
'@anzahl' => count($insurance->mapping),
|
'@anzahl' => count($uniqueMapping),
|
||||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $insurance->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,18 +199,44 @@ 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 $payload The payload array to modify
|
* @param array<string, mixed> $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
|
||||||
{
|
{
|
||||||
if (0 < count($bookingData->pickupsOutbound)) {
|
$payload['zustiege'] = [];
|
||||||
|
|
||||||
|
if (0 < count($bookingData->pickups)) {
|
||||||
$payload['zustiege']['zustieg'] = [];
|
$payload['zustiege']['zustieg'] = [];
|
||||||
foreach ($bookingData->pickupsOutbound as $pickup) {
|
foreach ($bookingData->pickups as $pickup) {
|
||||||
|
$uniqueMapping = array_unique($pickup->mapping);
|
||||||
$payload['zustiege']['zustieg'][] = [
|
$payload['zustiege']['zustieg'][] = [
|
||||||
'@idzustieg' => $pickup->id,
|
'@idzustieg' => $pickup->id,
|
||||||
'@anzahl' => count($pickup->mapping),
|
'@anzahl' => count($uniqueMapping),
|
||||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $pickup->mapping)),
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the drop-off locations section of the payload.
|
||||||
|
*
|
||||||
|
* Only included in payload if there are actual drop-off assignments for bus transportation.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $payload The payload array to modify
|
||||||
|
* @param Booking $bookingData The booking data object
|
||||||
|
*/
|
||||||
|
public function buildDropOffPayload(array &$payload, Booking $bookingData): void
|
||||||
|
{
|
||||||
|
if (0 < count($bookingData->dropOffs)) {
|
||||||
|
$payload['ausstiege']['ausstieg'] = [];
|
||||||
|
foreach ($bookingData->dropOffs as $dropOff) {
|
||||||
|
$uniqueMapping = array_unique($dropOff->mapping);
|
||||||
|
$payload['ausstiege']['ausstieg'][] = [
|
||||||
|
'@idzustieg' => $dropOff->id,
|
||||||
|
'@anzahl' => count($uniqueMapping),
|
||||||
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +250,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 The structured payload array for BusProNet API submission
|
* @return array<string, mixed> The structured payload array for BusProNet API submission
|
||||||
*/
|
*/
|
||||||
public function buildCreatePayload(BookingDto $bookingDto, string $bookingType): array
|
public function buildCreatePayload(BookingDto $bookingDto, string $bookingType): array
|
||||||
{
|
{
|
||||||
@@ -270,6 +303,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,
|
||||||
'name' => $participant->lastName,
|
'name' => $participant->lastName,
|
||||||
'vorname' => $participant->firstName,
|
'vorname' => $participant->firstName,
|
||||||
'geschlecht' => $participant->gender ?? '',
|
'geschlecht' => $participant->gender ?? '',
|
||||||
@@ -359,6 +393,13 @@ class BookingPayloadBuilder
|
|||||||
$this->addRoomMappingsToPayload($payload, $roomMap, $bookingDto);
|
$this->addRoomMappingsToPayload($payload, $roomMap, $bookingDto);
|
||||||
$this->addServicesFromMap($payload, 'zusatzleistungen', 'zusatzleistung', '@idleistung', $serviceMap);
|
$this->addServicesFromMap($payload, 'zusatzleistungen', 'zusatzleistung', '@idleistung', $serviceMap);
|
||||||
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
|
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
|
||||||
|
if (false === array_key_exists('zustiege', $payload)) {
|
||||||
|
$payload['zustiege'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dropOffMap = $this->mappingCollector->collectDropOffMappings($bookingDto);
|
||||||
|
$this->addServicesFromMap($payload, 'ausstiege', 'ausstieg', '@idzustieg', $dropOffMap);
|
||||||
|
|
||||||
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
|
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
|
||||||
|
|
||||||
// Add purchase vouchers (regular purchase vouchers, excluding goodwill vouchers)
|
// Add purchase vouchers (regular purchase vouchers, excluding goodwill vouchers)
|
||||||
@@ -399,11 +440,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 $payload The payload array to modify
|
* @param array<string, mixed> $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 $serviceMap Map of service ID to participant IDs
|
* @param array<int|string, int[]> $serviceMap Map of service ID to participant IDs
|
||||||
*/
|
*/
|
||||||
public function addServicesFromMap(
|
public function addServicesFromMap(
|
||||||
array &$payload,
|
array &$payload,
|
||||||
@@ -415,10 +456,11 @@ class BookingPayloadBuilder
|
|||||||
if (false === empty($serviceMap)) {
|
if (false === empty($serviceMap)) {
|
||||||
$payload[$sectionKey][$itemKey] = [];
|
$payload[$sectionKey][$itemKey] = [];
|
||||||
foreach ($serviceMap as $serviceId => $participantIds) {
|
foreach ($serviceMap as $serviceId => $participantIds) {
|
||||||
|
$uniqueParticipantIds = array_unique($participantIds);
|
||||||
$payload[$sectionKey][$itemKey][] = [
|
$payload[$sectionKey][$itemKey][] = [
|
||||||
$idAttributeName => $serviceId,
|
$idAttributeName => $serviceId,
|
||||||
'@anzahl' => count($participantIds),
|
'@anzahl' => count($uniqueParticipantIds),
|
||||||
'@zuordnung' => implode(',', $participantIds),
|
'@zuordnung' => implode(',', $uniqueParticipantIds),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,9 +476,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 $payload The payload array to modify
|
* @param array<string, mixed> $payload The payload array to modify
|
||||||
* @param array $roomMap Map of room ID to participant IDs
|
* @param array<int|string, int[]> $roomMap Map of room ID to participant IDs
|
||||||
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
|
* @param 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
|
||||||
{
|
{
|
||||||
@@ -462,6 +504,7 @@ class BookingPayloadBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$quantity = $roomQuantities[$roomId] ?? 1;
|
$quantity = $roomQuantities[$roomId] ?? 1;
|
||||||
|
$uniqueParticipantIds = array_unique($participantIds);
|
||||||
|
|
||||||
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
||||||
'@idzimmer' => $room->id,
|
'@idzimmer' => $room->id,
|
||||||
@@ -470,7 +513,7 @@ class BookingPayloadBuilder
|
|||||||
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
|
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
|
||||||
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
|
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
|
||||||
'@anzahl' => $quantity,
|
'@anzahl' => $quantity,
|
||||||
'@zuordnung' => implode(',', $participantIds),
|
'@zuordnung' => implode(',', $uniqueParticipantIds),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,8 +521,8 @@ class BookingPayloadBuilder
|
|||||||
/**
|
/**
|
||||||
* Adds purchase vouchers to the payload.
|
* Adds purchase vouchers to the payload.
|
||||||
*
|
*
|
||||||
* @param array $payload The payload array to modify
|
* @param array<string, mixed> $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,8 +5,10 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes participant service selections for booking updates.
|
* Processes participant service selections for booking updates.
|
||||||
@@ -17,6 +19,11 @@ use App\Form\Model\ParticipantDto;
|
|||||||
*/
|
*/
|
||||||
class ParticipantServiceProcessor
|
class ParticipantServiceProcessor
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets all existing participant-to-service mappings to start with a clean slate.
|
* Resets all existing participant-to-service mappings to start with a clean slate.
|
||||||
*
|
*
|
||||||
@@ -30,8 +37,8 @@ class ParticipantServiceProcessor
|
|||||||
$servicesToReset = [
|
$servicesToReset = [
|
||||||
...$bookingData->additionalServices,
|
...$bookingData->additionalServices,
|
||||||
...$bookingData->transportationServices,
|
...$bookingData->transportationServices,
|
||||||
...$bookingData->pickupsOutbound,
|
...$bookingData->pickups,
|
||||||
...$bookingData->pickupsInbound,
|
...$bookingData->dropOffs,
|
||||||
...$bookingData->rooms,
|
...$bookingData->rooms,
|
||||||
...$bookingData->insurances,
|
...$bookingData->insurances,
|
||||||
];
|
];
|
||||||
@@ -60,6 +67,7 @@ class ParticipantServiceProcessor
|
|||||||
$this->processAdditionalServices($participant, $bookingData, $travelData);
|
$this->processAdditionalServices($participant, $bookingData, $travelData);
|
||||||
$this->processTransportationServices($participant, $bookingData, $travelData);
|
$this->processTransportationServices($participant, $bookingData, $travelData);
|
||||||
$this->processPickupLocations($participant, $bookingData);
|
$this->processPickupLocations($participant, $bookingData);
|
||||||
|
$this->processDropOffLocations($participant, $bookingData);
|
||||||
$this->processRoomAssignment($participant, $bookingData);
|
$this->processRoomAssignment($participant, $bookingData);
|
||||||
$this->processInsurance($participant, $bookingData, $travelData);
|
$this->processInsurance($participant, $bookingData, $travelData);
|
||||||
}
|
}
|
||||||
@@ -86,15 +94,15 @@ class ParticipantServiceProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($bookingData->pickupsOutbound as $pickup) {
|
foreach ($bookingData->pickups as $pickup) {
|
||||||
if (0 === count($pickup->mapping)) {
|
if (0 === count($pickup->mapping)) {
|
||||||
unset($bookingData->pickupsOutbound[$pickup->id]);
|
unset($bookingData->pickups[$pickup->id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($bookingData->pickupsInbound as $pickup) {
|
foreach ($bookingData->dropOffs as $dropOff) {
|
||||||
if (0 === count($pickup->mapping)) {
|
if (0 === count($dropOff->mapping)) {
|
||||||
unset($bookingData->pickupsInbound[$pickup->id]);
|
unset($bookingData->dropOffs[$dropOff->id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +117,11 @@ class ParticipantServiceProcessor
|
|||||||
* Collects additional services from a participant for mapping.
|
* Collects additional services from a participant for mapping.
|
||||||
*
|
*
|
||||||
* Extracts all additional services (courses, board, rentals, ski pass, rental insurance,
|
* Extracts all additional services (courses, board, rentals, ski pass, rental insurance,
|
||||||
* additional services) from a participant into a flat array of Service objects.
|
* parking, additional services) from a participant into a flat array of Service objects.
|
||||||
*
|
*
|
||||||
* @param ParticipantDto $participant The participant data
|
* @param ParticipantDto $participant The participant data
|
||||||
*
|
*
|
||||||
* @return array<\App\BusProNet\Model\Service> Array of services selected by this participant
|
* @return array<Service> Array of services selected by this participant
|
||||||
*/
|
*/
|
||||||
private function collectParticipantAdditionalServices(ParticipantDto $participant): array
|
private function collectParticipantAdditionalServices(ParticipantDto $participant): array
|
||||||
{
|
{
|
||||||
@@ -124,6 +132,11 @@ 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;
|
||||||
@@ -134,6 +147,11 @@ class ParticipantServiceProcessor
|
|||||||
$services[] = $participant->rentalInsurance;
|
$services[] = $participant->rentalInsurance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add parking service if selected (for self-organized PKW transportation)
|
||||||
|
if (true === $participant->parking && null !== $participant->parkingService) {
|
||||||
|
$services[] = $participant->parkingService;
|
||||||
|
}
|
||||||
|
|
||||||
return $services;
|
return $services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,11 +172,18 @@ class ParticipantServiceProcessor
|
|||||||
foreach ($servicesToMap as $service) {
|
foreach ($servicesToMap as $service) {
|
||||||
if (false === isset($bookingData->additionalServices[$service->id])) {
|
if (false === isset($bookingData->additionalServices[$service->id])) {
|
||||||
$serviceToAdd = $travelData->additionalServices[$service->id] ?? null;
|
$serviceToAdd = $travelData->additionalServices[$service->id] ?? null;
|
||||||
if (null !== $serviceToAdd) {
|
if (null === $serviceToAdd) {
|
||||||
$bookingData->additionalServices[$service->id] = $serviceToAdd;
|
$this->logger->warning('Additional service not found in travel data, skipping mapping', [
|
||||||
$bookingData->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
'bookingId' => $bookingData->id,
|
||||||
|
'serviceId' => $service->id,
|
||||||
|
'participantIndex' => $participant->index,
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
$bookingData->additionalServices[$service->id] = $serviceToAdd;
|
||||||
|
$bookingData->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookingData->additionalServices[$service->id]->mapping[] = $participant->index;
|
$bookingData->additionalServices[$service->id]->mapping[] = $participant->index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,13 +201,24 @@ class ParticipantServiceProcessor
|
|||||||
private function processTransportationServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
private function processTransportationServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
||||||
{
|
{
|
||||||
foreach ([$participant->transportationOutbound, $participant->transportationInbound] as $service) {
|
foreach ([$participant->transportationOutbound, $participant->transportationInbound] as $service) {
|
||||||
|
if (null === $service) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (false === isset($bookingData->transportationServices[$service->id])) {
|
if (false === isset($bookingData->transportationServices[$service->id])) {
|
||||||
$serviceToAdd = $travelData->transportationServices[$service->id] ?? null;
|
$serviceToAdd = $travelData->transportationServices[$service->id] ?? null;
|
||||||
if (null !== $serviceToAdd) {
|
if (null === $serviceToAdd) {
|
||||||
$bookingData->transportationServices[$service->id] = $serviceToAdd;
|
$this->logger->warning('Transportation service not found in travel data, skipping mapping', [
|
||||||
$bookingData->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
'bookingId' => $bookingData->id,
|
||||||
|
'serviceId' => $service->id,
|
||||||
|
'participantIndex' => $participant->index,
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
$bookingData->transportationServices[$service->id] = $serviceToAdd;
|
||||||
|
$bookingData->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookingData->transportationServices[$service->id]->mapping[] = $participant->index;
|
$bookingData->transportationServices[$service->id]->mapping[] = $participant->index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,10 +239,34 @@ class ParticipantServiceProcessor
|
|||||||
$hasInboundBus = null !== $participant->transportationInbound && 'BUS' === $participant->transportationInbound->subType;
|
$hasInboundBus = null !== $participant->transportationInbound && 'BUS' === $participant->transportationInbound->subType;
|
||||||
|
|
||||||
if (($hasOutboundBus || $hasInboundBus) && null !== $selectedPickup = $participant->pickup) {
|
if (($hasOutboundBus || $hasInboundBus) && null !== $selectedPickup = $participant->pickup) {
|
||||||
if (false === isset($bookingData->pickupsOutbound[$selectedPickup->id])) {
|
if (false === isset($bookingData->pickups[$selectedPickup->id])) {
|
||||||
$bookingData->pickupsOutbound[$selectedPickup->id] = $selectedPickup;
|
$bookingData->pickups[$selectedPickup->id] = $selectedPickup;
|
||||||
}
|
}
|
||||||
$bookingData->pickupsOutbound[$selectedPickup->id]->mapping[] = $participant->index;
|
$bookingData->pickups[$selectedPickup->id]->mapping[] = $participant->index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes drop-off locations for participants using bus transportation.
|
||||||
|
*
|
||||||
|
* Only processes drop-off locations for bus transportation services and maps the participant
|
||||||
|
* to their selected drop-off location.
|
||||||
|
*
|
||||||
|
* @param ParticipantDto $participant The participant data from the form
|
||||||
|
* @param Booking $bookingData The booking data object to update
|
||||||
|
*/
|
||||||
|
private function processDropOffLocations(ParticipantDto $participant, Booking $bookingData): void
|
||||||
|
{
|
||||||
|
$hasOutboundBus = null !== $participant->transportationOutbound
|
||||||
|
&& 'BUS' === $participant->transportationOutbound->subType;
|
||||||
|
$hasInboundBus = null !== $participant->transportationInbound
|
||||||
|
&& 'BUS' === $participant->transportationInbound->subType;
|
||||||
|
|
||||||
|
if (($hasOutboundBus || $hasInboundBus) && null !== $selectedDropOff = $participant->dropOff) {
|
||||||
|
if (false === isset($bookingData->dropOffs[$selectedDropOff->id])) {
|
||||||
|
$bookingData->dropOffs[$selectedDropOff->id] = $selectedDropOff;
|
||||||
|
}
|
||||||
|
$bookingData->dropOffs[$selectedDropOff->id]->mapping[] = $participant->index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +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\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,28 +20,53 @@ class PersonalDataSynchronizer
|
|||||||
/**
|
/**
|
||||||
* Updates participant personal data from form input.
|
* Updates participant personal data from form input.
|
||||||
*
|
*
|
||||||
* Processes all active participants (status 'F' or 'A'). Skips canceled participants (status 'S').
|
* Processes all participants regardless of status, including canceled participants (status 'S').
|
||||||
* Updates all personal data fields and communication information.
|
* Updates all personal data fields and communication information.
|
||||||
*
|
*
|
||||||
|
* Also fills missing mandatory fields on the applicant object for company bookings
|
||||||
|
* created by travel agencies. The applicant is separate from participant 0 and may
|
||||||
|
* contain only company data without personal fields required by the BPN API.
|
||||||
|
*
|
||||||
* IMPORTANT: The applicant's address must never be modified. This method updates
|
* IMPORTANT: The applicant's address must never be modified. This method updates
|
||||||
* participant addresses independently to ensure applicant data remains intact.
|
* participant addresses independently to ensure applicant data remains intact.
|
||||||
*
|
*
|
||||||
* @param array<ParticipantDto> $participants The participants array from the form
|
* @param array<ParticipantDto> $participants The participants array from the form
|
||||||
* @param Booking $bookingData The booking data object to update
|
* @param Booking $bookingData The booking data object to update
|
||||||
|
* @param bool $isInternalAgencyBooking Whether to fill default names for participants with empty names
|
||||||
*/
|
*/
|
||||||
public function updateParticipantPersonalData(array $participants, Booking $bookingData): void
|
public function updateParticipantPersonalData(
|
||||||
{
|
array $participants,
|
||||||
foreach ($participants as $participant) {
|
Booking $bookingData,
|
||||||
// Skip canceled participants (status 'S')
|
bool $isInternalAgencyBooking = false,
|
||||||
if ('S' === $participant->status) {
|
): void {
|
||||||
continue;
|
// Fill missing mandatory fields on applicant (for company bookings by travel agencies)
|
||||||
}
|
// This is separate from participants - applicant may be a company while participants are real people
|
||||||
|
$this->fillMissingApplicantFields($bookingData);
|
||||||
|
|
||||||
|
foreach ($participants as $participant) {
|
||||||
$bookingData->participants[$participant->index]->firstName = $participant->firstName;
|
$bookingData->participants[$participant->index]->firstName = $participant->firstName;
|
||||||
$bookingData->participants[$participant->index]->name = $participant->lastName;
|
$bookingData->participants[$participant->index]->name = $participant->lastName;
|
||||||
|
|
||||||
|
// For internal agency bookings, participants with empty names get default values.
|
||||||
|
// Agencies pre-book slots with placeholder data (e.g. applicant surname, empty first name),
|
||||||
|
// and the BPN API rejects updates with empty name fields.
|
||||||
|
if ($isInternalAgencyBooking) {
|
||||||
|
$defaultName = sprintf('Teilnehmer:in %d', $participant->index + 1);
|
||||||
|
$personalData = $bookingData->participants[$participant->index];
|
||||||
|
|
||||||
|
if (null === $personalData->firstName || '' === $personalData->firstName) {
|
||||||
|
$personalData->firstName = $defaultName;
|
||||||
|
}
|
||||||
|
if (null === $personalData->name || '' === $personalData->name) {
|
||||||
|
$personalData->name = $defaultName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$bookingData->participants[$participant->index]->dateOfBirth = $participant->dateOfBirth;
|
$bookingData->participants[$participant->index]->dateOfBirth = $participant->dateOfBirth;
|
||||||
$bookingData->participants[$participant->index]->gender = $participant->gender;
|
$bookingData->participants[$participant->index]->gender = $participant->gender;
|
||||||
$bookingData->participants[$participant->index]->nationality = $participant->nationality;
|
$bookingData->participants[$participant->index]->nationality = $participant->nationality;
|
||||||
|
|
||||||
|
$this->fillMissingParticipantFields($bookingData->participants[$participant->index]);
|
||||||
$bookingData->participants[$participant->index]->height = $participant->height;
|
$bookingData->participants[$participant->index]->height = $participant->height;
|
||||||
$bookingData->participants[$participant->index]->weight = $participant->weight;
|
$bookingData->participants[$participant->index]->weight = $participant->weight;
|
||||||
$bookingData->participants[$participant->index]->shoeSize = $participant->shoeSize;
|
$bookingData->participants[$participant->index]->shoeSize = $participant->shoeSize;
|
||||||
@@ -60,12 +85,61 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills missing mandatory fields on the applicant for company bookings.
|
||||||
|
*
|
||||||
|
* Travel agencies often create bookings with company data as the applicant,
|
||||||
|
* which lacks personal data fields. These fields are mandatory for the BPN
|
||||||
|
* API, so we fill them with sensible defaults. The applicant is separate from
|
||||||
|
* participants - participant 0 is a real person and must not be modified here.
|
||||||
|
*/
|
||||||
|
private function fillMissingApplicantFields(Booking $bookingData): void
|
||||||
|
{
|
||||||
|
if (null === $bookingData->applicant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$applicant = $bookingData->applicant;
|
||||||
|
|
||||||
|
if (null === $applicant->firstName || '' === $applicant->firstName) {
|
||||||
|
$applicant->firstName = 'Anmelder';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $applicant->gender || '' === $applicant->gender) {
|
||||||
|
$applicant->gender = 'D';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $applicant->nationality || '' === $applicant->nationality) {
|
||||||
|
$applicant->nationality = 'D';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $applicant->dateOfBirth) {
|
||||||
|
$applicant->dateOfBirth = new \DateTimeImmutable('-20 years');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $applicant->communication->mobile || '' === $applicant->communication->mobile) {
|
||||||
|
$applicant->communication->mobile = '12345';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills missing mandatory fields on a participant.
|
||||||
|
*
|
||||||
|
* Ensures mandatory fields have sensible defaults before submission to the
|
||||||
|
* BPN API. Currently handles nationality which defaults to 'D' (German).
|
||||||
|
*
|
||||||
|
* @param PersonalData $participant The participant to fill defaults for
|
||||||
|
*/
|
||||||
|
private function fillMissingParticipantFields(PersonalData $participant): void
|
||||||
|
{
|
||||||
|
if (null === $participant->nationality || '' === $participant->nationality) {
|
||||||
|
$participant->nationality = 'D';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ class PickupPlanningTransformer
|
|||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>> $payload
|
* @param array<int, array<string, mixed>> $payload
|
||||||
*
|
*
|
||||||
* @return array<string, array<int, array<string, string>>>
|
* @return array<string, array<int, array{city: string, location: string, datetime: string, busNumber: string}>>
|
||||||
*/
|
*/
|
||||||
public function transform(array $payload): array
|
public function transform(array $payload): array
|
||||||
{
|
{
|
||||||
@@ -27,26 +27,25 @@ class PickupPlanningTransformer
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$travelCodes = $item['Reisecodes'] ?? [];
|
$code = $item['Reisecode'] ?? '';
|
||||||
|
|
||||||
if (0 === count($travelCodes)) {
|
if ('' === $code) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dateTime = \DateTimeImmutable::createFromFormat('d.m.Y H:i', $item['Zeit'] ?? '');
|
$dateTime = \DateTimeImmutable::createFromFormat('d.m.Y H:i', $item['Zeit'] ?? '');
|
||||||
|
|
||||||
foreach ($travelCodes as $code) {
|
$pickup = [
|
||||||
$pickup = [
|
'city' => $item['Zustieg/Ausstieg Ort'] ?? '',
|
||||||
'city' => $item['Zustieg/Ausstieg Ort'] ?? '',
|
'location' => $item['Zustieg/Ausstieg Strasse'] ?? '',
|
||||||
'location' => $item['Zustieg/Ausstieg Strasse'] ?? '',
|
'datetime' => false !== $dateTime ? $dateTime->format('Y-m-d H:i') : '',
|
||||||
'datetime' => false !== $dateTime ? $dateTime->format('Y-m-d H:i') : '',
|
'busNumber' => $item['BusNummer'] ?? '',
|
||||||
];
|
];
|
||||||
|
|
||||||
$key = sprintf('%s|%s|%s', $pickup['city'], $pickup['location'], $pickup['datetime']);
|
$key = sprintf('%s|%s|%s|%s', $pickup['city'], $pickup['location'], $pickup['datetime'], $pickup['busNumber']);
|
||||||
|
|
||||||
if (false === isset($result[$code][$key])) {
|
if (false === isset($result[$code][$key])) {
|
||||||
$result[$code][$key] = $pickup;
|
$result[$code][$key] = $pickup;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class ServiceMappingCollector
|
|||||||
*
|
*
|
||||||
* Groups participants by their assigned room ID.
|
* Groups participants by their assigned room ID.
|
||||||
*
|
*
|
||||||
* @return array<string, array<int>> Map of room ID to participant IDs
|
* @return array<int, list<int>> Map of room ID to participant IDs
|
||||||
*/
|
*/
|
||||||
public function collectRoomMappings(BookingDto $bookingDto): array
|
public function collectRoomMappings(BookingDto $bookingDto): array
|
||||||
{
|
{
|
||||||
@@ -57,6 +57,11 @@ class ServiceMappingCollector
|
|||||||
$serviceMap[$board->id][] = $participantId;
|
$serviceMap[$board->id][] = $participantId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Veg (vegetarian/vegan) preference
|
||||||
|
if (null !== $participant->veg) {
|
||||||
|
$serviceMap[$participant->veg->id][] = $participantId;
|
||||||
|
}
|
||||||
|
|
||||||
// Ski pass
|
// Ski pass
|
||||||
if (null !== $participant->skiPass) {
|
if (null !== $participant->skiPass) {
|
||||||
$serviceMap[$participant->skiPass->id][] = $participantId;
|
$serviceMap[$participant->skiPass->id][] = $participantId;
|
||||||
@@ -138,6 +143,28 @@ class ServiceMappingCollector
|
|||||||
return $pickupMap;
|
return $pickupMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects drop-off location mappings.
|
||||||
|
*
|
||||||
|
* Collects the drop-off selection for the inbound/return direction.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int>> Map of drop-off ID to participant IDs
|
||||||
|
*/
|
||||||
|
public function collectDropOffMappings(BookingDto $bookingDto): array
|
||||||
|
{
|
||||||
|
$dropOffMap = [];
|
||||||
|
|
||||||
|
foreach ($bookingDto->participants as $index => $participant) {
|
||||||
|
$participantId = $index + 1;
|
||||||
|
|
||||||
|
if (null !== $participant->dropOff) {
|
||||||
|
$dropOffMap[$participant->dropOff->id][] = $participantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dropOffMap;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collects insurance mappings.
|
* Collects insurance mappings.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class CountryDataProvider
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, Country> */
|
||||||
public function getAll(): array
|
public function getAll(): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BusProNet\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when the BPN server closes the connection immediately without sending data.
|
||||||
|
*
|
||||||
|
* This typically indicates the server is busy processing other requests.
|
||||||
|
* Unlike a true timeout, this is a transient condition that may succeed on retry.
|
||||||
|
*/
|
||||||
|
class ImmediateConnectionCloseException extends ApiClientException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ 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)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\BusProNet\Model;
|
namespace App\BusProNet\Model;
|
||||||
|
|
||||||
|
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.
|
||||||
*
|
*
|
||||||
@@ -13,16 +16,35 @@ namespace App\BusProNet\Model;
|
|||||||
*/
|
*/
|
||||||
class Address
|
class Address
|
||||||
{
|
{
|
||||||
|
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||||
public ?string $street = null;
|
public ?string $street = null;
|
||||||
|
|
||||||
|
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||||
public ?string $postCode = null;
|
public ?string $postCode = null;
|
||||||
|
|
||||||
|
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||||
public ?string $city = null;
|
public ?string $city = null;
|
||||||
|
|
||||||
public ?string $district = null;
|
public ?string $district = null;
|
||||||
|
|
||||||
|
#[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.
|
||||||
*
|
*
|
||||||
@@ -41,4 +63,15 @@ 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,6 +12,7 @@ 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,6 +12,7 @@ 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)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -19,7 +20,7 @@ class BaseData
|
|||||||
/**
|
/**
|
||||||
* Retrieves all items in the base data collection.
|
* Retrieves all items in the base data collection.
|
||||||
*
|
*
|
||||||
* @return array The complete items array
|
* @return array<array-key, mixed> The complete items array
|
||||||
*/
|
*/
|
||||||
public function getItems(): array
|
public function getItems(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ 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;
|
||||||
@@ -36,14 +38,50 @@ 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 = [];
|
||||||
public array $pickupsOutbound = [];
|
|
||||||
public array $pickupsInbound = [];
|
/**
|
||||||
|
* @var array<int, Pickup>
|
||||||
|
*/
|
||||||
|
public array $pickups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int, Pickup>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
@@ -136,7 +174,7 @@ class Booking
|
|||||||
* Retrieves pickup service for a specific participant.
|
* Retrieves pickup service for a specific participant.
|
||||||
*
|
*
|
||||||
* Finds the pickup service assigned to the specified participant
|
* Finds the pickup service assigned to the specified participant
|
||||||
* from the outbound pickup services.
|
* from the pickup services.
|
||||||
*
|
*
|
||||||
* @param int $participantIndex The participant index to search for
|
* @param int $participantIndex The participant index to search for
|
||||||
*
|
*
|
||||||
@@ -144,7 +182,7 @@ class Booking
|
|||||||
*/
|
*/
|
||||||
public function getPickupForParticipant(int $participantIndex): ?Pickup
|
public function getPickupForParticipant(int $participantIndex): ?Pickup
|
||||||
{
|
{
|
||||||
foreach ($this->pickupsOutbound as $pickup) {
|
foreach ($this->pickups as $pickup) {
|
||||||
if (in_array($participantIndex, $pickup->mapping)) {
|
if (in_array($participantIndex, $pickup->mapping)) {
|
||||||
return $pickup;
|
return $pickup;
|
||||||
}
|
}
|
||||||
@@ -153,6 +191,27 @@ class Booking
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves drop-off service for a specific participant.
|
||||||
|
*
|
||||||
|
* Finds the drop-off service assigned to the specified participant
|
||||||
|
* from the drop-off services (inbound/return direction).
|
||||||
|
*
|
||||||
|
* @param int $participantIndex The participant index to search for
|
||||||
|
*
|
||||||
|
* @return Pickup|null The matching drop-off service or null if not found
|
||||||
|
*/
|
||||||
|
public function getDropOffForParticipant(int $participantIndex): ?Pickup
|
||||||
|
{
|
||||||
|
foreach ($this->dropOffs as $dropOff) {
|
||||||
|
if (in_array($participantIndex, $dropOff->mapping)) {
|
||||||
|
return $dropOff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the insurance assigned to a specific participant.
|
* Gets the insurance assigned to a specific participant.
|
||||||
*
|
*
|
||||||
@@ -163,7 +222,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,16 +15,20 @@ 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 string|null $transactionNumber Transaction number (vorgang)
|
* @param int|null $bookingNumber Booking number (BPN XML: vorgang)
|
||||||
* @param array<int, PriceItem> $priceItems Individual price items from response
|
* @param array<int, PriceItem> $priceItems Individual price items from response
|
||||||
* @param float|null $totalPrice Total price (gesamtpreis)
|
* @param float|null $totalPrice Total price (gesamtpreis)
|
||||||
* @param PaymentTerms|null $paymentTerms Payment terms (anzahlung/restzahlung)
|
* @param PaymentTerms|null $paymentTerms Payment terms (anzahlung/restzahlung)
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $status,
|
public readonly string $status,
|
||||||
public readonly ?string $transactionNumber = null,
|
public readonly ?int $bookingNumber = null,
|
||||||
public readonly array $priceItems = [],
|
public readonly array $priceItems = [],
|
||||||
public readonly ?float $totalPrice = null,
|
public readonly ?float $totalPrice = null,
|
||||||
public readonly ?PaymentTerms $paymentTerms = null,
|
public readonly ?PaymentTerms $paymentTerms = null,
|
||||||
@@ -37,7 +41,7 @@ class BookingResponse
|
|||||||
*/
|
*/
|
||||||
public function isInquiryValid(): bool
|
public function isInquiryValid(): bool
|
||||||
{
|
{
|
||||||
return 'möglich' === $this->status;
|
return self::BOOKING_POSSIBLE === $this->status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +49,7 @@ class BookingResponse
|
|||||||
*/
|
*/
|
||||||
public function isBookingSuccessful(): bool
|
public function isBookingSuccessful(): bool
|
||||||
{
|
{
|
||||||
return 'erfolgt' === $this->status;
|
return self::BOOKING_PROCESSED === $this->status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,4 +106,48 @@ class BookingResponse
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets API-applied discounts that cannot be predicted by the local price calculator.
|
||||||
|
*
|
||||||
|
* The BusProNet API may apply automatic discounts based on business rules that
|
||||||
|
* are not known to the local application, such as:
|
||||||
|
* - ERM/GRU: Gruppenrabatt (group discount for large bookings)
|
||||||
|
* - Other ERM subtypes: Various automatic discounts
|
||||||
|
*
|
||||||
|
* These discounts appear as negative price items with type "ERM" and must be
|
||||||
|
* accounted for when comparing the API total price against the locally calculated price.
|
||||||
|
*
|
||||||
|
* Important: Only ERM type discounts are considered API-applied. Other negative price
|
||||||
|
* items (like BEF transportation discounts) are already handled by the local price
|
||||||
|
* calculator and should not be subtracted again.
|
||||||
|
*
|
||||||
|
* Note: Voucher discounts (AKTION, KULANZGUTSCHEIN) are handled separately via
|
||||||
|
* getVoucherDiscountFromPrices() as they are user-initiated, not automatic.
|
||||||
|
*
|
||||||
|
* @return array<PriceItem> Price items representing API-applied automatic discounts
|
||||||
|
*/
|
||||||
|
public function getApiAppliedDiscounts(): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$this->priceItems,
|
||||||
|
fn (PriceItem $item) => $item->totalPrice < 0 && 'ERM' === $item->type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets total discount from API-applied automatic discounts.
|
||||||
|
*
|
||||||
|
* Returns the absolute sum of negative price items that represent automatic
|
||||||
|
* discounts applied by the API (excluding voucher discounts which are handled separately).
|
||||||
|
*/
|
||||||
|
public function getApiAppliedDiscountTotal(): float
|
||||||
|
{
|
||||||
|
$discount = 0.0;
|
||||||
|
foreach ($this->getApiAppliedDiscounts() as $item) {
|
||||||
|
$discount += abs($item->totalPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($discount, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,23 @@ 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'])]
|
#[Groups(['api:single', 'snapshot'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:single'])]
|
#[Groups(['api:single', 'snapshot'])]
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
|
|
||||||
#[Groups(['api:single'])]
|
#[Groups(['api:single', 'snapshot'])]
|
||||||
public bool $mutable = false;
|
public bool $mutable = false;
|
||||||
|
|
||||||
#[Groups(['api:single'])]
|
#[Groups(['api:single', 'snapshot'])]
|
||||||
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<string, string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
public static array $includedServicesMapping = [
|
public static array $includedServicesMapping = [
|
||||||
7 => 'Skipass',
|
7 => 'Skipass',
|
||||||
@@ -32,13 +32,16 @@ class CrmSelectionGroup
|
|||||||
90 => 'Transport',
|
90 => 'Transport',
|
||||||
];
|
];
|
||||||
|
|
||||||
#[Groups(['api:single'])]
|
#[Groups(['api:single', 'snapshot'])]
|
||||||
public ?int $id = null;
|
public ?int $id = null;
|
||||||
|
|
||||||
#[Groups(['api:single'])]
|
#[Groups(['api:single', 'snapshot'])]
|
||||||
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
<?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#',
|
|
||||||
];
|
|
||||||
}
|
|
||||||