354 lines
13 KiB
PHP
354 lines
13 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
use objects\department_customer_price_overrides_o;
|
|
use objects\departments_o;
|
|
use objects\products_o;
|
|
use objects\users_o;
|
|
|
|
class department_customer_pricing_service
|
|
{
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getPricing(int $departmentId, int $userId): array
|
|
{
|
|
$department = $this->department($departmentId);
|
|
$customer = $this->customer($userId);
|
|
$this->assertEnabled($department);
|
|
|
|
$overrides = (new department_customer_price_overrides_o())->getAllPrices($departmentId, $userId);
|
|
|
|
return [
|
|
'department' => $department,
|
|
'customer' => $customer,
|
|
'overrides' => $overrides,
|
|
'categories' => $this->catalog($departmentId, $customer['id']),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function updatePricing(int $departmentId, int $userId, array $payload): array
|
|
{
|
|
$department = $this->department($departmentId);
|
|
$customer = $this->customer($userId);
|
|
$this->assertEnabled($department);
|
|
|
|
if (array_key_exists('department_id', $payload) && (int)$payload['department_id'] !== $departmentId) {
|
|
throw new limited_backoffice_exception('Department ID in body does not match the route.', 400);
|
|
}
|
|
if (array_key_exists('user_id', $payload) && (int)$payload['user_id'] !== $userId) {
|
|
throw new limited_backoffice_exception('User ID in body does not match the route.', 400);
|
|
}
|
|
|
|
$overrides = $payload['overrides'] ?? null;
|
|
if (!is_array($overrides)) {
|
|
throw new limited_backoffice_exception('Overrides are required.', 400);
|
|
}
|
|
|
|
$normalized = $this->normalizeOverrides($departmentId, $overrides);
|
|
$overrideObject = new department_customer_price_overrides_o();
|
|
$existingOverrides = $overrideObject->getAllPrices($departmentId, $customer['id']);
|
|
$normalizedKeys = [];
|
|
foreach ($normalized as $override) {
|
|
$normalizedKeys[$this->overrideKey((bool)$override['is_category'], $override['product_or_category_id'])] = true;
|
|
}
|
|
|
|
global $db;
|
|
$db->conn()->begin_transaction();
|
|
try {
|
|
$db->query(
|
|
'DELETE FROM `department_customer_price_overrides` WHERE `department_id` = '
|
|
. (int)$departmentId . ' AND `user_id` = ' . (int)$customer['id']
|
|
);
|
|
|
|
foreach ($normalized as $override) {
|
|
$overrideObject->setPrice(
|
|
$departmentId,
|
|
$customer['id'],
|
|
(bool)$override['is_category'],
|
|
$override['product_or_category_id'],
|
|
(int)$override['percentage'],
|
|
$override['fixed_price']
|
|
);
|
|
}
|
|
|
|
$db->conn()->commit();
|
|
} catch (\Throwable) {
|
|
$db->conn()->rollback();
|
|
throw new limited_backoffice_exception('Unable to update department customer pricing.', 500);
|
|
}
|
|
|
|
foreach ($normalized as $override) {
|
|
$this->recordVersion($customer, $departmentId, $override);
|
|
}
|
|
|
|
foreach ($existingOverrides as $existingOverride) {
|
|
$key = $this->overrideKey((bool)$existingOverride['is_category'], $existingOverride['product_or_category_id']);
|
|
if (isset($normalizedKeys[$key])) {
|
|
continue;
|
|
}
|
|
|
|
$this->recordVersion($customer, $departmentId, [
|
|
'is_category' => (bool)$existingOverride['is_category'],
|
|
'product_or_category_id' => $existingOverride['product_or_category_id'],
|
|
'percentage' => 0,
|
|
'fixed_price' => null,
|
|
]);
|
|
}
|
|
|
|
return $this->getPricing($departmentId, $customer['id']);
|
|
}
|
|
|
|
/**
|
|
* @return array{id:int,name:string,description:string,custom_pricing_only:bool}
|
|
*/
|
|
private function department(int $departmentId): array
|
|
{
|
|
$department = (new departments_o())->getDepartmentById($departmentId);
|
|
if (!is_array($department) || empty($department)) {
|
|
throw new limited_backoffice_exception('Department not found', 404);
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$department['id'],
|
|
'name' => (string)$department['name'],
|
|
'description' => (string)($department['description'] ?? ''),
|
|
'custom_pricing_only' => (bool)(int)($department['custom_pricing_only'] ?? 0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{id:int,customer_number:int,display_name:string}
|
|
*/
|
|
private function customer(int $userId): array
|
|
{
|
|
$customer = (new users_o())->getUserById($userId);
|
|
if (!$customer->exists()) {
|
|
throw new limited_backoffice_exception('Customer not found', 404);
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$customer->id,
|
|
'customer_number' => (int)$customer->customer_number->value(),
|
|
'display_name' => (string)($customer->display_name->value() ?: ('Customer #' . $customer->customer_number->value())),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $department
|
|
*/
|
|
private function assertEnabled(array $department): void
|
|
{
|
|
if (!($department['custom_pricing_only'] ?? false)) {
|
|
throw new limited_backoffice_exception('Department customer pricing is disabled.', 409, [
|
|
'message' => 'Department customer pricing is disabled.',
|
|
'code' => 'department_customer_pricing_disabled',
|
|
'department' => $department,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
private function catalog(int $departmentId, int $userId): array
|
|
{
|
|
global $db;
|
|
|
|
$sql = "
|
|
SELECT
|
|
c.`id` AS `category_id`,
|
|
c.`name` AS `category_name`,
|
|
c.`description` AS `category_description`,
|
|
p.*,
|
|
pdp.`price` AS `department_price`
|
|
FROM `department_categories` dc
|
|
INNER JOIN `categories` c ON c.`id` = dc.`category_id`
|
|
INNER JOIN `products` p ON p.`category` = dc.`category_id`
|
|
LEFT JOIN `product_department_prices` pdp
|
|
ON pdp.`department_id` = dc.`department_id`
|
|
AND pdp.`product_id` = p.`id`
|
|
WHERE dc.`department_id` = " . (int)$departmentId . "
|
|
AND dc.`deleted_at` IS NULL
|
|
ORDER BY c.`name` ASC, c.`id` ASC, p.`order_priority` ASC, p.`name` ASC, p.`id` ASC";
|
|
|
|
$result = $db->query($sql);
|
|
$rows = $result ? $db->fetch_all($result) : [];
|
|
$customer = (new users_o())->getUserById($userId);
|
|
$categories = [];
|
|
$seen = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$productId = (int)$row['id'];
|
|
if (isset($seen[$productId])) {
|
|
continue;
|
|
}
|
|
$seen[$productId] = true;
|
|
|
|
$categoryId = (int)$row['category_id'];
|
|
if (!isset($categories[$categoryId])) {
|
|
$categories[$categoryId] = [
|
|
'id' => $categoryId,
|
|
'name' => (string)$row['category_name'],
|
|
'description' => (string)($row['category_description'] ?? ''),
|
|
'products' => [],
|
|
];
|
|
}
|
|
|
|
$departmentPrice = $row['department_price'] === null ? null : (int)$row['department_price'];
|
|
$effectivePrice = products_o::CUSTOM_PRICING_MISSING_PRICE;
|
|
if ($departmentPrice !== null) {
|
|
$effectivePrice = $customer->applyProductCustomerPricing($productId, $departmentPrice, true, $departmentId);
|
|
}
|
|
|
|
$categories[$categoryId]['products'][] = [
|
|
'id' => $productId,
|
|
'name' => (string)$row['name'],
|
|
'description' => (string)($row['description'] ?? ''),
|
|
'category' => $categoryId,
|
|
'apply_category_discount' => (bool)$row['apply_category_discount'],
|
|
'base_price' => (int)$row['price'],
|
|
'department_price' => $departmentPrice,
|
|
'effective_price' => $effectivePrice,
|
|
'missing_department_price' => $departmentPrice === null,
|
|
];
|
|
}
|
|
|
|
return array_values($categories);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $overrides
|
|
* @return array<int, array{is_category:bool,product_or_category_id:int|string,percentage:int,fixed_price:int|null}>
|
|
*/
|
|
private function normalizeOverrides(int $departmentId, array $overrides): array
|
|
{
|
|
$normalized = [];
|
|
foreach ($overrides as $override) {
|
|
if (!is_array($override)) {
|
|
throw new limited_backoffice_exception('Invalid override payload.', 400);
|
|
}
|
|
|
|
$isCategory = (bool)($override['is_category'] ?? false);
|
|
$objectId = $override['product_or_category_id'] ?? $override['object_id'] ?? null;
|
|
if ($objectId === null || $objectId === '') {
|
|
throw new limited_backoffice_exception('Override object is required.', 400);
|
|
}
|
|
|
|
$percentage = filter_var($override['discount'] ?? $override['percentage'] ?? 0, FILTER_VALIDATE_INT);
|
|
if ($percentage === false || $percentage < 0 || $percentage > 100) {
|
|
throw new limited_backoffice_exception('Discount must be between 0 and 100.', 400);
|
|
}
|
|
|
|
$fixedPrice = null;
|
|
if (array_key_exists('fixed_price', $override) && $override['fixed_price'] !== null && $override['fixed_price'] !== '') {
|
|
$fixedPrice = filter_var($override['fixed_price'], FILTER_VALIDATE_INT);
|
|
if ($fixedPrice === false || $fixedPrice < 0) {
|
|
throw new limited_backoffice_exception('Fixed price must be zero or more.', 400);
|
|
}
|
|
}
|
|
|
|
if ($isCategory) {
|
|
$fixedPrice = null;
|
|
$objectId = (string)$objectId;
|
|
if ($objectId !== 'global') {
|
|
$this->assertDepartmentCategory($departmentId, $objectId);
|
|
}
|
|
} else {
|
|
$objectId = (int)$objectId;
|
|
$this->assertDepartmentProduct($departmentId, $objectId);
|
|
}
|
|
|
|
if ($percentage <= 0 && $fixedPrice === null) {
|
|
continue;
|
|
}
|
|
|
|
$key = $this->overrideKey($isCategory, $objectId);
|
|
$normalized[$key] = [
|
|
'is_category' => $isCategory,
|
|
'product_or_category_id' => $objectId,
|
|
'percentage' => (int)$percentage,
|
|
'fixed_price' => $fixedPrice === null ? null : (int)$fixedPrice,
|
|
];
|
|
}
|
|
|
|
return array_values($normalized);
|
|
}
|
|
|
|
/**
|
|
* @param array{id:int,customer_number:int,display_name:string} $customer
|
|
* @param array{is_category:bool,product_or_category_id:int|string,percentage:int,fixed_price:int|null} $override
|
|
*/
|
|
private function recordVersion(array $customer, int $departmentId, array $override): void
|
|
{
|
|
try {
|
|
(new economic_v2_versioning_service())->recordDiscountOverrideVersion(
|
|
(int)$customer['id'],
|
|
(int)$customer['customer_number'],
|
|
(bool)$override['is_category'],
|
|
(string)$override['product_or_category_id'],
|
|
(int)$override['percentage'],
|
|
date('Y-m-d H:i:s'),
|
|
'live.department_discount_override.route',
|
|
1.0,
|
|
false,
|
|
[
|
|
'route' => 'department_customer_pricing',
|
|
'department_id' => $departmentId,
|
|
],
|
|
$override['fixed_price'],
|
|
$departmentId
|
|
);
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
|
|
private function overrideKey(bool $isCategory, int|string $objectId): string
|
|
{
|
|
return ((int)$isCategory) . ':' . (string)$objectId;
|
|
}
|
|
|
|
private function assertDepartmentProduct(int $departmentId, int $productId): void
|
|
{
|
|
global $db;
|
|
|
|
$result = $db->query(
|
|
'SELECT p.`id`
|
|
FROM `department_categories` dc
|
|
INNER JOIN `products` p ON p.`category` = dc.`category_id`
|
|
WHERE dc.`department_id` = ' . (int)$departmentId . '
|
|
AND dc.`deleted_at` IS NULL
|
|
AND p.`id` = ' . (int)$productId . '
|
|
LIMIT 1'
|
|
);
|
|
|
|
if (!$result || $result->num_rows < 1) {
|
|
throw new limited_backoffice_exception('Product is not available for this department.', 400);
|
|
}
|
|
}
|
|
|
|
private function assertDepartmentCategory(int $departmentId, string $categoryId): void
|
|
{
|
|
global $db;
|
|
|
|
$categoryId = $db->escape_string($categoryId);
|
|
$result = $db->query(
|
|
"SELECT `id`
|
|
FROM `department_categories`
|
|
WHERE `department_id` = " . (int)$departmentId . "
|
|
AND `deleted_at` IS NULL
|
|
AND `category_id` = '{$categoryId}'
|
|
LIMIT 1"
|
|
);
|
|
|
|
if (!$result || $result->num_rows < 1) {
|
|
throw new limited_backoffice_exception('Category is not available for this department.', 400);
|
|
}
|
|
}
|
|
}
|