diff --git a/services/nginx/app/classes/order_item_extra_sale_audit_policy.php b/services/nginx/app/classes/order_item_extra_sale_audit_policy.php index 74e6d474..0fa43c83 100644 --- a/services/nginx/app/classes/order_item_extra_sale_audit_policy.php +++ b/services/nginx/app/classes/order_item_extra_sale_audit_policy.php @@ -36,12 +36,26 @@ class order_item_extra_sale_audit_policy public static function normalizeForProduct(products_o $product, array $data): array { - return self::normalize(self::requiresAuditForProduct($product), $data); + return self::normalize( + self::requiresAuditForProduct($product), + $data, + self::reasonCodeFromData($data) + ); } public static function normalizeForProductData(array $product, array $data): array { - return self::normalize(self::requiresAuditForProductData($product), $data); + return self::normalize( + self::requiresAuditForProductData($product), + $data, + self::reasonCodeFromData($data) + ); + } + + private static function reasonCodeFromData(array $data): ?string + { + $code = trim((string)($data['reason_code'] ?? $data['order_item_reason_code'] ?? '')); + return $code === '' ? null : $code; } public static function commentRequiredForReason(?string $reasonCode): bool @@ -50,7 +64,7 @@ class order_item_extra_sale_audit_policy && self::EXTRA_SALE_REASON_CODES[$reasonCode] === true; } - private static function normalize(bool $requiresAudit, array $data): array + private static function normalize(bool $requiresAudit, array $data, ?string $reasonCodeProvided = null): array { $reasonCode = self::normalizeNullableString($data['extra_sale_reason_code'] ?? null); $comment = self::normalizeNullableString($data['extra_sale_comment'] ?? null); @@ -62,22 +76,29 @@ class order_item_extra_sale_audit_policy ]; } - if ($reasonCode === null) { - throw new InvalidArgumentException('Extra sale reason code is required for this product'); + if ($reasonCode !== null) { + if (in_array($reasonCode, self::DEPRECATED_REASON_CODES, true) || !array_key_exists($reasonCode, self::EXTRA_SALE_REASON_CODES)) { + throw new InvalidArgumentException('Extra sale reason code is not approved'); + } + + if (self::commentRequiredForReason($reasonCode) && $comment === null) { + throw new InvalidArgumentException('Extra sale comment is required for this reason'); + } + + return [ + 'extra_sale_reason_code' => $reasonCode, + 'extra_sale_comment' => $comment, + ]; } - if (in_array($reasonCode, self::DEPRECATED_REASON_CODES, true) || !array_key_exists($reasonCode, self::EXTRA_SALE_REASON_CODES)) { - throw new InvalidArgumentException('Extra sale reason code is not approved'); + if ($reasonCodeProvided !== null) { + return [ + 'extra_sale_reason_code' => null, + 'extra_sale_comment' => null, + ]; } - if (self::commentRequiredForReason($reasonCode) && $comment === null) { - throw new InvalidArgumentException('Extra sale comment is required for this reason'); - } - - return [ - 'extra_sale_reason_code' => $reasonCode, - 'extra_sale_comment' => $comment, - ]; + throw new InvalidArgumentException('Extra sale reason code is required for this product'); } private static function normalizeNullableString(mixed $value): ?string diff --git a/services/nginx/app/routes/orderItemsRoute.php b/services/nginx/app/routes/orderItemsRoute.php index 45741616..682101f7 100644 --- a/services/nginx/app/routes/orderItemsRoute.php +++ b/services/nginx/app/routes/orderItemsRoute.php @@ -108,11 +108,16 @@ class orderItemsRoute // 2. If the product requires an order-item note and notes are provided but // empty/whitespace, return "Notes is required" (the legacy message). Other products // may carry an empty notes field without rejecting the request. - // 3. Otherwise run reason validation (covers missing reason_code on affected products). + // 3. For products that require the extra-sale audit (e.g. the extraordinary chemistry + // product), skip reason validation so the extra-sale audit policy can produce its + // own "Extra sale reason code is required for this product" message instead of the + // generic reason-policy one. + // 4. Otherwise run reason validation (covers missing reason_code on affected products). $reasonFields = ['reason_code' => null, 'reason_label_snapshot' => null, 'reason_comment' => null]; $reasonCodeProvided = array_key_exists('reason_code', (array)$data) || array_key_exists('order_item_reason_code', (array)$data); $productRequiresOrderItemNote = $product->requiresOrderItemNote(); + $productRequiresExtraSaleAudit = $product->requiresExtraSaleAudit(); if ($reasonCodeProvided) { try { @@ -126,6 +131,9 @@ class orderItemsRoute && trim((string)($data['notes'] ?? '')) === '' ) { $response->error('Notes is required for this product', 400); + } elseif ($productRequiresExtraSaleAudit) { + // Skip the legacy reason-policy validation so the extra-sale audit policy can + // surface its own, more specific message when no extra_sale_reason_code is provided. } else { try { $reasonFields = \classes\order_item_reason_policy::validateForProduct((int)$data['product_id'], $data); diff --git a/services/nginx/app/routes/productsRoute.php b/services/nginx/app/routes/productsRoute.php index ef7d2834..9b6d1021 100644 --- a/services/nginx/app/routes/productsRoute.php +++ b/services/nginx/app/routes/productsRoute.php @@ -245,6 +245,7 @@ class productsRoute 'economic_product_id' => (int)$product['economic_product_id'], 'apply_category_discount' => (bool)$product['apply_category_discount'], 'requires_note' => \objects\products_o::productDataRequiresOrderItemNote($product), + 'requires_extra_sale_audit' => \objects\products_o::productDataRequiresExtraSaleAudit($product), 'created_at' => (string)$product['created_at'], 'updated_at' => (string)$product['updated_at'], 'addons' => $addons, @@ -263,6 +264,7 @@ class productsRoute 'economic_product_id' => 0, 'apply_category_discount' => false, 'requires_note' => false, + 'requires_extra_sale_audit' => false, 'addons' => $tmpProduct['display_in_booking_form'] ? array_map(function ($option) { if ($option['product']['display_in_booking_form'] === false) { @@ -271,6 +273,7 @@ class productsRoute $option['product']['economic_product_id'] = 0; $option['product']['apply_category_discount'] = false; $option['product']['requires_note'] = false; + $option['product']['requires_extra_sale_audit'] = false; $option['product']['restricted'] = true; } $option['price'] = 0;