Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.67% |
13 / 15 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
ErrorMessages | |
86.67% |
13 / 15 |
|
50.00% |
1 / 2 |
8.15 | |
0.00% |
0 / 1 |
get | |
100.00% |
7 / 7 |
|
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 | ], |
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 | } |