Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.50% |
14 / 16 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
ErrorMessages | |
87.50% |
14 / 16 |
|
50.00% |
1 / 2 |
8.12 | |
0.00% |
0 / 1 |
get | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getHighestStatusCode | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
6.56 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace BO\Zmscitizenapi\Localization; |
6 | |
7 | class ErrorMessages |
8 | { |
9 | private const HTTP_OK = 200; |
10 | private const HTTP_BAD_REQUEST = 400; |
11 | private const HTTP_FORBIDDEN = 403; |
12 | private const HTTP_NOT_FOUND = 404; |
13 | private const HTTP_INVALID_REQUEST_METHOD = 405; |
14 | private const HTTP_NOT_ACCEPTABLE = 406; |
15 | private const HTTP_CONFLICT = 409; |
16 | private const HTTP_REQUEST_ENTITY_TOO_LARGE = 413; |
17 | private const HTTP_TOO_MANY_REQUESTS = 429; |
18 | private const HTTP_INTERNAL_SERVER_ERROR = 500; |
19 | private const HTTP_NOT_IMPLEMENTED = 501; |
20 | private const HTTP_UNAVAILABLE = 503; |
21 | private const HTTP_UNKNOWN = 520; |
22 | |
23 | // English messages only |
24 | private const MESSAGES = [ |
25 | 'zmsClientCommunicationError' => [ |
26 | 'errorCode' => 'zmsClientCommunicationError', |
27 | 'errorMessage' => 'The service is temporarily unavailable. Please try again later.', |
28 | 'statusCode' => self::HTTP_UNAVAILABLE, |
29 | 'errorType' => 'error' |
30 | ], |
31 | 'notImplemented' => [ |
32 | 'errorCode' => 'notImplemented', |
33 | 'errorMessage' => 'Feature not implemented yet.', |
34 | 'statusCode' => self::HTTP_NOT_IMPLEMENTED, |
35 | 'errorType' => 'error' |
36 | ], |
37 | 'notFound' => [ |
38 | 'errorCode' => 'notFound', |
39 | 'statusCode' => self::HTTP_NOT_FOUND, |
40 | 'errorMessage' => 'Endpoint not found.', |
41 | 'errorType' => 'error' |
42 | ], |
43 | 'invalidRequest' => [ |
44 | 'errorCode' => 'invalidRequest', |
45 | 'statusCode' => self::HTTP_BAD_REQUEST, |
46 | 'errorMessage' => 'Invalid request.', |
47 | 'errorType' => 'warning' |
48 | ], |
49 | 'requestMethodNotAllowed' => [ |
50 | 'errorCode' => 'requestMethodNotAllowed', |
51 | 'statusCode' => self::HTTP_INVALID_REQUEST_METHOD, |
52 | 'errorMessage' => 'Request method not allowed.', |
53 | 'errorType' => 'warning' |
54 | ], |
55 | 'captchaVerificationFailed' => [ |
56 | 'errorCode' => 'captchaVerificationFailed', |
57 | 'statusCode' => self::HTTP_BAD_REQUEST, |
58 | 'errorMessage' => 'Captcha verification failed.', |
59 | 'errorType' => 'warning' |
60 | ], |
61 | 'invalidLocationAndServiceCombination' => [ |
62 | 'errorCode' => 'invalidLocationAndServiceCombination', |
63 | 'errorMessage' => 'The provided service(s) do not exist at the given location.', |
64 | 'statusCode' => self::HTTP_BAD_REQUEST, |
65 | 'errorType' => 'warning' |
66 | ], |
67 | 'invalidStartDate' => [ |
68 | 'errorCode' => 'invalidStartDate', |
69 | 'statusCode' => self::HTTP_BAD_REQUEST, |
70 | 'errorMessage' => 'startDate is required and must be a valid date.', |
71 | 'errorType' => 'warning' |
72 | ], |
73 | 'invalidEndDate' => [ |
74 | 'errorCode' => 'invalidEndDate', |
75 | 'statusCode' => self::HTTP_BAD_REQUEST, |
76 | 'errorMessage' => 'endDate is required and must be a valid date.', |
77 | 'errorType' => 'warning' |
78 | ], |
79 | 'invalidOfficeId' => [ |
80 | 'errorCode' => 'invalidOfficeId', |
81 | 'statusCode' => self::HTTP_BAD_REQUEST, |
82 | 'errorType' => 'warning', |
83 | 'errorMessage' => 'officeId should be a 32-bit integer.' |
84 | ], |
85 | 'invalidServiceId' => [ |
86 | 'errorCode' => 'invalidServiceId', |
87 | 'statusCode' => self::HTTP_BAD_REQUEST, |
88 | 'errorType' => 'warning', |
89 | 'errorMessage' => 'serviceId should be a 32-bit integer.' |
90 | ], |
91 | 'emptyServiceArrays' => [ |
92 | 'errorCode' => 'EMPTY_SERVICE_ARRAYS', |
93 | 'statusCode' => self::HTTP_BAD_REQUEST, |
94 | 'errorMessage' => 'Service IDs and counts cannot be empty.', |
95 | 'errorType' => 'warning' |
96 | ], |
97 | 'mismatchedArrays' => [ |
98 | 'errorCode' => 'MISMATCHED_ARRAYS', |
99 | 'statusCode' => self::HTTP_BAD_REQUEST, |
100 | 'errorMessage' => 'Service IDs and counts must have same length.', |
101 | 'errorType' => 'warning' |
102 | ], |
103 | 'invalidServiceCount' => [ |
104 | 'errorCode' => 'invalidServiceCount', |
105 | 'statusCode' => self::HTTP_BAD_REQUEST, |
106 | 'errorType' => 'warning', |
107 | 'errorMessage' => 'serviceCounts should be an array of numeric values.' |
108 | ], |
109 | 'invalidProcessId' => [ |
110 | 'errorCode' => 'invalidProcessId', |
111 | 'statusCode' => self::HTTP_BAD_REQUEST, |
112 | 'errorType' => 'warning', |
113 | 'errorMessage' => 'processId should be a positive 32-bit integer.' |
114 | ], |
115 | 'invalidScopeId' => [ |
116 | 'errorCode' => 'invalidScopeId', |
117 | 'statusCode' => self::HTTP_BAD_REQUEST, |
118 | 'errorType' => 'warning', |
119 | 'errorMessage' => 'scopeId should be a positive 32-bit integer.' |
120 | ], |
121 | 'invalidAuthKey' => [ |
122 | 'errorCode' => 'invalidAuthKey', |
123 | 'statusCode' => self::HTTP_BAD_REQUEST, |
124 | 'errorType' => 'warning', |
125 | 'errorMessage' => 'authKey should be a string.' |
126 | ], |
127 | 'invalidDate' => [ |
128 | 'errorCode' => 'invalidDate', |
129 | 'statusCode' => self::HTTP_BAD_REQUEST, |
130 | 'errorType' => 'warning', |
131 | 'errorMessage' => 'date is required and must be a valid date.' |
132 | ], |
133 | 'invalidTimestamp' => [ |
134 | 'errorCode' => 'invalidTimestamp', |
135 | 'statusCode' => self::HTTP_BAD_REQUEST, |
136 | 'errorType' => 'warning', |
137 | 'errorMessage' => 'Missing timestamp or invalid timestamp format. It should be a positive numeric value.' |
138 | ], |
139 | 'invalidFamilyName' => [ |
140 | 'errorCode' => 'invalidFamilyName', |
141 | 'statusCode' => self::HTTP_BAD_REQUEST, |
142 | 'errorType' => 'warning', |
143 | 'errorMessage' => 'familyName should be a non-empty string.' |
144 | ], |
145 | 'invalidEmail' => [ |
146 | 'errorCode' => 'invalidEmail', |
147 | 'statusCode' => self::HTTP_BAD_REQUEST, |
148 | 'errorType' => 'warning', |
149 | 'errorMessage' => 'email should be a valid email address.' |
150 | ], |
151 | 'invalidTelephone' => [ |
152 | 'errorCode' => 'invalidTelephone', |
153 | 'statusCode' => self::HTTP_BAD_REQUEST, |
154 | 'errorType' => 'warning', |
155 | 'errorMessage' => 'telephone should be a numeric string between 7 and 15 digits.' |
156 | ], |
157 | 'invalidCustomTextfield' => [ |
158 | 'errorCode' => 'invalidCustomTextfield', |
159 | 'statusCode' => self::HTTP_BAD_REQUEST, |
160 | 'errorType' => 'warning', |
161 | 'errorMessage' => 'customTextfield should be a string.' |
162 | ], |
163 | 'appointmentCanNotBeCanceled' => [ |
164 | 'errorCode' => 'appointmentCanNotBeCanceled', |
165 | 'statusCode' => self::HTTP_NOT_ACCEPTABLE, |
166 | 'errorType' => 'warning', |
167 | 'errorMessage' => 'The selected appointment cannot be canceled.' |
168 | ], |
169 | 'appointmentNotAvailable' => [ |
170 | 'errorCode' => 'appointmentNotAvailable', |
171 | 'statusCode' => self::HTTP_NOT_FOUND, |
172 | 'errorMessage' => 'The selected appointment is unfortunately no longer available.', |
173 | 'errorType' => 'error' |
174 | ], |
175 | 'noAppointmentForThisDay' => [ |
176 | 'errorCode' => 'noAppointmentForThisDay', |
177 | 'statusCode' => self::HTTP_NOT_FOUND, |
178 | 'errorMessage' => 'No available days found for the given criteria.', |
179 | 'errorType' => 'info' |
180 | ], |
181 | 'captchaVerificationError' => [ |
182 | 'errorCode' => 'captchaVerificationError', |
183 | 'statusCode' => self::HTTP_BAD_REQUEST, |
184 | 'errorType' => 'warning', |
185 | 'errorMessage' => 'An error occurred during captcha verification.' |
186 | ], |
187 | 'captchaMissing' => [ |
188 | 'errorCode' => 'captchaMissing', |
189 | 'statusCode' => self::HTTP_BAD_REQUEST, |
190 | 'errorType' => 'warning', |
191 | 'errorMessage' => 'Missing captcha token.', |
192 | ], |
193 | 'captchaInvalid' => [ |
194 | 'errorCode' => 'captchaInvalid', |
195 | 'statusCode' => self::HTTP_BAD_REQUEST, |
196 | 'errorType' => 'warning', |
197 | 'errorMessage' => 'Invalid captcha token.', |
198 | ], |
199 | 'captchaExpired' => [ |
200 | 'errorCode' => 'captchaExpired', |
201 | 'statusCode' => self::HTTP_BAD_REQUEST, |
202 | 'errorType' => 'warning', |
203 | 'errorMessage' => 'Captcha token expired.', |
204 | ], |
205 | 'serviceUnavailable' => [ |
206 | 'errorCode' => 'serviceUnavailable', |
207 | 'statusCode' => self::HTTP_UNAVAILABLE, |
208 | 'errorMessage' => 'Service Unavailable: The application is under maintenance.', |
209 | 'errorType' => 'error' |
210 | ], |
211 | 'invalidSchema' => [ |
212 | 'errorCode' => 'invalidSchema', |
213 | 'statusCode' => self::HTTP_BAD_REQUEST, |
214 | 'errorType' => 'warning', |
215 | 'errorMessage' => 'Data does not match the required schema.' |
216 | ], |
217 | |
218 | //Zmsapi exceptions |
219 | 'internalError' => [ |
220 | 'errorCode' => 'internalError', |
221 | 'errorMessage' => 'An internal error occurred. Please try again later.', |
222 | 'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR, |
223 | 'errorType' => 'error' |
224 | ], |
225 | 'invalidApiClient' => [ |
226 | 'errorCode' => 'invalidApiClient', |
227 | 'errorMessage' => 'Invalid API client.', |
228 | 'statusCode' => self::HTTP_BAD_REQUEST, |
229 | 'errorType' => 'warning' |
230 | ], |
231 | 'sourceNotFound' => [ |
232 | 'errorCode' => 'sourceNotFound', |
233 | 'statusCode' => self::HTTP_NOT_FOUND, |
234 | 'errorMessage' => 'Source not found.', |
235 | 'errorType' => 'error' |
236 | ], |
237 | 'departmentNotFound' => [ |
238 | 'errorCode' => 'departmentNotFound', |
239 | 'errorMessage' => 'Department not found.', |
240 | 'statusCode' => self::HTTP_NOT_FOUND, |
241 | 'errorType' => 'error' |
242 | ], |
243 | 'mailNotFound' => [ |
244 | 'errorCode' => 'mailNotFound', |
245 | 'errorMessage' => 'Mail template not found.', |
246 | 'statusCode' => self::HTTP_NOT_FOUND, |
247 | 'errorType' => 'error' |
248 | ], |
249 | 'organisationNotFound' => [ |
250 | 'errorCode' => 'organisationNotFound', |
251 | 'errorMessage' => 'Organisation not found.', |
252 | 'statusCode' => self::HTTP_NOT_FOUND, |
253 | 'errorType' => 'error' |
254 | ], |
255 | 'providerNotFound' => [ |
256 | 'errorCode' => 'providerNotFound', |
257 | 'errorMessage' => 'Provider not found.', |
258 | 'statusCode' => self::HTTP_NOT_FOUND, |
259 | 'errorType' => 'error' |
260 | ], |
261 | 'requestNotFound' => [ |
262 | 'errorCode' => 'requestNotFound', |
263 | 'errorMessage' => 'Requested service not found.', |
264 | 'statusCode' => self::HTTP_NOT_FOUND, |
265 | 'errorType' => 'error' |
266 | ], |
267 | 'scopeNotFound' => [ |
268 | 'errorCode' => 'scopeNotFound', |
269 | 'errorMessage' => 'Scope not found.', |
270 | 'statusCode' => self::HTTP_NOT_FOUND, |
271 | 'errorType' => 'error' |
272 | ], |
273 | 'processInvalid' => [ |
274 | 'errorCode' => 'processInvalid', |
275 | 'errorMessage' => 'The process data is invalid.', |
276 | 'statusCode' => self::HTTP_BAD_REQUEST, |
277 | 'errorType' => 'warning' |
278 | ], |
279 | 'processAlreadyExists' => [ |
280 | 'errorCode' => 'processAlreadyExists', |
281 | 'errorMessage' => 'An appointment process already exists.', |
282 | 'statusCode' => self::HTTP_CONFLICT, |
283 | 'errorType' => 'warning' |
284 | ], |
285 | 'processDeleteFailed' => [ |
286 | 'errorCode' => 'processDeleteFailed', |
287 | 'errorMessage' => 'Failed to delete the appointment.', |
288 | 'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR, |
289 | 'errorType' => 'error' |
290 | ], |
291 | 'processAlreadyCalled' => [ |
292 | 'errorCode' => 'processAlreadyCalled', |
293 | 'errorMessage' => 'The appointment has already been called.', |
294 | 'statusCode' => self::HTTP_CONFLICT, |
295 | 'errorType' => 'warning' |
296 | ], |
297 | 'processNotReservedAnymore' => [ |
298 | 'errorCode' => 'processNotReservedAnymore', |
299 | 'errorMessage' => 'The appointment is no longer reserved.', |
300 | 'statusCode' => self::HTTP_CONFLICT, |
301 | 'errorType' => 'error' |
302 | ], |
303 | 'processNotPreconfirmedAnymore' => [ |
304 | 'errorCode' => 'processNotPreconfirmedAnymore', |
305 | 'errorMessage' => 'The appointment is no longer preconfirmed.', |
306 | 'statusCode' => self::HTTP_CONFLICT, |
307 | 'errorType' => 'error' |
308 | ], |
309 | 'emailIsRequired' => [ |
310 | 'errorCode' => 'emailIsRequired', |
311 | 'errorMessage' => 'Email address is required.', |
312 | 'statusCode' => self::HTTP_NOT_ACCEPTABLE, |
313 | 'errorType' => 'warning' |
314 | ], |
315 | 'telephoneIsRequired' => [ |
316 | 'errorCode' => 'telephoneIsRequired', |
317 | 'errorMessage' => 'Telephone number is required.', |
318 | 'statusCode' => self::HTTP_NOT_ACCEPTABLE, |
319 | 'errorType' => 'warning' |
320 | ], |
321 | 'appointmentNotFound' => [ |
322 | 'errorCode' => 'appointmentNotFound', |
323 | 'errorMessage' => 'Maybe you have already canceled your appointment? Otherwise, please check that you have used the correct link.', |
324 | 'statusCode' => self::HTTP_NOT_FOUND, |
325 | 'errorType' => 'error' |
326 | ], |
327 | 'authKeyMismatch' => [ |
328 | 'errorCode' => 'authKeyMismatch', |
329 | 'errorMessage' => 'Invalid authentication key.', |
330 | 'statusCode' => self::HTTP_NOT_ACCEPTABLE, |
331 | 'errorType' => 'warning' |
332 | ], |
333 | 'noAppointmentForThisScope' => [ |
334 | 'errorCode' => 'noAppointmentForThisScope', |
335 | 'errorMessage' => 'Please try again at a later time.', |
336 | 'statusCode' => self::HTTP_NOT_FOUND, |
337 | 'errorType' => 'info' |
338 | ], |
339 | 'tooManyAppointmentsWithSameMail' => [ |
340 | 'errorCode' => 'tooManyAppointmentsWithSameMail', |
341 | 'errorMessage' => 'You can only book a limited number of appointments with your e-mail address. Please cancel another appointment before you book a new one.', |
342 | 'statusCode' => self::HTTP_NOT_ACCEPTABLE, |
343 | 'errorType' => 'error' |
344 | ], |
345 | 'scopesNotFound' => [ |
346 | 'errorCode' => 'scopesNotFound', |
347 | 'errorMessage' => 'No scopes found.', |
348 | 'statusCode' => self::HTTP_NOT_FOUND, |
349 | 'errorType' => 'error' |
350 | ], |
351 | 'preconfirmationExpired' => [ |
352 | 'errorCode' => 'preconfirmationExpired', |
353 | 'statusCode' => self::HTTP_BAD_REQUEST, |
354 | 'errorMessage' => 'Unfortunately, the time for activating your appointment has expired. Please schedule the appointment again.', |
355 | 'errorType' => 'error' |
356 | ], |
357 | |
358 | //Middleware exceptions |
359 | 'ipBlacklisted' => [ |
360 | 'errorCode' => 'IP_BLACKLISTED', |
361 | 'statusCode' => self::HTTP_FORBIDDEN, |
362 | 'errorMessage' => 'Access denied - IP address is blacklisted.', |
363 | 'errorType' => 'error' |
364 | ], |
365 | 'rateLimitExceeded' => [ |
366 | 'errorCode' => 'rateLimitExceeded', |
367 | 'statusCode' => self::HTTP_TOO_MANY_REQUESTS, |
368 | 'errorMessage' => 'Rate limit exceeded. Please try again later.', |
369 | 'errorType' => 'warning' |
370 | ], |
371 | 'requestEntityTooLarge' => [ |
372 | 'errorCode' => 'requestEntityTooLarge', |
373 | 'statusCode' => self::HTTP_REQUEST_ENTITY_TOO_LARGE, |
374 | 'errorMessage' => 'Request entity too large.', |
375 | 'errorType' => 'warning' |
376 | ], |
377 | 'securityHeaderViolation' => [ |
378 | 'errorCode' => 'securityHeaderViolation', |
379 | 'statusCode' => self::HTTP_FORBIDDEN, |
380 | 'errorMessage' => 'Security policy violation.', |
381 | 'errorType' => 'error' |
382 | ] |
383 | ]; |
384 | |
385 | /** |
386 | * Get an error message by key. |
387 | * |
388 | * @param string $key The error message key. |
389 | * @return array The error message array. |
390 | */ |
391 | public static function get(string $key): array |
392 | { |
393 | if (isset(self::MESSAGES[$key])) { |
394 | return self::MESSAGES[$key]; |
395 | } |
396 | |
397 | return [ |
398 | 'errorCode' => 'unknownError', |
399 | 'statusCode' => self::HTTP_UNKNOWN, |
400 | 'errorMessage' => 'An unknown error occurred.', |
401 | 'errorType' => 'error' |
402 | ]; |
403 | } |
404 | |
405 | /** |
406 | * Get the highest status code from an array of errors. |
407 | * |
408 | * @param array $errors Array of error messages |
409 | * @return int The highest status code found, or HTTP_OK (200) if no errors |
410 | * @throws \InvalidArgumentException If any error has an invalid structure |
411 | */ |
412 | public static function getHighestStatusCode(array $errors): int |
413 | { |
414 | if (empty($errors)) { |
415 | return self::HTTP_OK; |
416 | } |
417 | |
418 | $errorCodes = []; |
419 | foreach ($errors as $error) { |
420 | if (!is_array($error) || !isset($error['statusCode']) || !is_int($error['statusCode'])) { |
421 | throw new \InvalidArgumentException('Invalid error structure. Each error must have a statusCode.'); |
422 | } |
423 | $errorCodes[] = $error['statusCode']; |
424 | } |
425 | |
426 | return max($errorCodes); |
427 | } |
428 | } |