Skip to content

Commit 709c68c

Browse files
authored
fix(ZMSKVR-1129 ZMSKVR-1134): add improve logging in zmscitizenapi and fix displayNumber for logged in bayern id citizens (#1779)
* feat(ZMSKVR-1129): add appointment process context to citizen API request logging (#1761) * feat(ZMSKVR-1129): add appointment process context to citizen API request logging - Log appointment processId from request/response across the citizen booking lifecycle - Bundle process-related data under context.process (processId, officeId, scopeId, serviceId, subRequestCounts, displayNumber) - Enable correlation of reserve/update/preconfirm/confirm/cancel/my-appointments calls by process * refactor(ZMSKVR-1129): simplify process context extraction in LoggerService * chore(ZMSKVR-1129): clean up LoggerService helpers * feat(ZMSKVR-1129): expose scope displayNumberPrefix in citizen API and logging * chore(logger): simplify process context extraction * refactor(logger): extract process context logic into reusable helper * fix(ZMSKVR-1129): tests * chore(ZMSKVR-1129): suppress NPath complexity warnings for logger and facade methods * chore(ZMSKVR-1129): simplify response body handling in logger * chore(ZMSKVR-1129): stop logging displayNumber in process context * chore(ZMSKVR-1129): log raw process id without prefix * feat(ZMSKVR-1129): add displayNumber to request logging in zmscitizenapi (#1771) * feat(ZMSKVR-1129): add appointment process context to citizen API request logging - Log appointment processId from request/response across the citizen booking lifecycle - Bundle process-related data under context.process (processId, officeId, scopeId, serviceId, subRequestCounts, displayNumber) - Enable correlation of reserve/update/preconfirm/confirm/cancel/my-appointments calls by process * refactor(ZMSKVR-1129): simplify process context extraction in LoggerService * chore(ZMSKVR-1129): clean up LoggerService helpers * feat(ZMSKVR-1129): expose scope displayNumberPrefix in citizen API and logging * chore(logger): simplify process context extraction * refactor(logger): extract process context logic into reusable helper * fix(ZMSKVR-1129): tests * chore(ZMSKVR-1129): suppress NPath complexity warnings for logger and facade methods * chore(ZMSKVR-1129): simplify response body handling in logger * chore(ZMSKVR-1129): stop logging displayNumber in process context * chore(ZMSKVR-1129): log raw process id without prefix * feat(ZMSKVR-1129): ensure displayNumber is assigned at reserved status - Add displayNumber to ThinnedProcess model and schema - Update MapperService to include displayNumber in mapping - Add displayNumber to ProcessContextExtractor for logging - Fix ProcessReserve to ensure scope preferences are loaded before displayNumber assignment - Update shouldUpdateDisplayNumber to include reserved status - Ensure displayNumber is consistently generated for reserve-appointment and update-appointment operations * clean(ZMSKVR-1129): remove displayNumberPrefix from ThinnedScope - Remove displayNumberPrefix property from ThinnedScope model - Remove displayNumberPrefix from thinnedScope.json schema - Remove displayNumberPrefix from MapperService mappings - Remove displayNumberPrefix from ZmsApiFacadeService mappings - Remove displayNumberPrefix from all test files - Add cache/ to .gitignore * clean(ZMSKVR-1129): comments from code * fix(ZMSKVR-1129): tests * clean(ZMSKVR-1129): remove unnecessary entity creation * fix(ZMSKVR-1129): handle processing transition to avoid invalid callback Add setStatusProcessing and map it in writeUpdatedStatus so processing transitions don’t hit a missing callback (call_user_func_array TypeError in zmsadmin call flow). * fix(ZMSKVR-1134): show displayNumber in my appointments * perf(ZMSKVR-1129): remove status update from processUpdate * perf(ZMSKVR-1129): remove unneeded status update in processReserved * fix(ZMSKVR-1134): use displayNumber in appointment detail breadcrumb and title * fix(ZMSKVR-1134): pass displayNumber in URL for logged-out users - Add ap-display query parameter to pass displayNumber in URL - Update AppointmentCard and AppointmentView to include displayNumber in navigation URLs - Update AppointmentDetailView to read and display displayNumber from URL when logged out - Use processId for fetching when logged in, displayNumber for display * clean(ZMSKVR-1129): remove unsupported locale parameter from ErrorMessages::get() call * clean(ZMSKVR-1134): remove unused appointmentNumber parameter from goToAppointmentLink * clean(ZMSKVR-1129): Remove unused method
1 parent c34371b commit 709c68c

22 files changed

+194
-33
lines changed

zmscitizenapi/src/Zmscitizenapi/Models/ThinnedProcess.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ class ThinnedProcess extends Entity implements JsonSerializable
4848
public ?string $captchaToken;
4949
/** @var int|null */
5050
public ?int $slotCount;
51+
/** @var string|null */
52+
public ?string $displayNumber;
5153
/** @var string|null */
5254
public ?string $icsContent;
5355

54-
public function __construct(?int $processId = null, ?string $timestamp = null, ?string $authKey = null, ?string $familyName = null, ?string $customTextfield = null, ?string $customTextfield2 = null, ?string $email = null, ?string $telephone = null, ?string $officeName = null, ?int $officeId = null, ?ThinnedScope $scope = null, array $subRequestCounts = [], ?int $serviceId = null, ?string $serviceName = null, int $serviceCount = 0, ?string $status = null, ?string $captchaToken = null, ?int $slotCount = null, ?string $icsContent = null)
56+
public function __construct(?int $processId = null, ?string $timestamp = null, ?string $authKey = null, ?string $familyName = null, ?string $customTextfield = null, ?string $customTextfield2 = null, ?string $email = null, ?string $telephone = null, ?string $officeName = null, ?int $officeId = null, ?ThinnedScope $scope = null, array $subRequestCounts = [], ?int $serviceId = null, ?string $serviceName = null, int $serviceCount = 0, ?string $status = null, ?string $captchaToken = null, ?int $slotCount = null, ?string $displayNumber = null, ?string $icsContent = null)
5557
{
5658
$this->processId = $processId;
5759
$this->timestamp = $timestamp;
@@ -71,6 +73,7 @@ public function __construct(?int $processId = null, ?string $timestamp = null, ?
7173
$this->status = $status;
7274
$this->captchaToken = $captchaToken;
7375
$this->slotCount = $slotCount;
76+
$this->displayNumber = $displayNumber;
7477
$this->icsContent = $icsContent;
7578
$this->ensureValid();
7679
}
@@ -101,6 +104,7 @@ public function toArray(): array
101104
'status' => $this->status ?? null,
102105
'captchaToken' => $this->captchaToken ?? null,
103106
'slotCount' => $this->slotCount ?? null,
107+
'displayNumber' => $this->displayNumber ?? null,
104108
'icsContent' => $this->icsContent ?? null
105109
];
106110
}

zmscitizenapi/src/Zmscitizenapi/Services/Core/LoggerService.php

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use BO\Zmscitizenapi\Application;
88
use BO\Zmscitizenapi\Utils\ClientIpHelper;
99
use BO\Zmscitizenapi\Utils\ErrorMessages;
10+
use BO\Zmscitizenapi\Services\Core\ProcessContextExtractor;
1011
use Psr\Http\Message\RequestInterface;
1112
use Psr\Http\Message\ResponseInterface;
1213
use Psr\Http\Message\ServerRequestInterface;
@@ -142,6 +143,9 @@ public static function logInfo(string $message, array $context = []): void
142143
\App::$log->info($message, $context);
143144
}
144145

