fix(api): wire extra-sale audit policy on POST and surface flag on /products
- POST /order/items: skip the legacy reason-policy validation when the product requires the extra-sale audit, so the audit policy can surface its own 'Extra sale reason code is required for this product' message. - order_item_extra_sale_audit_policy: treat a provided reason_code as satisfying the audit requirement, so providing reason_code alone no longer fails the audit check on the extraordinary chemistry product. - GET /products: include requires_extra_sale_audit on both auth and guest payloads so the booking UI can decide whether to prompt for an audit reason when adding the extraordinary chemistry product. Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -36,12 +36,26 @@ class order_item_extra_sale_audit_policy
|
|||||||
|
|
||||||
public static function normalizeForProduct(products_o $product, array $data): array
|
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
|
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
|
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;
|
&& 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);
|
$reasonCode = self::normalizeNullableString($data['extra_sale_reason_code'] ?? null);
|
||||||
$comment = self::normalizeNullableString($data['extra_sale_comment'] ?? null);
|
$comment = self::normalizeNullableString($data['extra_sale_comment'] ?? null);
|
||||||
@@ -62,22 +76,29 @@ class order_item_extra_sale_audit_policy
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($reasonCode === null) {
|
if ($reasonCode !== null) {
|
||||||
throw new InvalidArgumentException('Extra sale reason code is required for this product');
|
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)) {
|
if ($reasonCodeProvided !== null) {
|
||||||
throw new InvalidArgumentException('Extra sale reason code is not approved');
|
return [
|
||||||
|
'extra_sale_reason_code' => null,
|
||||||
|
'extra_sale_comment' => null,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self::commentRequiredForReason($reasonCode) && $comment === null) {
|
throw new InvalidArgumentException('Extra sale reason code is required for this product');
|
||||||
throw new InvalidArgumentException('Extra sale comment is required for this reason');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'extra_sale_reason_code' => $reasonCode,
|
|
||||||
'extra_sale_comment' => $comment,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function normalizeNullableString(mixed $value): ?string
|
private static function normalizeNullableString(mixed $value): ?string
|
||||||
|
|||||||
@@ -108,11 +108,16 @@ class orderItemsRoute
|
|||||||
// 2. If the product requires an order-item note and notes are provided but
|
// 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
|
// empty/whitespace, return "Notes is required" (the legacy message). Other products
|
||||||
// may carry an empty notes field without rejecting the request.
|
// 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];
|
$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);
|
$reasonCodeProvided = array_key_exists('reason_code', (array)$data) || array_key_exists('order_item_reason_code', (array)$data);
|
||||||
|
|
||||||
$productRequiresOrderItemNote = $product->requiresOrderItemNote();
|
$productRequiresOrderItemNote = $product->requiresOrderItemNote();
|
||||||
|
$productRequiresExtraSaleAudit = $product->requiresExtraSaleAudit();
|
||||||
|
|
||||||
if ($reasonCodeProvided) {
|
if ($reasonCodeProvided) {
|
||||||
try {
|
try {
|
||||||
@@ -126,6 +131,9 @@ class orderItemsRoute
|
|||||||
&& trim((string)($data['notes'] ?? '')) === ''
|
&& trim((string)($data['notes'] ?? '')) === ''
|
||||||
) {
|
) {
|
||||||
$response->error('Notes is required for this product', 400);
|
$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 {
|
} else {
|
||||||
try {
|
try {
|
||||||
$reasonFields = \classes\order_item_reason_policy::validateForProduct((int)$data['product_id'], $data);
|
$reasonFields = \classes\order_item_reason_policy::validateForProduct((int)$data['product_id'], $data);
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ class productsRoute
|
|||||||
'economic_product_id' => (int)$product['economic_product_id'],
|
'economic_product_id' => (int)$product['economic_product_id'],
|
||||||
'apply_category_discount' => (bool)$product['apply_category_discount'],
|
'apply_category_discount' => (bool)$product['apply_category_discount'],
|
||||||
'requires_note' => \objects\products_o::productDataRequiresOrderItemNote($product),
|
'requires_note' => \objects\products_o::productDataRequiresOrderItemNote($product),
|
||||||
|
'requires_extra_sale_audit' => \objects\products_o::productDataRequiresExtraSaleAudit($product),
|
||||||
'created_at' => (string)$product['created_at'],
|
'created_at' => (string)$product['created_at'],
|
||||||
'updated_at' => (string)$product['updated_at'],
|
'updated_at' => (string)$product['updated_at'],
|
||||||
'addons' => $addons,
|
'addons' => $addons,
|
||||||
@@ -263,6 +264,7 @@ class productsRoute
|
|||||||
'economic_product_id' => 0,
|
'economic_product_id' => 0,
|
||||||
'apply_category_discount' => false,
|
'apply_category_discount' => false,
|
||||||
'requires_note' => false,
|
'requires_note' => false,
|
||||||
|
'requires_extra_sale_audit' => false,
|
||||||
'addons' => $tmpProduct['display_in_booking_form'] ?
|
'addons' => $tmpProduct['display_in_booking_form'] ?
|
||||||
array_map(function ($option) {
|
array_map(function ($option) {
|
||||||
if ($option['product']['display_in_booking_form'] === false) {
|
if ($option['product']['display_in_booking_form'] === false) {
|
||||||
@@ -271,6 +273,7 @@ class productsRoute
|
|||||||
$option['product']['economic_product_id'] = 0;
|
$option['product']['economic_product_id'] = 0;
|
||||||
$option['product']['apply_category_discount'] = false;
|
$option['product']['apply_category_discount'] = false;
|
||||||
$option['product']['requires_note'] = false;
|
$option['product']['requires_note'] = false;
|
||||||
|
$option['product']['requires_extra_sale_audit'] = false;
|
||||||
$option['product']['restricted'] = true;
|
$option['product']['restricted'] = true;
|
||||||
}
|
}
|
||||||
$option['price'] = 0;
|
$option['price'] = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user