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