146+
/**
147+
* @SuppressWarnings(PHPMD.NPathComplexity)
148+
*/
145149
public static function logRequest(ServerRequestInterface $request, ResponseInterface $response): void
146150
{
147151
if (!self::checkRateLimit()) {
@@ -176,27 +180,29 @@ public static function logRequest(ServerRequestInterface $request, ResponseInter
176180
'headers' => self::filterSensitiveHeaders($request->getHeaders())
177181
];
178182

183+
// Read response body once so it can be reused for both process extraction and error logging
184+
$bodyStream = $response->getBody();
185+
$rawBody = $bodyStream !== null ? (string) $bodyStream : null;
186+
187+
$processContext = ProcessContextExtractor::extractProcessContext($request, $rawBody);
188+
if (!empty($processContext)) {
189+
$data = array_merge($data, $processContext);
190+
}
191+
179192
if ($response->getStatusCode() >= 400) {
180-
$stream = $response->getBody();
181-
try {
182-
$body = (string)$stream;
183-
184-
if (!empty($body)) {
185-
$decodedBody = json_decode($body, true);
186-
if (json_last_error() === JSON_ERROR_NONE && isset($decodedBody['errors'])) {
187-
$englishErrors = [];
188-
foreach ($decodedBody['errors'] as $error) {
189-
if (isset($error['errorCode'])) {
190-
$englishErrors[] = ErrorMessages::get($error['errorCode'], 'en');
191-
} else {
192-
$englishErrors[] = $error;
193-
}
193+
if (!empty($rawBody)) {
194+
$decodedBody = json_decode($rawBody, true);
195+
if (json_last_error() === JSON_ERROR_NONE && isset($decodedBody['errors'])) {
196+
$errorMessages = [];
197+
foreach ($decodedBody['errors'] as $error) {
198+
if (isset($error['errorCode'])) {
199+
$errorMessages[] = ErrorMessages::get($error['errorCode']);
200+
} else {
201+
$errorMessages[] = $error;
194202
}
195-
$data['errors'] = $englishErrors;
196203
}
204+
$data['errors'] = $errorMessages;
197205
}
198-
} catch (\Exception $e) {
199-
$data['stream_error'] = 'Unable to read response body: ' . $e->getMessage();
200206
}
201207
}
202208

zmscitizenapi/src/Zmscitizenapi/Services/Core/MapperService.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ public static function processToThinnedProcess(Process $myProcess): ThinnedProce
424424
serviceCount: isset($mainServiceCount) ? $mainServiceCount : 0,
425425
status: (isset($myProcess->queue) && isset($myProcess->queue->status)) ? $myProcess->queue->status : null,
426426
slotCount: (isset($myProcess->appointments[0]) && isset($myProcess->appointments[0]->slotCount)) ? (int) $myProcess->appointments[0]->slotCount : null,
427+
displayNumber: ($myProcess->getDisplayNumber() ?: null),
427428
icsContent: isset($icsContent) ? $icsContent : null
428429
);
429430
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace BO\Zmscitizenapi\Services\Core;
6+
7+
use Psr\Http\Message\ServerRequestInterface;
8+
9+
class ProcessContextExtractor
10+
{
11+
public static function extractProcessContext(ServerRequestInterface $request, ?string $responseBody): array
12+
{
13+
$process = [];
14+
15+
// 1) Start with what the client sent (request body)
16+
$bodyData = $request->getParsedBody();
17+
if (is_array($bodyData)) {
18+
$process = self::buildProcessContextFromArray($bodyData);
19+
}
20+
21+
// 2) Overwrite with what the API returns (response JSON), if any
22+
if ($responseBody !== null && $responseBody !== '') {
23+
$decoded = json_decode($responseBody, true);
24+
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
25+
$fromResponse = self::buildProcessContextFromArray($decoded);
26+
if (!empty($fromResponse)) {
27+
$process = array_replace($process, $fromResponse);
28+
}
29+
}
30+
}
31+
32+
if (empty($process)) {
33+
return [];
34+
}
35+
36+
return ['process' => $process];
37+
}
38+
39+
private static function buildProcessContextFromArray(array $data): array
40+
{
41+
$process = [];
42+
43+
self::addIntFieldIfPresent($process, 'processId', $data, 'processId');
44+
self::addIntFieldIfPresent($process, 'officeId', $data, 'officeId');
45+
46+
$scopeId = self::extractScopeId($data);
47+
if ($scopeId !== null) {
48+
$process['scopeId'] = $scopeId;
49+
}
50+
51+
self::addIntFieldIfPresent($process, 'serviceId', $data, 'serviceId');
52+
53+
if (isset($data['displayNumber']) && is_string($data['displayNumber'])) {
54+
$process['displayNumber'] = $data['displayNumber'];
55+
}
56+
57+
return $process;
58+
}
59+
60+
private static function addIntFieldIfPresent(array &$process, string $targetKey, array $source, string $sourceKey): void
61+
{
62+
if (isset($source[$sourceKey]) && is_numeric($source[$sourceKey])) {
63+
$process[$targetKey] = (int)$source[$sourceKey];
64+
}
65+
}
66+
67+
private static function extractScopeId(array $data): ?int
68+
{
69+
if (isset($data['scope']['id']) && is_numeric($data['scope']['id'])) {
70+
return (int)$data['scope']['id'];
71+
}
72+
73+
if (isset($data['scopeId']) && is_numeric($data['scopeId'])) {
74+
return (int)$data['scopeId'];
75+
}
76+
77+
return null;
78+
}
79+
}

zmscitizenapi/src/Zmscitizenapi/Services/Core/ZmsApiFacadeService.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ public static function getOffices(bool $showUnpublished = false): OfficeList
137137
return $result;
138138
}
139139

140+
/**
141+
* @SuppressWarnings(PHPMD.NPathComplexity)
142+
*/
140143
public static function getScopes(): ThinnedScopeList|array
141144
{
142145
$cacheKey = self::CACHE_KEY_SCOPES;
@@ -322,7 +325,7 @@ public static function getScopeByOfficeId(int $officeId): ThinnedScope|array
322325
'whitelistedMails' => ((string) $matchingScope->getWhitelistedMails() === '' ? null : (string) $matchingScope->getWhitelistedMails()) ?? null,
323326
'reservationDuration' => (int) MapperService::extractReservationDuration($matchingScope),
324327
'activationDuration' => MapperService::extractActivationDuration($matchingScope),
325-
'hint' => (trim((string) ($matchingScope->getScopeHint() ?? '')) === '') ? null : (string) $matchingScope->getScopeHint(),
328+
'hint' => (trim((string) ($matchingScope->getScopeHint() ?? '')) === '') ? null : (string) $matchingScope->getScopeHint()
326329
];
327330
return new ThinnedScope(
328331
id: (int) $result['id'],
@@ -809,6 +812,9 @@ public static function getProcessById(?int $processId, ?string $authKey, ?Authen
809812
}
810813
}
811814

815+
/**
816+
* @SuppressWarnings(PHPMD.NPathComplexity)
817+
*/
812818
public static function getThinnedProcessById(int $processId, ?string $authKey, ?AuthenticatedUser $user): ThinnedProcess|array
813819
{
814820
$process = self::getProcessById($processId, $authKey, $user);

zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentByIdControllerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public function testRendering()
118118
"serviceName" => "Gewerbe anmelden",
119119
"serviceCount" => 1,
120120
"slotCount" => 1,
121+
"displayNumber" => null
121122
];
122123

123124
$this->assertEquals(200, $response->getStatusCode());

zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentCancelControllerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ public function testRendering()
123123
'serviceCount' => 1,
124124
'status' => 'deleted',
125125
'slotCount' => 1,
126+
'displayNumber' => null
126127
];
127128

128129
$this->assertEquals(200, $response->getStatusCode());

zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentConfirmControllerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public function testRendering()
120120
'serviceCount' => 1,
121121
'status' => 'confirmed',
122122
'slotCount' => 1,
123+
'displayNumber' => null
123124
];
124125

125126
$this->assertEquals(200, $response->getStatusCode());

zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentPreconfirmControllerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public function testRendering()
118118
'serviceCount' => 1,
119119
'status' => 'preconfirmed',
120120
'slotCount' => 1,
121+
'displayNumber' => null
121122
];
122123

123124
$this->assertEquals(200, $response->getStatusCode());

zmscitizenapi/tests/Zmscitizenapi/Controllers/Appointment/AppointmentReserveControllerTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public function testRendering()
121121
"serviceName" => null,
122122
"serviceCount" => 0,
123123
"slotCount" => 4,
124+
'displayNumber' => null
124125
];
125126

126127
$this->assertEquals(200, $response->getStatusCode());

0 commit comments

Comments
 (0)