Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.50% covered (warning)
87.50%
14 / 16
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ErrorMessages
87.50% covered (warning)
87.50%
14 / 16
50.00% covered (danger)
50.00%
1 / 2
8.12
0.00% covered (danger)
0.00%
0 / 1
 get
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getHighestStatusCode
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
6.56
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Zmscitizenapi\Localization;
6
7class 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}