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\Utils;
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' => 'error'
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        'invalidSlotsStartDate' => [
80            'errorCode' => 'invalidSlotsStartDate',
81            'statusCode' => self::HTTP_BAD_REQUEST,
82            'errorMessage' => 'slotsStartDate must be a valid date.',
83            'errorType' => 'warning'
84        ],
85        'invalidSlotsEndDate' => [
86            'errorCode' => 'invalidSlotsEndDate',
87            'statusCode' => self::HTTP_BAD_REQUEST,
88            'errorMessage' => 'slotsEndDate must be a valid date.',
89            'errorType' => 'warning'
90        ],
91        'slotsStartDateAfterEndDate' => [
92            'errorCode' => 'slotsStartDateAfterEndDate',
93            'statusCode' => self::HTTP_BAD_REQUEST,
94            'errorMessage' => 'slotsStartDate must not be after slotsEndDate.',
95            'errorType' => 'warning'
96        ],
97        'invalidOfficeId' => [
98            'errorCode' => 'invalidOfficeId',
99            'statusCode' => self::HTTP_BAD_REQUEST,
100            'errorType' => 'warning',
101            'errorMessage' => 'officeId should be a 32-bit integer.'
102        ],
103        'invalidServiceId' => [
104            'errorCode' => 'invalidServiceId',
105            'statusCode' => self::HTTP_BAD_REQUEST,
106            'errorType' => 'warning',
107            'errorMessage' => 'serviceId should be a 32-bit integer.'
108        ],
109        'emptyServiceArrays' => [
110            'errorCode' => 'EMPTY_SERVICE_ARRAYS',
111            'statusCode' => self::HTTP_BAD_REQUEST,
112            'errorMessage' => 'Service IDs and counts cannot be empty.',
113            'errorType' => 'warning'
114        ],
115        'mismatchedArrays' => [
116            'errorCode' => 'MISMATCHED_ARRAYS',
117            'statusCode' => self::HTTP_BAD_REQUEST,
118            'errorMessage' => 'Service IDs and counts must have same length.',
119            'errorType' => 'warning'
120        ],
121        'invalidServiceCount' => [
122            'errorCode' => 'invalidServiceCount',
123            'statusCode' => self::HTTP_BAD_REQUEST,
124            'errorType' => 'warning',
125            'errorMessage' => 'serviceCounts should be an array of numeric values.'
126        ],
127        'invalidProcessId' => [
128            'errorCode' => 'invalidProcessId',
129            'statusCode' => self::HTTP_BAD_REQUEST,
130            'errorType' => 'warning',
131            'errorMessage' => 'processId should be a positive 32-bit integer.'
132        ],
133        'invalidScopeId' => [
134            'errorCode' => 'invalidScopeId',
135            'statusCode' => self::HTTP_BAD_REQUEST,
136            'errorType' => 'warning',
137            'errorMessage' => 'scopeId should be a positive 32-bit integer.'
138        ],
139        'invalidAuthKey' => [
140            'errorCode' => 'invalidAuthKey',
141            'statusCode' => self::HTTP_BAD_REQUEST,
142            'errorType' => 'warning',
143            'errorMessage' => 'authKey should be a string.'
144        ],
145        'invalidDate' => [
146            'errorCode' => 'invalidDate',
147            'statusCode' => self::HTTP_BAD_REQUEST,
148            'errorType' => 'warning',
149            'errorMessage' => 'date is required and must be a valid date.'
150        ],
151        'invalidTimestamp' => [
152            'errorCode' => 'invalidTimestamp',
153            'statusCode' => self::HTTP_BAD_REQUEST,
154            'errorType' => 'warning',
155            'errorMessage' => 'Missing timestamp or invalid timestamp format. It should be a positive numeric value.'
156        ],
157        'invalidFamilyName' => [
158            'errorCode' => 'invalidFamilyName',
159            'statusCode' => self::HTTP_BAD_REQUEST,
160            'errorType' => 'warning',
161            'errorMessage' => 'familyName should be a non-empty string.'
162        ],
163        'invalidEmail' => [
164            'errorCode' => 'invalidEmail',
165            'statusCode' => self::HTTP_BAD_REQUEST,
166            'errorType' => 'warning',
167            'errorMessage' => 'email should be a valid email address.'
168        ],
169        'invalidTelephone' => [
170            'errorCode' => 'invalidTelephone',
171            'statusCode' => self::HTTP_BAD_REQUEST,
172            'errorType' => 'warning',
173            'errorMessage' => 'telephone should be a numeric string between 7 and 15 digits.'
174        ],
175        'invalidCustomTextfield' => [
176            'errorCode' => 'invalidCustomTextfield',
177            'statusCode' => self::HTTP_BAD_REQUEST,
178            'errorType' => 'warning',
179            'errorMessage' => 'customTextfield must be non-empty when required and at most 250 characters after normalization.'
180        ],
181        'invalidCustomTextfield2' => [
182            'errorCode' => 'invalidCustomTextfield2',
183            'statusCode' => self::HTTP_BAD_REQUEST,
184            'errorType' => 'warning',
185            'errorMessage' => 'customTextfield2 must be non-empty when required and at most 250 characters after normalization.'
186        ],
187        'appointmentCanNotBeCanceled' => [
188            'errorCode' => 'appointmentCanNotBeCanceled',
189            'statusCode' => self::HTTP_NOT_ACCEPTABLE,
190            'errorType' => 'warning',
191            'errorMessage' => 'The selected appointment cannot be canceled.'
192        ],
193        'appointmentNotAvailable' => [
194            'errorCode' => 'appointmentNotAvailable',
195            'statusCode' => self::HTTP_NOT_FOUND,
196            'errorMessage' => 'The selected appointment is unfortunately no longer available.',
197            'errorType' => 'error'
198        ],
199        'noAppointmentForThisDay' => [
200            'errorCode' => 'noAppointmentForThisDay',
201            'statusCode' => self::HTTP_NOT_FOUND,
202            'errorMessage' => 'No available days found for the given criteria.',
203            'errorType' => 'info'
204        ],
205        'captchaVerificationError' => [
206            'errorCode' => 'captchaVerificationError',
207            'statusCode' => self::HTTP_BAD_REQUEST,
208            'errorType' => 'error',
209            'errorMessage' => 'An error occurred during captcha verification.'
210        ],
211        'captchaMissing' => [
212            'errorCode' => 'captchaMissing',
213            'statusCode' => self::HTTP_BAD_REQUEST,
214            'errorType' => 'error',
215            'errorMessage' => 'Missing captcha token.',
216        ],
217        'captchaInvalid' => [
218            'errorCode' => 'captchaInvalid',
219            'statusCode' => self::HTTP_BAD_REQUEST,
220            'errorType' => 'error',
221            'errorMessage' => 'Invalid captcha token.',
222        ],
223        'captchaExpired' => [
224            'errorCode' => 'captchaExpired',
225            'statusCode' => self::HTTP_BAD_REQUEST,
226            'errorType' => 'error',
227            'errorMessage' => 'Captcha token expired.',
228        ],
229        'serviceUnavailable' => [
230            'errorCode' => 'serviceUnavailable',
231            'statusCode' => self::HTTP_UNAVAILABLE,
232            'errorMessage' => 'Service Unavailable: The application is under maintenance.',
233            'errorType' => 'error'
234        ],
235        'invalidSchema' => [
236            'errorCode' => 'invalidSchema',
237            'statusCode' => self::HTTP_BAD_REQUEST,
238            'errorType' => 'warning',
239            'errorMessage' => 'Data does not match the required schema.'
240        ],
241
242        //Zmsbackend exceptions
243        'internalError' => [
244            'errorCode' => 'internalError',
245            'errorMessage' => 'An internal error occurred. Please try again later.',
246            'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR,
247            'errorType' => 'error'
248        ],
249        'invalidApiClient' => [
250            'errorCode' => 'invalidApiClient',
251            'errorMessage' => 'Invalid API client.',
252            'statusCode' => self::HTTP_BAD_REQUEST,
253            'errorType' => 'warning'
254        ],
255        'sourceNotFound' => [
256            'errorCode' => 'sourceNotFound',
257            'statusCode' => self::HTTP_NOT_FOUND,
258            'errorMessage' => 'Source not found.',
259            'errorType' => 'info'
260        ],
261        'departmentNotFound' => [
262            'errorCode' => 'departmentNotFound',
263            'errorMessage' => 'Department not found.',
264            'statusCode' => self::HTTP_NOT_FOUND,
265            'errorType' => 'error'
266        ],
267        'mailNotFound' => [
268            'errorCode' => 'mailNotFound',
269            'errorMessage' => 'Mail template not found.',
270            'statusCode' => self::HTTP_NOT_FOUND,
271            'errorType' => 'error'
272        ],
273        'organisationNotFound' => [
274            'errorCode' => 'organisationNotFound',
275            'errorMessage' => 'Organisation not found.',
276            'statusCode' => self::HTTP_NOT_FOUND,
277            'errorType' => 'error'
278        ],
279        'providerNotFound' => [
280            'errorCode' => 'providerNotFound',
281            'errorMessage' => 'Provider not found.',
282            'statusCode' => self::HTTP_NOT_FOUND,
283            'errorType' => 'error'
284        ],
285        'requestNotFound' => [
286            'errorCode' => 'requestNotFound',
287            'errorMessage' => 'Requested service not found.',
288            'statusCode' => self::HTTP_NOT_FOUND,
289            'errorType' => 'error'
290        ],
291        'scopeNotFound' => [
292            'errorCode' => 'scopeNotFound',
293            'errorMessage' => 'Scope not found.',
294            'statusCode' => self::HTTP_NOT_FOUND,
295            'errorType' => 'error'
296        ],
297        'processInvalid' => [
298            'errorCode' => 'processInvalid',
299            'errorMessage' => 'The process data is invalid.',
300            'statusCode' => self::HTTP_BAD_REQUEST,
301            'errorType' => 'warning'
302        ],
303        'processAlreadyExists' => [
304            'errorCode' => 'processAlreadyExists',
305            'errorMessage' => 'An appointment process already exists.',
306            'statusCode' => self::HTTP_CONFLICT,
307            'errorType' => 'warning'
308        ],
309        'processDeleteFailed' => [
310            'errorCode' => 'processDeleteFailed',
311            'errorMessage' => 'Failed to delete the appointment.',
312            'statusCode' => self::HTTP_INTERNAL_SERVER_ERROR,
313            'errorType' => 'error'
314        ],
315        'processAlreadyCalled' => [
316            'errorCode' => 'processAlreadyCalled',
317            'errorMessage' => 'The appointment has already been called.',
318            'statusCode' => self::HTTP_CONFLICT,
319            'errorType' => 'warning'
320        ],
321        'processNotReservedAnymore' => [
322            'errorCode' => 'processNotReservedAnymore',
323            'errorMessage' => 'The appointment is no longer reserved.',
324            'statusCode' => self::HTTP_CONFLICT,
325            'errorType' => 'error'
326        ],
327        'processNotPreconfirmedAnymore' => [
328            'errorCode' => 'processNotPreconfirmedAnymore',
329            'errorMessage' => 'The appointment is no longer preconfirmed.',
330            'statusCode' => self::HTTP_CONFLICT,
331            'errorType' => 'error'
332        ],
333        'emailIsRequired' => [
334            'errorCode' => 'emailIsRequired',
335            'errorMessage' => 'Email address is required.',
336            'statusCode' => self::HTTP_NOT_ACCEPTABLE,
337            'errorType' => 'warning'
338        ],
339        'telephoneIsRequired' => [
340            'errorCode' => 'telephoneIsRequired',
341            'errorMessage' => 'Telephone number is required.',
342            'statusCode' => self::HTTP_NOT_ACCEPTABLE,
343            'errorType' => 'warning'
344        ],
345        'appointmentNotFound' => [
346            'errorCode' => 'appointmentNotFound',
347            'errorMessage' => 'Maybe you have already canceled your appointment? Otherwise, please check that you have used the correct link.',
348            'statusCode' => self::HTTP_NOT_FOUND,
349            'errorType' => 'error'
350        ],
351        'authKeyMismatch' => [
352            'errorCode' => 'authKeyMismatch',
353            'errorMessage' => 'Invalid authentication key.',
354            'statusCode' => self::HTTP_NOT_ACCEPTABLE,
355            'errorType' => 'warning'
356        ],
357        'invalidCredentials' => [
358            'errorCode' => 'invalidCredentials',
359            'errorMessage' => 'Invalid credentials.',
360            'statusCode' => self::HTTP_NOT_ACCEPTABLE,
361            'errorType' => 'warning'
362        ],
363        'noAppointmentForThisScope' => [
364            'errorCode' => 'noAppointmentForThisScope',
365            'errorMessage' => 'Please try again at a later time.',
366            'statusCode' => self::HTTP_NOT_FOUND,
367            'errorType' => 'info'
368        ],
369        'tooManyAppointmentsWithSameMail' => [
370            'errorCode' => 'tooManyAppointmentsWithSameMail',
371            '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.',
372            'statusCode' => self::HTTP_NOT_ACCEPTABLE,
373            'errorType' => 'error'
374        ],
375        'tooManySlotsPerAppointment' => [
376            'errorCode' => 'tooManySlotsPerAppointment',
377            'errorMessage' => 'The number of slots exceeds the maximum allowed slots per appointment.',
378            'statusCode' => self::HTTP_BAD_REQUEST,
379            'errorType' => 'error'
380        ],
381        'tooManyServicesPerAppointment' => [
382            'errorCode' => 'tooManyServicesPerAppointment',
383            'errorMessage' => 'The quantity of a service exceeds the maximum allowed quantity.',
384            'statusCode' => self::HTTP_BAD_REQUEST,
385            'errorType' => 'error'
386        ],
387        'scopesNotFound' => [
388            'errorCode' => 'scopesNotFound',
389            'errorMessage' => 'No scopes found.',
390            'statusCode' => self::HTTP_NOT_FOUND,
391            'errorType' => 'error'
392        ],
393        'preconfirmationExpired' => [
394            'errorCode' => 'preconfirmationExpired',
395            'statusCode' => self::HTTP_BAD_REQUEST,
396            'errorMessage' => 'Unfortunately, the time for activating your appointment has expired. Please schedule the appointment again.',
397            'errorType' => 'error'
398        ],
399
400        //Middleware exceptions
401        'ipBlacklisted' => [
402            'errorCode' => 'IP_BLACKLISTED',
403            'statusCode' => self::HTTP_FORBIDDEN,
404            'errorMessage' => 'Access denied - IP address is blacklisted.',
405            'errorType' => 'error'
406        ],
407        'rateLimitExceeded' => [
408            'errorCode' => 'rateLimitExceeded',
409            'statusCode' => self::HTTP_TOO_MANY_REQUESTS,
410            'errorMessage' => 'Rate limit exceeded. Please try again later.',
411            'errorType' => 'warning'
412        ],
413        'requestEntityTooLarge' => [
414            'errorCode' => 'requestEntityTooLarge',
415            'statusCode' => self::HTTP_REQUEST_ENTITY_TOO_LARGE,
416            'errorMessage' => 'Request entity too large.',
417            'errorType' => 'warning'
418        ],
419        'securityHeaderViolation' => [
420            'errorCode' => 'securityHeaderViolation',
421            'statusCode' => self::HTTP_FORBIDDEN,
422            'errorMessage' => 'Security policy violation.',
423            'errorType' => 'error'
424        ],
425        'unauthorized' => [
426            'errorCode' => 'unauthorized',
427            'errorMessage' => 'You are not authorized to access this resource.',
428            'statusCode' => self::HTTP_FORBIDDEN,
429            'errorType' => 'error'
430        ],
431    ];
432
433    /**
434     * Get an error message by key.
435     *
436     * @param string $key The error message key.
437     * @return array The error message array.
438     */
439    public static function get(string $key): array
440    {
441        if (isset(self::MESSAGES[$key])) {
442            return self::MESSAGES[$key];
443        }
444
445        return [
446            'errorCode' => 'unknownError',
447            'statusCode' => self::HTTP_UNKNOWN,
448            'errorMessage' => 'An unknown error occurred.',
449            'errorType' => 'error'
450        ];
451    }
452
453    /**
454     * Get the highest status code from an array of errors.
455     *
456     * @param array $errors Array of error messages
457     * @return int The highest status code found, or HTTP_OK (200) if no errors
458     * @throws \InvalidArgumentException If any error has an invalid structure
459     */
460    public static function getHighestStatusCode(array $errors): int
461    {
462        if (empty($errors)) {
463            return self::HTTP_OK;
464        }
465
466        $errorCodes = [];
467        foreach ($errors as $error) {
468            if (!is_array($error) || !isset($error['statusCode']) || !is_int($error['statusCode'])) {
469                throw new \InvalidArgumentException('Invalid error structure. Each error must have a statusCode.');
470            }
471            $errorCodes[] = $error['statusCode'];
472        }
473
474        return max($errorCodes);
475    }
476}