Implement department-specific customer pricing functionality
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace classes;
|
||||
|
||||
/**
|
||||
* Ensures additive schema for department-scoped customer price overrides.
|
||||
*/
|
||||
class department_customer_price_overrides_schema_bootstrap
|
||||
{
|
||||
private static bool $initialized = false;
|
||||
|
||||
public static function ensureTables(): void
|
||||
{
|
||||
if (self::$initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $db;
|
||||
|
||||
if (!isset($db) || !is_object($db) || !method_exists($db, 'query')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$db->query(
|
||||
"CREATE TABLE IF NOT EXISTS `department_customer_price_overrides` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`department_id` INT NOT NULL,
|
||||
`user_id` INT NOT NULL,
|
||||
`is_category` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`product_or_category_id` VARCHAR(191) NOT NULL,
|
||||
`percentage` INT NOT NULL DEFAULT 0,
|
||||
`fixed_price` INT NULL DEFAULT NULL,
|
||||
`created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_department_customer_price_overrides_lookup` (`department_id`, `user_id`, `is_category`, `product_or_category_id`),
|
||||
KEY `idx_department_customer_price_overrides_department` (`department_id`),
|
||||
KEY `idx_department_customer_price_overrides_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ class economic_v2_distribution_service
|
||||
|
||||
public function __construct(?economic_v2_versioning_service $versioning = null, ?economic $economic = null)
|
||||
{
|
||||
department_customer_price_overrides_schema_bootstrap::ensureTables();
|
||||
$this->versioning = $versioning ?? new economic_v2_versioning_service();
|
||||
$this->economic = $economic;
|
||||
}
|
||||
@@ -388,7 +389,7 @@ class economic_v2_distribution_service
|
||||
continue;
|
||||
}
|
||||
|
||||
$discount_row = $this->resolveDiscountForProduct($customer_number, $product_id, $created_at);
|
||||
$discount_row = $this->resolveDiscountForProduct($customer_number, $product_id, $created_at, $department_id);
|
||||
if ($discount_row === null) {
|
||||
continue;
|
||||
}
|
||||
@@ -1528,7 +1529,7 @@ class economic_v2_distribution_service
|
||||
$line_price = ((float)$this->getProductDepartmentPrice($product_id, $department_id)) * $quantity;
|
||||
}
|
||||
|
||||
$discount_row = $this->resolveDiscountForProduct($customer_number, $product_id, $timestamp);
|
||||
$discount_row = $this->resolveDiscountForProduct($customer_number, $product_id, $timestamp, $department_id);
|
||||
if ($discount_row !== null && array_key_exists('fixed_price', $discount_row) && $discount_row['fixed_price'] !== null) {
|
||||
$line_price = ((float)$discount_row['fixed_price']) * $quantity;
|
||||
$total += $line_price;
|
||||
@@ -1544,14 +1545,18 @@ class economic_v2_distribution_service
|
||||
return $total;
|
||||
}
|
||||
|
||||
protected function resolveDiscountForProduct(int $customer_number, int $product_id, string $timestamp): ?array
|
||||
protected function resolveDiscountForProduct(int $customer_number, int $product_id, string $timestamp, ?int $department_id = null): ?array
|
||||
{
|
||||
$cache_key = $customer_number . '|' . $product_id . '|' . substr($timestamp, 0, 19);
|
||||
$cache_key = $customer_number . '|' . $product_id . '|' . (int)($department_id ?? 0) . '|' . substr($timestamp, 0, 19);
|
||||
if (array_key_exists($cache_key, $this->discount_resolution_cache)) {
|
||||
return $this->discount_resolution_cache[$cache_key];
|
||||
}
|
||||
|
||||
$direct = $this->versioning->resolveDiscountOverrideAt($customer_number, false, (string)$product_id, $timestamp);
|
||||
$scopedDepartmentId = $department_id !== null && (new \objects\departments_o())->isCustomPricingOnly((int)$department_id)
|
||||
? (int)$department_id
|
||||
: null;
|
||||
|
||||
$direct = $this->versioning->resolveDiscountOverrideAt($customer_number, false, (string)$product_id, $timestamp, $scopedDepartmentId);
|
||||
if ($direct !== null && (
|
||||
(array_key_exists('fixed_price', $direct) && $direct['fixed_price'] !== null)
|
||||
|| (int)($direct['discount'] ?? 0) > 0
|
||||
@@ -1563,13 +1568,20 @@ class economic_v2_distribution_service
|
||||
if ($product !== null) {
|
||||
$category = (string)$product->category->value();
|
||||
if ($category !== '') {
|
||||
$category_discount = $this->versioning->resolveDiscountOverrideAt($customer_number, true, $category, $timestamp);
|
||||
$category_discount = $this->versioning->resolveDiscountOverrideAt($customer_number, true, $category, $timestamp, $scopedDepartmentId);
|
||||
if ($category_discount !== null && (int)($category_discount['discount'] ?? 0) > 0) {
|
||||
return $this->discount_resolution_cache[$cache_key] = $category_discount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($scopedDepartmentId !== null) {
|
||||
$global_discount = $this->versioning->resolveDiscountOverrideAt($customer_number, true, 'global', $timestamp, $scopedDepartmentId);
|
||||
if ($global_discount !== null && (int)($global_discount['discount'] ?? 0) > 0) {
|
||||
return $this->discount_resolution_cache[$cache_key] = $global_discount;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->discount_resolution_cache[$cache_key] = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class economic_v2_schema_bootstrap
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS customer_discount_override_versions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
department_id INT NULL DEFAULT NULL,
|
||||
user_id INT NOT NULL,
|
||||
customer_number INT NOT NULL,
|
||||
is_category TINYINT(1) NOT NULL,
|
||||
@@ -75,6 +76,7 @@ class economic_v2_schema_bootstrap
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_discount_override_versions_lookup (customer_number, is_category, object_id, effective_from, effective_to),
|
||||
INDEX idx_discount_override_versions_department_lookup (department_id, customer_number, is_category, object_id, effective_from, effective_to),
|
||||
INDEX idx_discount_override_versions_user (user_id),
|
||||
INDEX idx_discount_override_versions_source (source)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
@@ -92,6 +94,14 @@ class economic_v2_schema_bootstrap
|
||||
);
|
||||
}
|
||||
|
||||
if (!self::tableHasColumn('customer_discount_override_versions', 'department_id')) {
|
||||
$db->query(
|
||||
"ALTER TABLE customer_discount_override_versions
|
||||
ADD COLUMN department_id INT NULL DEFAULT NULL
|
||||
AFTER id"
|
||||
);
|
||||
}
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,8 @@ class economic_v2_versioning_service
|
||||
float $confidence = 1.0,
|
||||
bool $inferred = false,
|
||||
array $metadata = [],
|
||||
?int $fixed_price = null
|
||||
?int $fixed_price = null,
|
||||
?int $department_id = null
|
||||
): array {
|
||||
$identity = [
|
||||
'user_id' => $user_id,
|
||||
@@ -144,6 +145,9 @@ class economic_v2_versioning_service
|
||||
'is_category' => (int)$is_category,
|
||||
'object_id' => (string)$object_id,
|
||||
];
|
||||
if ($department_id !== null) {
|
||||
$identity['department_id'] = (int)$department_id;
|
||||
}
|
||||
|
||||
if (($discount === null || (int)$discount === 0) && $fixed_price === null) {
|
||||
return $this->closeActiveVersion(
|
||||
@@ -240,15 +244,21 @@ class economic_v2_versioning_service
|
||||
int $customer_number,
|
||||
bool $is_category,
|
||||
int|string $object_id,
|
||||
string $timestamp
|
||||
string $timestamp,
|
||||
?int $department_id = null
|
||||
): ?array {
|
||||
$identity = [
|
||||
'customer_number' => $customer_number,
|
||||
'is_category' => (int)$is_category,
|
||||
'object_id' => (string)$object_id,
|
||||
];
|
||||
if ($department_id !== null) {
|
||||
$identity['department_id'] = (int)$department_id;
|
||||
}
|
||||
|
||||
$rows = $this->resolveActiveVersions(
|
||||
'customer_discount_override_versions',
|
||||
[
|
||||
'customer_number' => $customer_number,
|
||||
'is_category' => (int)$is_category,
|
||||
'object_id' => (string)$object_id,
|
||||
],
|
||||
$identity,
|
||||
$timestamp,
|
||||
'effective_from DESC, id DESC',
|
||||
1
|
||||
|
||||
@@ -31,6 +31,7 @@ class invoice_period_flag_service
|
||||
{
|
||||
invoice_period_flag_schema_bootstrap::ensureTables();
|
||||
price_overrides_schema_bootstrap::ensureColumns();
|
||||
department_customer_price_overrides_schema_bootstrap::ensureTables();
|
||||
}
|
||||
|
||||
public function createManualFlag(array $payload, int $userId): array
|
||||
@@ -712,9 +713,21 @@ class invoice_period_flag_service
|
||||
p.max_quantity_per_order,
|
||||
c.name AS category_name,
|
||||
pdp.price AS department_price,
|
||||
product_discount.percentage AS product_discount_percentage,
|
||||
product_discount.fixed_price AS product_fixed_price,
|
||||
category_discount.percentage AS category_discount_percentage
|
||||
CASE
|
||||
WHEN d.custom_pricing_only = 1 THEN department_product_discount.percentage
|
||||
ELSE product_discount.percentage
|
||||
END AS product_discount_percentage,
|
||||
CASE
|
||||
WHEN d.custom_pricing_only = 1 THEN department_product_discount.fixed_price
|
||||
ELSE product_discount.fixed_price
|
||||
END AS product_fixed_price,
|
||||
CASE
|
||||
WHEN d.custom_pricing_only = 1 THEN GREATEST(
|
||||
COALESCE(department_category_discount.percentage, 0),
|
||||
COALESCE(department_global_discount.percentage, 0)
|
||||
)
|
||||
ELSE category_discount.percentage
|
||||
END AS category_discount_percentage
|
||||
FROM orders o
|
||||
LEFT JOIN (
|
||||
SELECT customer_number, MIN(id) AS id, MAX(display_name) AS display_name
|
||||
@@ -745,6 +758,32 @@ class invoice_period_flag_service
|
||||
) category_discount
|
||||
ON category_discount.customer_number = o.customer_id
|
||||
AND category_discount.product_or_category_id = p.category
|
||||
LEFT JOIN (
|
||||
SELECT department_id, user_id, product_or_category_id, MAX(percentage) AS percentage, MAX(fixed_price) AS fixed_price
|
||||
FROM department_customer_price_overrides
|
||||
WHERE is_category = 0
|
||||
GROUP BY department_id, user_id, product_or_category_id
|
||||
) department_product_discount
|
||||
ON department_product_discount.department_id = o.department_id
|
||||
AND department_product_discount.user_id = u.id
|
||||
AND department_product_discount.product_or_category_id = p.id
|
||||
LEFT JOIN (
|
||||
SELECT department_id, user_id, product_or_category_id, MAX(percentage) AS percentage
|
||||
FROM department_customer_price_overrides
|
||||
WHERE is_category = 1 AND product_or_category_id <> 'global'
|
||||
GROUP BY department_id, user_id, product_or_category_id
|
||||
) department_category_discount
|
||||
ON department_category_discount.department_id = o.department_id
|
||||
AND department_category_discount.user_id = u.id
|
||||
AND department_category_discount.product_or_category_id = p.category
|
||||
LEFT JOIN (
|
||||
SELECT department_id, user_id, MAX(percentage) AS percentage
|
||||
FROM department_customer_price_overrides
|
||||
WHERE is_category = 1 AND product_or_category_id = 'global'
|
||||
GROUP BY department_id, user_id
|
||||
) department_global_discount
|
||||
ON department_global_discount.department_id = o.department_id
|
||||
AND department_global_discount.user_id = u.id
|
||||
WHERE o.created_at BETWEEN '{$escapedDateFrom}' AND '{$escapedDateTo}'
|
||||
AND o.deleted_at IS NULL
|
||||
ORDER BY o.customer_id, o.id, oi.id";
|
||||
|
||||
@@ -795,6 +795,34 @@ class limited_backoffice_service
|
||||
return $this->getDepartmentPrices($user, $departmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getDepartmentCustomerPricing(users_o $user, int $departmentId, int $customerUserId): array
|
||||
{
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
return (new department_customer_pricing_service())->getPricing($departmentId, $customerUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function updateDepartmentCustomerPricing(users_o $user, int $departmentId, int $customerUserId, array $payload): array
|
||||
{
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return (new department_customer_pricing_service())->updatePricing($departmentId, $customerUserId, [
|
||||
...$payload,
|
||||
'department_id' => $departmentId,
|
||||
'user_id' => $customerUserId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace objects;
|
||||
|
||||
use classes\db;
|
||||
use classes\department_customer_price_overrides_schema_bootstrap;
|
||||
use traits\db_object_t;
|
||||
|
||||
class department_customer_price_overrides_o extends db
|
||||
{
|
||||
use db_object_t;
|
||||
|
||||
public function structure(): void
|
||||
{
|
||||
department_customer_price_overrides_schema_bootstrap::ensureTables();
|
||||
$this->setTable('department_customer_price_overrides');
|
||||
}
|
||||
|
||||
public function objectChanged(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function setPrice(int $departmentId, int $userId, bool $isCategory, int|string $objectId, int $percentage, ?int $fixedPrice = null): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
if ($isCategory) {
|
||||
$fixedPrice = null;
|
||||
}
|
||||
|
||||
$this->removePrice($departmentId, $userId, $isCategory, $objectId);
|
||||
|
||||
if ($percentage <= 0 && $fixedPrice === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$objectId = $db->escape_string((string)$objectId);
|
||||
$fixedPriceSql = $fixedPrice === null ? 'NULL' : (string)max(0, (int)$fixedPrice);
|
||||
$sql = "INSERT INTO {$this->table} (`department_id`, `user_id`, `is_category`, `product_or_category_id`, `percentage`, `fixed_price`)
|
||||
VALUES (" . (int)$departmentId . ", " . (int)$userId . ", " . (int)$isCategory . ", '{$objectId}', " . (int)$percentage . ", {$fixedPriceSql})";
|
||||
$db->query($sql);
|
||||
}
|
||||
|
||||
public function removePrice(int $departmentId, int $userId, bool $isCategory, int|string $objectId): void
|
||||
{
|
||||
global $db;
|
||||
|
||||
$objectId = $db->escape_string((string)$objectId);
|
||||
$sql = "DELETE FROM {$this->table}
|
||||
WHERE `department_id` = " . (int)$departmentId . "
|
||||
AND `user_id` = " . (int)$userId . "
|
||||
AND `is_category` = " . (int)$isCategory . "
|
||||
AND `product_or_category_id` = '{$objectId}'";
|
||||
$db->query($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getAllPrices(int $departmentId, int $userId): array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE `department_id` = " . (int)$departmentId . "
|
||||
AND `user_id` = " . (int)$userId . "
|
||||
ORDER BY `is_category` DESC, `product_or_category_id` ASC";
|
||||
$result = $db->query($sql);
|
||||
$prices = [];
|
||||
if ($result && $result->num_rows > 0) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$prices[] = $this->parseRow($row);
|
||||
}
|
||||
}
|
||||
|
||||
return $prices;
|
||||
}
|
||||
|
||||
public function getDirectPriceRow(int $departmentId, int $userId, bool $isCategory, int|string $objectId): ?array
|
||||
{
|
||||
global $db;
|
||||
|
||||
$objectId = $db->escape_string((string)$objectId);
|
||||
$sql = "SELECT * FROM {$this->table}
|
||||
WHERE `department_id` = " . (int)$departmentId . "
|
||||
AND `user_id` = " . (int)$userId . "
|
||||
AND `is_category` = " . (int)$isCategory . "
|
||||
AND `product_or_category_id` = '{$objectId}'
|
||||
LIMIT 1";
|
||||
$result = $db->query($sql);
|
||||
if (!$result || $result->num_rows < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->parseRow($result->fetch_assoc());
|
||||
}
|
||||
|
||||
private function parseRow(array $row): array
|
||||
{
|
||||
$isCategory = (bool)$row['is_category'];
|
||||
|
||||
return [
|
||||
'id' => (int)$row['id'],
|
||||
'department_id' => (int)$row['department_id'],
|
||||
'user_id' => (int)$row['user_id'],
|
||||
'is_category' => $isCategory,
|
||||
'product_or_category_id' => $isCategory ? (string)$row['product_or_category_id'] : (int)$row['product_or_category_id'],
|
||||
'percentage' => (int)$row['percentage'],
|
||||
'fixed_price' => array_key_exists('fixed_price', $row) && $row['fixed_price'] !== null ? (int)$row['fixed_price'] : null,
|
||||
'created_at' => (string)$row['created_at'],
|
||||
'updated_at' => (string)$row['updated_at'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@ class order_items_o extends db
|
||||
// Check if the user has a discount on the product, or category
|
||||
$customer = (new orders_o())->getOrderCustomer($order_id);
|
||||
if (!products_o::priceResolutionIsCustomMissing($priceResolution)) {
|
||||
$price = $customer->applyProductCustomerPricing($product_id, (int)$price, false);
|
||||
$price = $customer->applyProductCustomerPricing($product_id, (int)$price, false, (int)$order->department_id->value());
|
||||
}
|
||||
|
||||
// If the price is forced, set the price to the forced price
|
||||
|
||||
@@ -1436,7 +1436,7 @@ class orders_o extends db
|
||||
throw new Exception('No user found matching the customer number in the usage log');
|
||||
}
|
||||
if (!products_o::priceResolutionIsCustomMissing($priceResolution)) {
|
||||
$product_price = $user->applyProductCustomerPricing((int)$order_item->product_id->value(), (int)$product_price);
|
||||
$product_price = $user->applyProductCustomerPricing((int)$order_item->product_id->value(), (int)$product_price, true, (int)$this->department_id->value());
|
||||
}
|
||||
$order_item->notes->set(null); // Set notes for the simulated order item
|
||||
$order_item->price->set((int)$product_price); // Set the price based on the product price and discount percentage
|
||||
@@ -1509,7 +1509,7 @@ class orders_o extends db
|
||||
if (products_o::priceResolutionIsCustomMissing($priceResolution)) {
|
||||
return $price;
|
||||
}
|
||||
return $current_user->applyProductCustomerPricing((int)$product->id, $price);
|
||||
return $current_user->applyProductCustomerPricing((int)$product->id, $price, true, (int)$this->department_id->value());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1598,7 +1598,7 @@ class orders_o extends db
|
||||
}
|
||||
$unitPrice = (int)$department_price_cache[$product_id]['price'];
|
||||
if (!products_o::priceResolutionIsCustomMissing($department_price_cache[$product_id])) {
|
||||
$unitPrice = $tmp_user->applyProductCustomerPricing($product_id, $unitPrice, false);
|
||||
$unitPrice = $tmp_user->applyProductCustomerPricing($product_id, $unitPrice, false, $department_id);
|
||||
}
|
||||
$post_discount = $unitPrice * $quantity;
|
||||
$total += $post_discount;
|
||||
|
||||
@@ -326,13 +326,13 @@ class products_o extends db
|
||||
return $this->getDepartmentPriceResolution($department_id)['price'];
|
||||
}
|
||||
|
||||
public function applyCustomerDiscounts(array $products, users_o $customer): array
|
||||
public function applyCustomerDiscounts(array $products, users_o $customer, ?int $departmentId = null): array
|
||||
{
|
||||
global $db;
|
||||
// Get the customer's discounts
|
||||
return array_map(fn($product) => $this->applyCustomerDiscount($product, $customer), $products);
|
||||
return array_map(fn($product) => $this->applyCustomerDiscount($product, $customer, $departmentId), $products);
|
||||
}
|
||||
public function applyCustomerDiscount(array $product, users_o $customer): array
|
||||
public function applyCustomerDiscount(array $product, users_o $customer, ?int $departmentId = null): array
|
||||
{
|
||||
global $db;
|
||||
// Validate input
|
||||
@@ -340,7 +340,7 @@ class products_o extends db
|
||||
throw new \InvalidArgumentException('Invalid product array, must contain id and price keys');
|
||||
}
|
||||
if (($product[self::PRICE_SOURCE_KEY] ?? null) !== self::PRICE_SOURCE_CUSTOM_MISSING) {
|
||||
$product['price'] = $customer->applyProductCustomerPricing((int)$product['id'], (int)$product['price']);
|
||||
$product['price'] = $customer->applyProductCustomerPricing((int)$product['id'], (int)$product['price'], true, $departmentId);
|
||||
}
|
||||
unset($product[self::PRICE_SOURCE_KEY]);
|
||||
return $product;
|
||||
|
||||
@@ -936,9 +936,18 @@ class users_o extends db
|
||||
* @param bool $is_category If the object is a category
|
||||
* @return int|null The discount percentage
|
||||
*/
|
||||
public function getCustomPrice(int $object_id, bool $is_category = false): int|null
|
||||
public function getCustomPrice(int $object_id, bool $is_category = false, ?int $department_id = null): int|null
|
||||
{
|
||||
self::requireSelected();
|
||||
if ($department_id !== null && (new departments_o())->isCustomPricingOnly((int)$department_id)) {
|
||||
$row = (new department_customer_price_overrides_o())->getDirectPriceRow(
|
||||
(int)$department_id,
|
||||
(int)$this->id,
|
||||
$is_category,
|
||||
$object_id
|
||||
);
|
||||
return $row === null ? 0 : (int)$row['percentage'];
|
||||
}
|
||||
// Get the custom price for the product
|
||||
$discount = $this->price_overrides->setUser($this->id)->getPrice($is_category, $object_id);
|
||||
// If the is_category is false, check if there is a custom price for the category that the product belongs to
|
||||
@@ -962,9 +971,12 @@ class users_o extends db
|
||||
* - Category discount (If the product allows category inheritance of discounts)
|
||||
* - Product discount (If the product has a custom price)
|
||||
*/
|
||||
public function getProductDiscountPercentage(int $product_id): int|null
|
||||
public function getProductDiscountPercentage(int $product_id, ?int $department_id = null): int|null
|
||||
{
|
||||
self::requireSelected();
|
||||
if ($department_id !== null && (new departments_o())->isCustomPricingOnly((int)$department_id)) {
|
||||
return $this->getDepartmentScopedProductDiscountPercentage($product_id, (int)$department_id);
|
||||
}
|
||||
// Get the product by ID
|
||||
$product = (new products_o())->select((int)$product_id);
|
||||
$doesProductAllowCategoryDiscount = (bool)$product->apply_category_discount->value();
|
||||
@@ -981,24 +993,36 @@ class users_o extends db
|
||||
return $discount_percentage === null ? 0 : (int)$discount_percentage;
|
||||
}
|
||||
|
||||
public function getProductFixedPrice(int $product_id): ?int
|
||||
public function getProductFixedPrice(int $product_id, ?int $department_id = null): ?int
|
||||
{
|
||||
self::requireSelected();
|
||||
if ($department_id !== null && (new departments_o())->isCustomPricingOnly((int)$department_id)) {
|
||||
$row = (new department_customer_price_overrides_o())->getDirectPriceRow(
|
||||
(int)$department_id,
|
||||
(int)$this->id,
|
||||
false,
|
||||
(int)$product_id
|
||||
);
|
||||
if ($row === null || $row['fixed_price'] === null) {
|
||||
return null;
|
||||
}
|
||||
return (int)$row['fixed_price'];
|
||||
}
|
||||
return $this->price_overrides->setUser($this->id)->getFixedPrice(false, $product_id);
|
||||
}
|
||||
|
||||
public function applyProductCustomerPricing(int $product_id, int $base_price, bool $use_final_price_discount_calculation = true): int
|
||||
public function applyProductCustomerPricing(int $product_id, int $base_price, bool $use_final_price_discount_calculation = true, ?int $department_id = null): int
|
||||
{
|
||||
self::requireSelected();
|
||||
|
||||
$fixed_price = $this->getProductFixedPrice($product_id);
|
||||
$fixed_price = $this->getProductFixedPrice($product_id, $department_id);
|
||||
if ($fixed_price !== null) {
|
||||
return $fixed_price;
|
||||
}
|
||||
|
||||
$discount_percentage = $use_final_price_discount_calculation
|
||||
? (int)$this->getProductDiscountPercentage($product_id)
|
||||
: (int)$this->getCustomPrice($product_id, false);
|
||||
? (int)$this->getProductDiscountPercentage($product_id, $department_id)
|
||||
: (int)$this->getCustomPrice($product_id, false, $department_id);
|
||||
if ($discount_percentage <= 0) {
|
||||
return $base_price;
|
||||
}
|
||||
@@ -1006,6 +1030,34 @@ class users_o extends db
|
||||
return (int)round($base_price * (1 - ($discount_percentage / 100)));
|
||||
}
|
||||
|
||||
private function getDepartmentScopedProductDiscountPercentage(int $product_id, int $department_id): int
|
||||
{
|
||||
self::requireSelected();
|
||||
|
||||
$overrides = new department_customer_price_overrides_o();
|
||||
$product = (new products_o())->select((int)$product_id);
|
||||
$discounts = [];
|
||||
|
||||
$productRow = $overrides->getDirectPriceRow($department_id, (int)$this->id, false, (int)$product_id);
|
||||
if ($productRow !== null) {
|
||||
$discounts[] = (int)$productRow['percentage'];
|
||||
}
|
||||
|
||||
if ((bool)$product->apply_category_discount->value()) {
|
||||
$categoryRow = $overrides->getDirectPriceRow($department_id, (int)$this->id, true, (string)$product->category->value());
|
||||
if ($categoryRow !== null) {
|
||||
$discounts[] = (int)$categoryRow['percentage'];
|
||||
}
|
||||
|
||||
$globalRow = $overrides->getDirectPriceRow($department_id, (int)$this->id, true, 'global');
|
||||
if ($globalRow !== null) {
|
||||
$discounts[] = (int)$globalRow['percentage'];
|
||||
}
|
||||
}
|
||||
|
||||
return max([0, ...$discounts]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -12667,6 +12667,139 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/superuser/department/customer-pricing:
|
||||
get:
|
||||
tags:
|
||||
- Departments
|
||||
summary: Get department-specific customer pricing
|
||||
description: Returns customer price overrides and the department product catalog when custom-only pricing is enabled for the department.
|
||||
operationId: getSuperuserDepartmentCustomerPricing
|
||||
parameters:
|
||||
- name: department_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
- name: user_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Department customer pricing
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
put:
|
||||
tags:
|
||||
- Departments
|
||||
summary: Replace department-specific customer pricing
|
||||
description: Replaces the complete override set for one customer in one custom-only department.
|
||||
operationId: setSuperuserDepartmentCustomerPricing
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Department customer pricing updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
|
||||
/limited-backoffice/departments/{departmentId}/customer-pricing:
|
||||
get:
|
||||
tags:
|
||||
- Limited Backoffice
|
||||
summary: Get limited-backoffice department customer pricing
|
||||
description: Returns department-specific customer pricing for an assigned custom-only department.
|
||||
operationId: getLimitedBackofficeDepartmentCustomerPricing
|
||||
parameters:
|
||||
- name: departmentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
- name: user_id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Department customer pricing
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
put:
|
||||
tags:
|
||||
- Limited Backoffice
|
||||
summary: Replace limited-backoffice department customer pricing
|
||||
description: Replaces the complete override set for one customer in an assigned custom-only department.
|
||||
operationId: setLimitedBackofficeDepartmentCustomerPricing
|
||||
parameters:
|
||||
- name: departmentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingLimitedUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Department customer pricing updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'409':
|
||||
$ref: '#/components/responses/Conflict'
|
||||
|
||||
/superuser/department/variables:
|
||||
get:
|
||||
tags:
|
||||
@@ -13482,6 +13615,164 @@ components:
|
||||
type: integer
|
||||
description: HTTP status code
|
||||
|
||||
DepartmentCustomerPricingUpdateRequest:
|
||||
type: object
|
||||
required:
|
||||
- department_id
|
||||
- user_id
|
||||
- overrides
|
||||
properties:
|
||||
department_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
user_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
overrides:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput'
|
||||
|
||||
DepartmentCustomerPricingLimitedUpdateRequest:
|
||||
type: object
|
||||
required:
|
||||
- user_id
|
||||
- overrides
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
overrides:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput'
|
||||
|
||||
DepartmentCustomerPricingOverrideInput:
|
||||
type: object
|
||||
required:
|
||||
- is_category
|
||||
- product_or_category_id
|
||||
properties:
|
||||
is_category:
|
||||
type: boolean
|
||||
product_or_category_id:
|
||||
oneOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
description: Product id, category id, or `global` for the customer-wide department discount.
|
||||
discount:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
percentage:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
fixed_price:
|
||||
type: integer
|
||||
nullable: true
|
||||
minimum: 0
|
||||
|
||||
DepartmentCustomerPricingOverride:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput'
|
||||
- type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
department_id:
|
||||
type: integer
|
||||
user_id:
|
||||
type: integer
|
||||
percentage:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
created_at:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
|
||||
DepartmentCustomerPricingProduct:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
category:
|
||||
type: integer
|
||||
apply_category_discount:
|
||||
type: boolean
|
||||
base_price:
|
||||
type: integer
|
||||
department_price:
|
||||
type: integer
|
||||
nullable: true
|
||||
effective_price:
|
||||
type: integer
|
||||
missing_department_price:
|
||||
type: boolean
|
||||
|
||||
DepartmentCustomerPricingCategory:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
products:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingProduct'
|
||||
|
||||
DepartmentCustomerPricingResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
department:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
custom_pricing_only:
|
||||
type: boolean
|
||||
customer:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
customer_number:
|
||||
type: integer
|
||||
display_name:
|
||||
type: string
|
||||
overrides:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingOverride'
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DepartmentCustomerPricingCategory'
|
||||
meta:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
includes:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
|
||||
ErrorReportSubmissionRequest:
|
||||
type: object
|
||||
required:
|
||||
|
||||
@@ -1735,7 +1735,7 @@ class InvoicingPeriodRoute
|
||||
if (!array_key_exists($product_id, $discount_cache)) {
|
||||
$unit_price = (int)$department_price_cache[$department_id][$product_id]['price'];
|
||||
if (!products_o::priceResolutionIsCustomMissing($department_price_cache[$department_id][$product_id])) {
|
||||
$unit_price = $user->applyProductCustomerPricing($product_id, $unit_price, false);
|
||||
$unit_price = $user->applyProductCustomerPricing($product_id, $unit_price, false, $department_id);
|
||||
}
|
||||
$discount_cache[$product_id] = $unit_price;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,31 @@ class limitedBackofficeRoute
|
||||
limited_backoffice_service::PERMISSION_MANAGE_PRICES => 'Manage limited backoffice department prices',
|
||||
]);
|
||||
|
||||
$this->get('/limited-backoffice/departments/{departmentId}/customer-pricing', function () {
|
||||
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
||||
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
||||
$departmentId = $this->routePositiveInt('departmentId');
|
||||
$customerUserId = $this->queryCustomerUserId();
|
||||
return $service->getDepartmentCustomerPricing($user, $departmentId, $customerUserId);
|
||||
});
|
||||
}, [
|
||||
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
||||
]);
|
||||
|
||||
$this->put('/limited-backoffice/departments/{departmentId}/customer-pricing', function () {
|
||||
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
||||
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
||||
$this->requirePermission(limited_backoffice_service::PERMISSION_MANAGE_PRICES);
|
||||
$departmentId = $this->routePositiveInt('departmentId');
|
||||
$payload = $this->requestPayload();
|
||||
$customerUserId = $this->payloadCustomerUserId($payload);
|
||||
return $service->updateDepartmentCustomerPricing($user, $departmentId, $customerUserId, $payload);
|
||||
});
|
||||
}, [
|
||||
limited_backoffice_service::PERMISSION_ACCESS => 'Access the limited backoffice',
|
||||
limited_backoffice_service::PERMISSION_MANAGE_PRICES => 'Manage limited backoffice department prices',
|
||||
]);
|
||||
|
||||
$this->get('/limited-backoffice/roles', function () {
|
||||
$this->withLimitedBackoffice(function (limited_backoffice_service $service, $user): array {
|
||||
$this->requirePermission(limited_backoffice_service::PERMISSION_ACCESS);
|
||||
@@ -154,4 +179,59 @@ class limitedBackofficeRoute
|
||||
}
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
private function queryPositiveInt(string $name): int
|
||||
{
|
||||
$value = $this->fromQuery($name);
|
||||
if (!is_string($value) || !ctype_digit($value) || (int)$value <= 0) {
|
||||
throw new limited_backoffice_exception('Invalid query parameter.', 400);
|
||||
}
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
private function queryCustomerUserId(): int
|
||||
{
|
||||
if ($this->fromQuery('user_id') !== null) {
|
||||
return $this->queryPositiveInt('user_id');
|
||||
}
|
||||
|
||||
return $this->userIdFromCustomerNumber($this->queryPositiveInt('customer_number'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function payloadPositiveInt(array $payload, string $name): int
|
||||
{
|
||||
$value = $payload[$name] ?? null;
|
||||
if (is_int($value) && $value > 0) {
|
||||
return $value;
|
||||
}
|
||||
if (!is_string($value) || !ctype_digit($value) || (int)$value <= 0) {
|
||||
throw new limited_backoffice_exception('Invalid request parameter.', 400);
|
||||
}
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function payloadCustomerUserId(array $payload): int
|
||||
{
|
||||
if (array_key_exists('user_id', $payload)) {
|
||||
return $this->payloadPositiveInt($payload, 'user_id');
|
||||
}
|
||||
|
||||
return $this->userIdFromCustomerNumber($this->payloadPositiveInt($payload, 'customer_number'));
|
||||
}
|
||||
|
||||
private function userIdFromCustomerNumber(int $customerNumber): int
|
||||
{
|
||||
$customer = (new \objects\users_o())->getUserByCustomerNumber($customerNumber);
|
||||
if (!$customer->exists()) {
|
||||
throw new limited_backoffice_exception('Customer not found', 404);
|
||||
}
|
||||
|
||||
return (int)$customer->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class productsRoute
|
||||
// Check if the customer is set
|
||||
if ($customer !== null) {
|
||||
// Apply the customers unique discounts
|
||||
$products = (new products_o())->applyCustomerDiscounts($products, $customer);
|
||||
$products = (new products_o())->applyCustomerDiscounts($products, $customer, $departmentId);
|
||||
} else {
|
||||
$products = products_o::stripDepartmentPriceSources($products);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use classes\department_customer_pricing_service;
|
||||
use classes\limited_backoffice_exception;
|
||||
use objects\branding_o;
|
||||
use objects\department_variables_o;
|
||||
use objects\departments_o;
|
||||
use objects\logs_o;
|
||||
use objects\products_o;
|
||||
use objects\users_o;
|
||||
use traits\route_t;
|
||||
|
||||
class superuserDepartmentRoute
|
||||
@@ -213,6 +216,57 @@ class superuserDepartmentRoute
|
||||
'superuser_fetch_department_prices' => 'Fetch department prices'
|
||||
]);
|
||||
|
||||
$this->get('/superuser/department/customer-pricing', function () {
|
||||
global $response;
|
||||
$this->requirePermission('superuser_fetch_department_customer_pricing');
|
||||
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_FETCH_DEPARTMENT_CUSTOMER_PRICING', 'No user found, or invalid session');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
$departmentId = $this->positiveIntFromRequest('department_id');
|
||||
$userId = $this->customerUserIdFromRequest();
|
||||
|
||||
try {
|
||||
(new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_FETCH_DEPARTMENT_CUSTOMER_PRICING', 'Successfully fetched department customer pricing');
|
||||
$response->success((new department_customer_pricing_service())->getPricing($departmentId, $userId));
|
||||
} catch (limited_backoffice_exception $exception) {
|
||||
$response->error($exception->payload(), $exception->statusCode());
|
||||
}
|
||||
}, [
|
||||
'superuser_fetch_department_customer_pricing' => 'Fetch department customer pricing'
|
||||
]);
|
||||
|
||||
$this->put('/superuser/department/customer-pricing', function () {
|
||||
global $response;
|
||||
$this->requirePermission('superuser_set_department_customer_pricing');
|
||||
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
(new logs_o())->add('departments', 'global', 1, 0, 'SUPERUSER_SET_DEPARTMENT_CUSTOMER_PRICING', 'No user found, or invalid session');
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
$payload = json_decode(file_get_contents('php://input'), true);
|
||||
if (!is_array($payload)) {
|
||||
$response->error('Invalid request body', 400);
|
||||
}
|
||||
|
||||
$departmentId = $this->positiveIntFromPayload($payload, 'department_id');
|
||||
$userId = $this->customerUserIdFromPayload($payload);
|
||||
|
||||
try {
|
||||
(new logs_o())->add('departments', 'global', 1, $user->id, 'SUPERUSER_SET_DEPARTMENT_CUSTOMER_PRICING', 'Successfully set department customer pricing');
|
||||
$response->success((new department_customer_pricing_service())->updatePricing($departmentId, $userId, $payload));
|
||||
} catch (limited_backoffice_exception $exception) {
|
||||
$response->error($exception->payload(), $exception->statusCode());
|
||||
}
|
||||
}, [
|
||||
'superuser_set_department_customer_pricing' => 'Set department customer pricing'
|
||||
]);
|
||||
|
||||
$this->get('/superuser/department/variables', function () {
|
||||
// Require the user to be logged in
|
||||
global $response;
|
||||
@@ -295,4 +349,78 @@ class superuserDepartmentRoute
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
private function positiveIntFromRequest(string $name): int
|
||||
{
|
||||
global $response;
|
||||
$value = $this->fromRequest($name);
|
||||
if (!is_string($value) && !is_int($value)) {
|
||||
$response->error($name . ' is required', 400);
|
||||
}
|
||||
|
||||
return $this->positiveIntValue($value, $name);
|
||||
}
|
||||
|
||||
private function customerUserIdFromRequest(): int
|
||||
{
|
||||
if ($this->fromRequest('user_id') !== null) {
|
||||
return $this->positiveIntFromRequest('user_id');
|
||||
}
|
||||
|
||||
return $this->userIdFromCustomerNumber($this->positiveIntFromRequest('customer_number'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function positiveIntFromPayload(array $payload, string $name): int
|
||||
{
|
||||
global $response;
|
||||
if (!array_key_exists($name, $payload)) {
|
||||
$response->error($name . ' is required', 400);
|
||||
}
|
||||
|
||||
return $this->positiveIntValue($payload[$name], $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function customerUserIdFromPayload(array $payload): int
|
||||
{
|
||||
if (array_key_exists('user_id', $payload)) {
|
||||
return $this->positiveIntFromPayload($payload, 'user_id');
|
||||
}
|
||||
|
||||
return $this->userIdFromCustomerNumber($this->positiveIntFromPayload($payload, 'customer_number'));
|
||||
}
|
||||
|
||||
private function positiveIntValue(mixed $value, string $name): int
|
||||
{
|
||||
global $response;
|
||||
if (is_int($value)) {
|
||||
$parsed = $value;
|
||||
} elseif (is_string($value) && preg_match('/^\d+$/', trim($value)) === 1) {
|
||||
$parsed = (int)trim($value);
|
||||
} else {
|
||||
$response->error($name . ' must be a positive integer', 400);
|
||||
}
|
||||
|
||||
if ($parsed <= 0) {
|
||||
$response->error($name . ' must be a positive integer', 400);
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
private function userIdFromCustomerNumber(int $customerNumber): int
|
||||
{
|
||||
global $response;
|
||||
$customer = (new users_o())->getUserByCustomerNumber($customerNumber);
|
||||
if (!$customer->exists()) {
|
||||
$response->error('Customer not found', 404);
|
||||
}
|
||||
|
||||
return (int)$customer->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use classes\limited_backoffice_service;
|
||||
|
||||
usesApiSuite();
|
||||
|
||||
function department_customer_pricing_price_insert(int $departmentId, int $productId, int $price): void
|
||||
{
|
||||
$statement = api_test_runtime()->db()->prepare(
|
||||
'INSERT INTO `product_department_prices` (`department_id`, `product_id`, `price`)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE `price` = VALUES(`price`)'
|
||||
);
|
||||
$statement->bind_param('iii', $departmentId, $productId, $price);
|
||||
$statement->execute();
|
||||
$statement->close();
|
||||
|
||||
api_fixtures()->cleanupDeleteWhere('product_department_prices', [
|
||||
'department_id' => $departmentId,
|
||||
'product_id' => $productId,
|
||||
]);
|
||||
}
|
||||
|
||||
function department_customer_pricing_setup(array $departmentAttributes = []): array
|
||||
{
|
||||
$department = api_fixtures()->createDepartment([
|
||||
'name' => 'Scoped Customer Pricing Department',
|
||||
'custom_pricing_only' => 1,
|
||||
...$departmentAttributes,
|
||||
]);
|
||||
$category = api_fixtures()->createCategory(['name' => 'Scoped Customer Pricing Category']);
|
||||
$product = api_fixtures()->createProduct([
|
||||
'name' => 'Scoped Customer Pricing Product',
|
||||
'category' => $category['id'],
|
||||
'price' => 1000,
|
||||
'apply_category_discount' => 1,
|
||||
]);
|
||||
api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']);
|
||||
department_customer_pricing_price_insert((int)$department['id'], (int)$product['id'], 1000);
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Scoped Customer Pricing Customer']);
|
||||
|
||||
return [
|
||||
'department' => $department,
|
||||
'category' => $category,
|
||||
'product' => $product,
|
||||
'customer' => $customer,
|
||||
];
|
||||
}
|
||||
|
||||
it('rejects department customer pricing when custom-only pricing is disabled', function (): void {
|
||||
api_test_covers('GET /superuser/department/customer-pricing', 'validation');
|
||||
|
||||
$fixture = department_customer_pricing_setup(['custom_pricing_only' => 0]);
|
||||
$session = api_fixtures()->createUserSession(['superuser_fetch_department_customer_pricing']);
|
||||
|
||||
$response = api_client()->get(
|
||||
'/superuser/department/customer-pricing?department_id=' . (int)$fixture['department']['id'] .
|
||||
'&user_id=' . (int)$fixture['customer']['id'],
|
||||
$session['headers']
|
||||
);
|
||||
|
||||
$response
|
||||
->assertStatus(409)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false);
|
||||
|
||||
expect($response->data()['code'] ?? null)->toBe('department_customer_pricing_disabled');
|
||||
});
|
||||
|
||||
it('sets and applies department-specific customer discounts without legacy fallback', function (): void {
|
||||
api_test_covers('GET /superuser/department/customer-pricing', 'happy');
|
||||
api_test_covers('PUT /superuser/department/customer-pricing', 'happy');
|
||||
api_test_covers('GET /products', 'pricing');
|
||||
|
||||
$fixture = department_customer_pricing_setup();
|
||||
api_fixtures()->createPriceOverride([
|
||||
'user_id' => $fixture['customer']['id'],
|
||||
'is_category' => 0,
|
||||
'product_or_category_id' => (string)$fixture['product']['id'],
|
||||
'percentage' => 80,
|
||||
]);
|
||||
|
||||
$session = api_fixtures()->createUserSession([
|
||||
'superuser_fetch_department_customer_pricing',
|
||||
'superuser_set_department_customer_pricing',
|
||||
'list_products',
|
||||
'department_access_' . (int)$fixture['department']['id'],
|
||||
]);
|
||||
|
||||
$updated = api_client()->put('/superuser/department/customer-pricing', [
|
||||
'department_id' => $fixture['department']['id'],
|
||||
'user_id' => $fixture['customer']['id'],
|
||||
'overrides' => [
|
||||
[
|
||||
'is_category' => false,
|
||||
'product_or_category_id' => $fixture['product']['id'],
|
||||
'discount' => 25,
|
||||
'fixed_price' => null,
|
||||
],
|
||||
],
|
||||
], $session['headers']);
|
||||
|
||||
$updated
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect($updated->data()['overrides'][0]['percentage'] ?? null)->toBe(25);
|
||||
expect($updated->data()['categories'][0]['products'][0]['effective_price'] ?? null)->toBe(750);
|
||||
|
||||
$byCustomerNumber = api_client()->get(
|
||||
'/superuser/department/customer-pricing?department_id=' . (int)$fixture['department']['id'] .
|
||||
'&customer_number=' . (int)$fixture['customer']['customer_number'],
|
||||
$session['headers']
|
||||
);
|
||||
$byCustomerNumber
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
expect($byCustomerNumber->data()['customer']['id'] ?? null)->toBe((int)$fixture['customer']['id']);
|
||||
|
||||
$productResponse = api_client()->get(
|
||||
'/products?final_price=true&id=' . (int)$fixture['product']['id'] .
|
||||
'&department_id=' . (int)$fixture['department']['id'] .
|
||||
'&customer_id=' . (int)$fixture['customer']['customer_number'],
|
||||
$session['headers']
|
||||
);
|
||||
|
||||
$productResponse
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect((int)($productResponse->data()['price'] ?? 0))->toBe(750);
|
||||
});
|
||||
|
||||
it('limits department customer pricing to assigned limited-backoffice departments', function (): void {
|
||||
api_test_covers('GET /limited-backoffice/departments/{departmentId}/customer-pricing', 'auth');
|
||||
api_test_covers('PUT /limited-backoffice/departments/{departmentId}/customer-pricing', 'auth');
|
||||
|
||||
$fixture = department_customer_pricing_setup();
|
||||
$otherFixture = department_customer_pricing_setup(['name' => 'Denied Scoped Customer Pricing Department']);
|
||||
$session = api_fixtures()->createUserSession([
|
||||
limited_backoffice_service::PERMISSION_ACCESS,
|
||||
limited_backoffice_service::PERMISSION_MANAGE_PRICES,
|
||||
'department_access_' . (int)$fixture['department']['id'],
|
||||
]);
|
||||
|
||||
api_client()
|
||||
->get(
|
||||
'/limited-backoffice/departments/' . (int)$otherFixture['department']['id'] .
|
||||
'/customer-pricing?user_id=' . (int)$otherFixture['customer']['id'],
|
||||
$session['headers']
|
||||
)
|
||||
->assertStatus(403)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMissingPermissions(['department_access_' . (int)$otherFixture['department']['id']]);
|
||||
|
||||
$updated = api_client()->put(
|
||||
'/limited-backoffice/departments/' . (int)$fixture['department']['id'] . '/customer-pricing',
|
||||
[
|
||||
'user_id' => $fixture['customer']['id'],
|
||||
'overrides' => [
|
||||
[
|
||||
'is_category' => true,
|
||||
'product_or_category_id' => (string)$fixture['category']['id'],
|
||||
'discount' => 30,
|
||||
],
|
||||
],
|
||||
],
|
||||
$session['headers']
|
||||
);
|
||||
|
||||
$updated
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
expect($updated->data()['overrides'][0]['percentage'] ?? null)->toBe(30);
|
||||
expect($updated->data()['categories'][0]['products'][0]['effective_price'] ?? null)->toBe(700);
|
||||
});
|
||||
@@ -1837,6 +1837,7 @@ final class ApiFixtures
|
||||
$this->deleteWhereIfPossible('tokens', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('user_key_value_pairs', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('price_overrides', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('department_customer_price_overrides', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('limited_backoffice_employees', ['user_id' => $userId]);
|
||||
$this->deleteWhereIfPossible('customer_default_department', ['customer_number' => $customerNumber]);
|
||||
$this->deleteWhereIfPossible('customer_fixed_pricing', ['customer_number' => $customerNumber]);
|
||||
|
||||
@@ -810,6 +810,23 @@ CREATE TABLE IF NOT EXISTS `price_overrides` (
|
||||
KEY `idx_price_overrides_user_id` (`user_id`),
|
||||
KEY `idx_price_overrides_lookup` (`user_id`, `is_category`, `product_or_category_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
SQL,
|
||||
'department_customer_price_overrides' => <<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `department_customer_price_overrides` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`department_id` INT NOT NULL,
|
||||
`user_id` INT NOT NULL,
|
||||
`is_category` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`product_or_category_id` VARCHAR(191) NOT NULL,
|
||||
`percentage` INT NOT NULL DEFAULT 0,
|
||||
`fixed_price` INT NULL DEFAULT NULL,
|
||||
`created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_department_customer_price_overrides_lookup` (`department_id`, `user_id`, `is_category`, `product_or_category_id`),
|
||||
KEY `idx_department_customer_price_overrides_department` (`department_id`),
|
||||
KEY `idx_department_customer_price_overrides_user` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
SQL,
|
||||
'products_options' => <<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `products_options` (
|
||||
|
||||
Reference in New Issue
Block a user