Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
45.61% covered (danger)
45.61%
130 / 285
45.00% covered (danger)
45.00%
9 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComebackCashPosApiController
45.61% covered (danger)
45.61%
130 / 285
45.00% covered (danger)
45.00%
9 / 20
1453.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getSettings
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 issueCoupon
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
110
 validateCoupon
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 redeemCoupon
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
90
 validateApiKey
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 sendUnauthorizedResponse
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 setJsonContentType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendJsonResponse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sendErrorResponse
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getJsonRequestBody
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 formatSideSettings
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 generateSettingsVersion
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 formatCouponForResponse
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 formatDateTime
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 sanitizePhone
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
 validateIssueCouponRequest
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
11
 validateRedemptionRequest
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
11
 mapErrorCodeToHttpStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 mapRedemptionErrorToHttpStatus
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3namespace BuyerKiosk\ComebackCash\Controllers;
4
5use Exception;
6use BuyerKiosk\ComebackCash\ComebackCashFactory;
7use BuyerKiosk\ComebackCash\Services\EventService;
8use BuyerKiosk\ComebackCash\Services\CouponService;
9use BuyerKiosk\ComebackCash\Services\RedemptionService;
10use BuyerKiosk\ComebackCash\Models\Event;
11
12/**
13 * ComebackCashPosApiController - REST API for external POS system integration
14 *
15 * Provides endpoints for Point-of-Sale systems to:
16 * 1. Fetch active event settings (GET /api/{typeNum}/comeback-cash/settings)
17 * 2. Issue coupons for qualifying transactions (POST /api/{typeNum}/comeback-cash/coupons)
18 * 3. Validate coupon codes (GET /api/{typeNum}/comeback-cash/coupons/{code})
19 * 4. Process coupon redemptions (POST /api/{typeNum}/comeback-cash/redeem)
20 *
21 * Authentication: X-API-Key header validated against store's API key
22 *
23 * @see SDD Section 6.2.1 - POS Integration API Specification
24 * @package BuyerKiosk\ComebackCash\Controllers
25 */
26class ComebackCashPosApiController
27{
28    /**
29     * @var \Slim\Slim Slim application instance
30     */
31    private $app;
32
33    /**
34     * @var \Store Store object
35     */
36    private $store;
37
38    /**
39     * @var ComebackCashFactory Factory for creating services
40     */
41    private ComebackCashFactory $factory;
42
43    /**
44     * Constructor
45     *
46     * @param \Slim\Slim $app Slim application instance
47     * @param \Store $store Store object (validated)
48     */
49    public function __construct($app, \Store $store)
50    {
51        $this->app = $app;
52        $this->store = $store;
53        $this->factory = new ComebackCashFactory($store->getTypeNum());
54    }
55
56    // =========================================================================
57    // PUBLIC ENDPOINT METHODS
58    // =========================================================================
59
60    /**
61     * GET /api/{typeNum}/comeback-cash/settings
62     *
63     * Fetch active event settings for both buy and sales sides.
64     * POS systems use this to display current promotion configuration
65     * and calculate rewards locally.
66     *
67     * Response includes a version hash (MD5) for cache invalidation.
68     *
69     * @see SDD Section 6.2.1.1 - Settings Endpoint
70     */
71    public function getSettings(): void
72    {
73        $this->setJsonContentType();
74
75        try {
76            $eventService = $this->factory->createEventService();
77
78            // Get active events for both sides
79            $buyEvent = $eventService->getActiveEventBySide(Event::SIDE_BUY);
80            $salesEvent = $eventService->getActiveEventBySide(Event::SIDE_SALES);
81
82            // Build response structure per SDD specification
83            $response = [
84                'success' => true,
85                'buy_side' => $this->formatSideSettings($buyEvent),
86                'sales_side' => $this->formatSideSettings($salesEvent),
87                'version' => $this->generateSettingsVersion($buyEvent, $salesEvent),
88            ];
89
90            $this->sendJsonResponse($response);
91
92        } catch (Exception $e) {
93            error_log("ComebackCashPosApiController::getSettings error: " . $e->getMessage());
94            $this->sendErrorResponse('Failed to retrieve settings', 500);
95        }
96    }
97
98    /**
99     * POST /api/{typeNum}/comeback-cash/coupons
100     *
101     * Issue a coupon for a qualifying transaction.
102     * Idempotent: duplicate transaction_ids return the existing coupon.
103     *
104     * Request Body:
105     * - side: "buy" or "sales" (required)
106     * - transaction_id: string, max 50 chars (required)
107     * - transaction_amount: decimal (required)
108     * - customer_phone: E.164 format (optional)
109     * - customer_name: string, max 100 chars (optional)
110     * - employee_id: int (optional)
111     *
112     * @see SDD Section 6.2.1.2 - Issue Coupon Endpoint
113     */
114    public function issueCoupon(): void
115    {
116        $this->setJsonContentType();
117
118        try {
119            // Parse request body
120            $data = $this->getJsonRequestBody();
121            if ($data === null) {
122                $this->sendErrorResponse('Invalid JSON in request body', 400);
123                return;
124            }
125
126            // Validate required fields
127            $validation = $this->validateIssueCouponRequest($data);
128            if ($validation !== null) {
129                $this->sendErrorResponse($validation['error'], $validation['status']);
130                return;
131            }
132
133            // Extract and sanitize parameters
134            $side = $data['side'];
135            $transactionId = substr(trim($data['transaction_id']), 0, 50);
136            $transactionAmount = (float) $data['transaction_amount'];
137            $customerPhone = isset($data['customer_phone']) ? $this->sanitizePhone($data['customer_phone']) : null;
138            $customerName = isset($data['customer_name']) ? substr(trim($data['customer_name']), 0, 100) : null;
139            $employeeId = isset($data['employee_id']) ? (int) $data['employee_id'] : null;
140
141            // Issue the coupon via service
142            $couponService = $this->factory->createCouponService();
143            $result = $couponService->issueCoupon(
144                $side,
145                $transactionId,
146                $transactionAmount,
147                $customerPhone,
148                $customerName,
149                $employeeId
150            );
151
152            // Transform service response to API response format
153            if ($result['success']) {
154                $response = [
155                    'success' => true,
156                    'coupon' => $this->formatCouponForResponse($result['coupon'] ?? null),
157                ];
158
159                // Include reason if no coupon was issued
160                if (isset($result['reason'])) {
161                    $response['reason'] = $result['reason'];
162                }
163
164                $this->app->response->setStatus(isset($result['coupon']) ? 201 : 200);
165                $this->sendJsonResponse($response);
166            } else {
167                // Map service error codes to HTTP status codes
168                $httpStatus = $this->mapErrorCodeToHttpStatus($result['code'] ?? 'UNKNOWN');
169                $this->sendErrorResponse($result['error'], $httpStatus, $result['code'] ?? null);
170            }
171
172        } catch (Exception $e) {
173            error_log("ComebackCashPosApiController::issueCoupon error: " . $e->getMessage());
174            $this->sendErrorResponse('Failed to issue coupon', 500);
175        }
176    }
177
178    /**
179     * GET /api/{typeNum}/comeback-cash/coupons/{code}
180     *
181     * Validate a coupon code and retrieve its details.
182     * Code lookup is case-insensitive and ignores hyphens.
183     *
184     * @param string $code The coupon code to validate
185     *
186     * @see SDD Section 6.2.1.3 - Validate Coupon Endpoint
187     */
188    public function validateCoupon(string $code): void
189    {
190        $this->setJsonContentType();
191
192        try {
193            // Validate code format
194            if (empty(trim($code))) {
195                $this->sendErrorResponse('Coupon code is required', 400);
196                return;
197            }
198
199            $couponService = $this->factory->createCouponService();
200            $result = $couponService->validateCoupon($code);
201
202            // Format response per SDD specification
203            $response = [
204                'valid' => $result['valid'],
205            ];
206
207            // Include coupon details if found
208            if (isset($result['coupon'])) {
209                $response['coupon'] = [
210                    'code' => $result['coupon']['code'],
211                    'value' => $result['coupon']['value'],
212                    'status' => $result['coupon']['status'],
213                    'expires_at' => $this->formatDateTime($result['coupon']['expires_at']),
214                    'event_name' => $result['coupon']['event_name'] ?? null,
215                    'redemption_min_purchase' => $result['coupon']['redemption_min_purchase'] ?? null,
216                ];
217            }
218
219            // Include reason if not valid
220            if (!$result['valid'] && isset($result['reason'])) {
221                $response['reason'] = $result['reason'];
222            }
223
224            // Set appropriate status code based on validation result
225            $httpStatus = $result['valid'] ? 200 : ($result['status'] === 'not_found' ? 404 : 200);
226            $this->app->response->setStatus($httpStatus);
227            $this->sendJsonResponse($response);
228
229        } catch (Exception $e) {
230            error_log("ComebackCashPosApiController::validateCoupon error: " . $e->getMessage());
231            $this->sendErrorResponse('Failed to validate coupon', 500);
232        }
233    }
234
235    /**
236     * POST /api/{typeNum}/comeback-cash/redeem
237     *
238     * Process a coupon redemption against a transaction.
239     * Supports partial redemption via amount_to_redeem parameter.
240     *
241     * Request Body:
242     * - code: string (required)
243     * - transaction_id: string (required)
244     * - transaction_amount: decimal (required)
245     * - employee_id: int (optional, defaults to 0)
246     * - redemption_method: string (optional, defaults to "pos")
247     * - amount_to_redeem: decimal (optional, defaults to full coupon value)
248     *
249     * @see SDD Section 6.2.1.4 - Redeem Coupon Endpoint
250     */
251    public function redeemCoupon(): void
252    {
253        $this->setJsonContentType();
254
255        try {
256            // Parse request body
257            $data = $this->getJsonRequestBody();
258            if ($data === null) {
259                $this->sendErrorResponse('Invalid JSON in request body', 400);
260                return;
261            }
262
263            // Validate required fields
264            $validation = $this->validateRedemptionRequest($data);
265            if ($validation !== null) {
266                $this->sendErrorResponse($validation['error'], $validation['status']);
267                return;
268            }
269
270            // Extract parameters
271            $code = trim($data['code']);
272            $transactionId = substr(trim($data['transaction_id']), 0, 50);
273            $transactionAmount = (float) $data['transaction_amount'];
274            $employeeId = isset($data['employee_id']) ? (int) $data['employee_id'] : 0;
275            $redemptionMethod = isset($data['redemption_method']) ? trim($data['redemption_method']) : 'pos';
276            $amountToRedeem = isset($data['amount_to_redeem']) ? (float) $data['amount_to_redeem'] : null;
277
278            // Process redemption via service
279            $redemptionService = $this->factory->createRedemptionService();
280            $result = $redemptionService->redeemCoupon(
281                $code,
282                $transactionId,
283                $transactionAmount,
284                $employeeId,
285                $redemptionMethod,
286                $amountToRedeem
287            );
288
289            // Format response
290            if ($result['success']) {
291                $response = [
292                    'success' => true,
293                    'redeemed_amount' => $result['redeemed_amount'],
294                    'remaining_value' => $result['remaining_value'],
295                    'coupon_status' => $result['coupon_status'],
296                ];
297
298                $this->sendJsonResponse($response);
299            } else {
300                // Map service error codes to HTTP status codes
301                $httpStatus = $this->mapRedemptionErrorToHttpStatus($result['code'] ?? 'UNKNOWN');
302
303                $response = [
304                    'success' => false,
305                    'error' => $result['error'],
306                    'code' => $result['code'] ?? null,
307                ];
308
309                // Include minimum required for MIN_PURCHASE_NOT_MET errors
310                if (isset($result['minimum_required'])) {
311                    $response['minimum_required'] = $result['minimum_required'];
312                }
313
314                $this->app->response->setStatus($httpStatus);
315                $this->sendJsonResponse($response);
316            }
317
318        } catch (Exception $e) {
319            error_log("ComebackCashPosApiController::redeemCoupon error: " . $e->getMessage());
320            $this->sendErrorResponse('Failed to process redemption', 500);
321        }
322    }
323
324    // =========================================================================
325    // STATIC API KEY VALIDATION
326    // =========================================================================
327
328    /**
329     * Validate X-API-Key header against store's API key
330     *
331     * This method is called before controller instantiation in the route.
332     * Uses the existing validateAPIKey function from BaseModel.
333     *
334     * @param \Slim\Slim $app Slim application instance
335     * @param string $typeNum Store identifier
336     * @return \Store|null Valid store object or null if authentication fails
337     */
338    public static function validateApiKey($app, string $typeNum): ?\Store
339    {
340        // Get API key from X-API-Key header
341        $apiKey = $app->request->headers->get('X-API-Key');
342
343        if (empty($apiKey)) {
344            return null;
345        }
346
347        // Use existing validation function from BaseModel
348        // validateAPIKey returns Store object on success, null on failure
349        return validateAPIKey($apiKey, $typeNum);
350    }
351
352    /**
353     * Send unauthorized response for API key validation failures
354     *
355     * @param \Slim\Slim $app Slim application instance
356     * @param string $message Error message
357     */
358    public static function sendUnauthorizedResponse($app, string $message = 'Invalid or missing API key'): void
359    {
360        $app->response->headers->set('Content-Type', 'application/json');
361        $app->response->setStatus(401);
362        $app->response->setBody(json_encode([
363            'success' => false,
364            'error' => $message,
365            'code' => 'UNAUTHORIZED',
366        ]));
367    }
368
369    // =========================================================================
370    // PRIVATE HELPER METHODS
371    // =========================================================================
372
373    /**
374     * Set JSON content type header
375     */
376    private function setJsonContentType(): void
377    {
378        $this->app->response->headers->set('Content-Type', 'application/json');
379    }
380
381    /**
382     * Send JSON response body
383     *
384     * @param array $data Response data
385     */
386    private function sendJsonResponse(array $data): void
387    {
388        $this->app->response->setBody(json_encode($data));
389    }
390
391    /**
392     * Send error response
393     *
394     * @param string $message Error message
395     * @param int $httpStatus HTTP status code
396     * @param string|null $errorCode Application error code
397     */
398    private function sendErrorResponse(string $message, int $httpStatus, ?string $errorCode = null): void
399    {
400        $response = [
401            'success' => false,
402            'error' => $message,
403        ];
404
405        if ($errorCode !== null) {
406            $response['code'] = $errorCode;
407        }
408
409        $this->app->response->setStatus($httpStatus);
410        $this->sendJsonResponse($response);
411    }
412
413    /**
414     * Parse JSON request body
415     *
416     * @return array|null Parsed data or null on error
417     */
418    private function getJsonRequestBody(): ?array
419    {
420        $body = $this->app->request->getBody();
421
422        if (empty($body)) {
423            // Try POST parameters as fallback
424            $post = $this->app->request->post();
425            if (!empty($post)) {
426                return $post;
427            }
428            return null;
429        }
430
431        $data = json_decode($body, true);
432
433        if (json_last_error() !== JSON_ERROR_NONE) {
434            return null;
435        }
436
437        return $data;
438    }
439
440    /**
441     * Format side settings for API response
442     *
443     * @param Event|null $event Active event for the side
444     * @return array Formatted side settings
445     */
446    private function formatSideSettings(?Event $event): array
447    {
448        if ($event === null) {
449            return [
450                'active' => false,
451                'event_id' => null,
452                'event_name' => null,
453                'earning_type' => null,
454                'earning_tiers' => null,
455                'earning_flat_amount' => null,
456                'earning_percentage' => null,
457                'redemption_min_purchase' => null,
458                'allow_double_up' => false,
459            ];
460        }
461
462        return [
463            'active' => true,
464            'event_id' => $event->id,
465            'event_name' => $event->name,
466            'earning_type' => $event->earningType,
467            'earning_tiers' => $event->earningTiers,
468            'earning_flat_amount' => $event->earningFlatAmount,
469            'earning_percentage' => $event->earningPercentage,
470            'redemption_min_purchase' => $event->redemptionMinPurchase,
471            'allow_double_up' => $event->allowDoubleUp,
472        ];
473    }
474
475    /**
476     * Generate settings version hash for cache invalidation
477     *
478     * @param Event|null $buyEvent Buy-side event
479     * @param Event|null $salesEvent Sales-side event
480     * @return string MD5 hash of settings
481     */
482    private function generateSettingsVersion(?Event $buyEvent, ?Event $salesEvent): string
483    {
484        $data = [
485            'buy' => $buyEvent ? $buyEvent->toArray() : null,
486            'sales' => $salesEvent ? $salesEvent->toArray() : null,
487        ];
488
489        return md5(json_encode($data));
490    }
491
492    /**
493     * Format coupon data for API response
494     *
495     * @param array|null $coupon Coupon data from service
496     * @return array|null Formatted coupon or null
497     */
498    private function formatCouponForResponse(?array $coupon): ?array
499    {
500        if ($coupon === null) {
501            return null;
502        }
503
504        return [
505            'code' => $coupon['display_code'] ?? $coupon['code'],
506            'value' => (float) $coupon['value'],
507            'expires_at' => $this->formatDateTime($coupon['expires_at']),
508            'event_name' => $coupon['event_name'] ?? null,
509        ];
510    }
511
512    /**
513     * Format datetime string to ISO8601
514     *
515     * @param string|null $datetime Datetime string
516     * @return string|null ISO8601 formatted datetime
517     */
518    private function formatDateTime(?string $datetime): ?string
519    {
520        if ($datetime === null) {
521            return null;
522        }
523
524        try {
525            $dt = new \DateTime($datetime);
526            return $dt->format('c'); // ISO8601 format
527        } catch (Exception $e) {
528            return $datetime;
529        }
530    }
531
532    /**
533     * Sanitize phone number to E.164 format
534     *
535     * @param string $phone Raw phone input
536     * @return string|null Sanitized phone or null if invalid
537     */
538    private function sanitizePhone(string $phone): ?string
539    {
540        // Remove all non-numeric characters except leading +
541        $phone = trim($phone);
542
543        if (empty($phone)) {
544            return null;
545        }
546
547        // If starts with +, keep it
548        $hasPlus = strpos($phone, '+') === 0;
549        $digits = preg_replace('/[^0-9]/', '', $phone);
550
551        // Must have at least 10 digits for US number
552        if (strlen($digits) < 10) {
553            return null;
554        }
555
556        // Format as E.164 for US numbers
557        if (strlen($digits) === 10) {
558            return '+1' . $digits;
559        } elseif (strlen($digits) === 11 && $digits[0] === '1') {
560            return '+' . $digits;
561        } elseif ($hasPlus) {
562            return '+' . $digits;
563        }
564
565        return '+' . $digits;
566    }
567
568    /**
569     * Validate issue coupon request data
570     *
571     * @param array $data Request data
572     * @return array|null Validation error or null if valid
573     */
574    private function validateIssueCouponRequest(array $data): ?array
575    {
576        // Required: side
577        if (!isset($data['side']) || !in_array($data['side'], ['buy', 'sales'], true)) {
578            return [
579                'error' => 'side is required and must be "buy" or "sales"',
580                'status' => 400,
581            ];
582        }
583
584        // Required: transaction_id
585        if (!isset($data['transaction_id']) || empty(trim($data['transaction_id']))) {
586            return [
587                'error' => 'transaction_id is required',
588                'status' => 400,
589            ];
590        }
591
592        if (strlen($data['transaction_id']) > 50) {
593            return [
594                'error' => 'transaction_id exceeds maximum length of 50 characters',
595                'status' => 400,
596            ];
597        }
598
599        // Required: transaction_amount
600        if (!isset($data['transaction_amount'])) {
601            return [
602                'error' => 'transaction_amount is required',
603                'status' => 400,
604            ];
605        }
606
607        if (!is_numeric($data['transaction_amount']) || (float) $data['transaction_amount'] < 0) {
608            return [
609                'error' => 'transaction_amount must be a non-negative number',
610                'status' => 400,
611            ];
612        }
613
614        // Optional: customer_name length
615        if (isset($data['customer_name']) && strlen($data['customer_name']) > 100) {
616            return [
617                'error' => 'customer_name exceeds maximum length of 100 characters',
618                'status' => 400,
619            ];
620        }
621
622        return null;
623    }
624
625    /**
626     * Validate redemption request data
627     *
628     * @param array $data Request data
629     * @return array|null Validation error or null if valid
630     */
631    private function validateRedemptionRequest(array $data): ?array
632    {
633        // Required: code
634        if (!isset($data['code']) || empty(trim($data['code']))) {
635            return [
636                'error' => 'code is required',
637                'status' => 400,
638            ];
639        }
640
641        // Required: transaction_id
642        if (!isset($data['transaction_id']) || empty(trim($data['transaction_id']))) {
643            return [
644                'error' => 'transaction_id is required',
645                'status' => 400,
646            ];
647        }
648
649        // Required: transaction_amount
650        if (!isset($data['transaction_amount'])) {
651            return [
652                'error' => 'transaction_amount is required',
653                'status' => 400,
654            ];
655        }
656
657        if (!is_numeric($data['transaction_amount']) || (float) $data['transaction_amount'] < 0) {
658            return [
659                'error' => 'transaction_amount must be a non-negative number',
660                'status' => 400,
661            ];
662        }
663
664        // Optional: amount_to_redeem validation
665        if (isset($data['amount_to_redeem'])) {
666            if (!is_numeric($data['amount_to_redeem']) || (float) $data['amount_to_redeem'] <= 0) {
667                return [
668                    'error' => 'amount_to_redeem must be a positive number',
669                    'status' => 400,
670                ];
671            }
672        }
673
674        return null;
675    }
676
677    /**
678     * Map coupon service error codes to HTTP status codes
679     *
680     * @param string $errorCode Service error code
681     * @return int HTTP status code
682     */
683    private function mapErrorCodeToHttpStatus(string $errorCode): int
684    {
685        return match ($errorCode) {
686            'INVALID_SIDE', 'INVALID_AMOUNT' => 400,
687            'NO_ACTIVE_EVENT' => 404,
688            default => 500,
689        };
690    }
691
692    /**
693     * Map redemption service error codes to HTTP status codes
694     *
695     * Error codes per SDD lines 1098-1104:
696     * - INVALID_CODE (404): Coupon code not found
697     * - ALREADY_REDEEMED (409): Coupon already fully redeemed
698     * - EXPIRED (410): Coupon has expired
699     * - MIN_PURCHASE_NOT_MET (422): Transaction below minimum
700     *
701     * @param string $errorCode Service error code
702     * @return int HTTP status code
703     */
704    private function mapRedemptionErrorToHttpStatus(string $errorCode): int
705    {
706        return match ($errorCode) {
707            'INVALID_CODE' => 404,
708            'ALREADY_REDEEMED' => 409,
709            'EXPIRED' => 410,
710            'MIN_PURCHASE_NOT_MET' => 422,
711            'INVALID_AMOUNT' => 400,
712            'DATABASE_ERROR', 'REDEMPTION_ERROR' => 500,
713            default => 400,
714        };
715    }
716}