From b565364d52c379af14d91faac41d543db1bb3da9 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Fri, 13 Nov 2026 16:32:37 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=80=D0=B0=D1=81=D1=87=D1=91=D1=82=20=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D0=B4=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=BC=D1=8B=20=D0=BB=D0=BE=D1=8F=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B0=20sessid=20=D0=B2=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/component/handlers/eventshandlers.php | 17 +-- .../lib/service/loyaltybasketpriceapplier.php | 37 ----- .../lib/service/loyaltyservice.php | 141 +++++++++++++++++- intaro.retailcrm/options.php | 2 +- tests/lib/service/LoyaltyCalculationTest.php | 15 ++ 5 files changed, 161 insertions(+), 51 deletions(-) diff --git a/intaro.retailcrm/lib/component/handlers/eventshandlers.php b/intaro.retailcrm/lib/component/handlers/eventshandlers.php index 6574f9fe..e39566ae 100644 --- a/intaro.retailcrm/lib/component/handlers/eventshandlers.php +++ b/intaro.retailcrm/lib/component/handlers/eventshandlers.php @@ -365,11 +365,6 @@ public static function OnSaleOrderSavedHandler(Event $event): void $isLoyaltyOn = ConfigProvider::getLoyaltyProgramStatus() === 'Y'; $isBonusesIssetAndAvailable = $isBonusInput && $availableBonuses >= $bonusFloat; - /** @var array $calculateItemsInput */ - $calculateItemsInput = $isDataForLoyaltyDiscount - ? json_decode(htmlspecialchars_decode($_POST['calculate-items-input']), true) - : []; - if ($isNewOrder && $isLoyaltyOn && ($isDataForLoyaltyDiscount || $isBonusInput)) { self::$disableSaleHandler = true; @@ -377,11 +372,9 @@ public static function OnSaleOrderSavedHandler(Event $event): void $hlInfoBuilder = new LoyaltyDataBuilder(); $hlInfoBuilder->setOrder($order); - $discountInput = isset($_POST['loyalty-discount-input']) - ? (float) $_POST['loyalty-discount-input'] - : 0; - + $discountInput = 0.0; $loyaltyBonusMsg = 0; + $calculateItemsInput = $loyaltyService->getShopDiscountItems($order); $preliminaryPrices = ConfigProvider::getLoyaltyBonusesForBasketRules() === 'Y' ? self::getBasketPrices($order) @@ -402,7 +395,11 @@ public static function OnSaleOrderSavedHandler(Event $event): void //Если бонусов нет, но скидка по ПЛ есть if ($isDataForLoyaltyDiscount && !$isBonusInput) { - $loyaltyService->saveDiscounts($order, $calculateItemsInput); + $loyaltyDiscount = $loyaltyService->applyCalculatedDiscounts($order); + + if ($loyaltyDiscount !== null) { + $discountInput = $loyaltyDiscount; + } } $orderLoyaltyDataService->saveBonusAndDiscToOrderProps( diff --git a/intaro.retailcrm/lib/service/loyaltybasketpriceapplier.php b/intaro.retailcrm/lib/service/loyaltybasketpriceapplier.php index 3817901b..fb5ad834 100644 --- a/intaro.retailcrm/lib/service/loyaltybasketpriceapplier.php +++ b/intaro.retailcrm/lib/service/loyaltybasketpriceapplier.php @@ -118,43 +118,6 @@ public function apply(Order $order, $response, bool $runFinalAction = true): Res return $result; } - public function applySavedDiscounts(Order $order, array $calculateItemsInput): Result - { - $result = new Result(); - - try { - foreach ($order->getBasket() as $basketItem) { - $basketItemId = (string) $basketItem->getId(); - $calculateItem = $calculateItemsInput[$basketItemId] ?? null; - - if (!is_array($calculateItem) - || !isset($calculateItem['SUM_NUM'], $calculateItem['QUANTITY']) - || !is_numeric($calculateItem['SUM_NUM']) - || !is_numeric($calculateItem['QUANTITY']) - || (float) $calculateItem['QUANTITY'] <= 0 - ) { - $result->addError(new Error(sprintf('Saved loyalty discount for basket item %s is invalid.', $basketItemId))); - continue; - } - - $price = (float) $calculateItem['SUM_NUM'] / (float) $calculateItem['QUANTITY']; - $setResult = $basketItem->setFields([ - 'CUSTOM_PRICE' => 'Y', - 'DISCOUNT_PRICE' => $basketItem->getBasePrice() - $price, - 'PRICE' => $price, - ]); - - if (!$setResult->isSuccess()) { - $result->addErrors($setResult->getErrors()); - } - } - } catch (\Throwable $exception) { - $result->addError(new Error($exception->getMessage())); - } - - return $result; - } - /** * @param mixed $calculatedItem * diff --git a/intaro.retailcrm/lib/service/loyaltyservice.php b/intaro.retailcrm/lib/service/loyaltyservice.php index 4b19b32a..49ee46fd 100644 --- a/intaro.retailcrm/lib/service/loyaltyservice.php +++ b/intaro.retailcrm/lib/service/loyaltyservice.php @@ -491,16 +491,151 @@ public function calculateOrderBasket(array $orderArResult, LoyaltyCalculateRespo /** * @param \Bitrix\Sale\Order $order - * @param array $calculateItemsInput + * + * @return float|null */ - public function saveDiscounts(Order $order, array $calculateItemsInput): void + public function applyCalculatedDiscounts(Order $order): ?float { try { - $result = (new LoyaltyBasketPriceApplier())->applySavedDiscounts($order, $calculateItemsInput); + $basket = $order->getBasket(); + + if ($basket === null) { + $this->logger->write( + 'Loyalty discount was not applied because the basket is empty.', + Constants::LOYALTY_ERROR + ); + + return null; + } + + $basketItems = []; + $calculateItems = []; + $shopDiscountTotal = 0.0; + + foreach ($basket as $basketItem) { + $basketItemId = (string) $basketItem->getId(); + + if ($basketItemId === '' || $basketItemId === '0') { + continue; + } + + $quantity = (float) $basketItem->getQuantity(); + $shopDiscountTotal += (float) $basketItem->getDiscountPrice() * $quantity; + $basketItems[] = $basketItem; + $calculateItems[] = [ + 'ID' => $basketItemId, + 'XML_ID' => (string) $basketItem->getField('PRODUCT_XML_ID'), + 'BASE_PRICE' => (float) $basketItem->getBasePrice(), + 'PRICE' => (float) $basketItem->getPrice(), + 'QUANTITY' => $quantity, + 'PRICE_TYPE_ID' => $basketItem->getField('PRICE_TYPE_ID'), + ]; + } + + if ($basketItems === []) { + $this->logger->write( + 'Loyalty discount was not applied because the basket has no saved items.', + Constants::LOYALTY_ERROR + ); + + return null; + } + + $calculation = $this->getLoyaltyCalculate($calculateItems, 0); + + if (!$calculation instanceof LoyaltyCalculateResponse + || !$calculation->success + || !isset($calculation->order->items) + ) { + $this->logger->write( + 'Loyalty discount was not applied because CRM calculation failed.', + Constants::LOYALTY_ERROR + ); + + return null; + } + + $result = (new LoyaltyBasketPriceApplier())->apply($order, $calculation, false); if (!$result->isSuccess()) { $this->logger->write($result->getErrorMessages(), Constants::LOYALTY_ERROR); + return null; + } + + return $this->resolveLoyaltyDiscount($calculation, $shopDiscountTotal); + } catch (Exception $exception) { + $this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR); + + return null; + } + } + + /** + * @param \Bitrix\Sale\Order $order + * + * @return array> + */ + public function getShopDiscountItems(Order $order): array + { + $items = []; + $basket = $order->getBasket(); + + if ($basket === null) { + return $items; + } + + foreach ($basket as $basketItem) { + $basketItemId = (string) $basketItem->getId(); + + if ($basketItemId === '' || $basketItemId === '0') { + continue; + } + + $items[$basketItemId] = [ + 'SHOP_ITEM_DISCOUNT' => round( + (float) $basketItem->getBasePrice() - (float) $basketItem->getPrice(), + 2 + ), + ]; + } + + return $items; + } + + /** + * @param \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse $calculation + * @param float $shopDiscountTotal + * + * @return float + */ + private function resolveLoyaltyDiscount(LoyaltyCalculateResponse $calculation, float $shopDiscountTotal): float + { + if (!is_array($calculation->calculations)) { + return 0.0; + } + + foreach ($calculation->calculations as $privilege) { + if ($privilege->maximum && $privilege->discount > 0) { + return max(0, round((float) $privilege->discount - $shopDiscountTotal, 2)); + } + } + + return 0.0; + } + + /** + * Переданный расчёт из браузера не используется. + * + * @param \Bitrix\Sale\Order $order + * @param array $calculateItemsInput + */ + public function saveDiscounts(Order $order, array $calculateItemsInput): void + { + unset($calculateItemsInput); + + try { + if ($this->applyCalculatedDiscounts($order) === null) { return; } diff --git a/intaro.retailcrm/options.php b/intaro.retailcrm/options.php index 923d3448..9ab8edde 100644 --- a/intaro.retailcrm/options.php +++ b/intaro.retailcrm/options.php @@ -265,7 +265,7 @@ } //update connection settings -if (isset($_POST['Update']) && ($_POST['Update'] === 'Y')) { +if (isset($_POST['Update']) && ($_POST['Update'] === 'Y') && check_bitrix_sessid()) { $error = null; $api_host = trim((string) ($_POST['api_host'] ?? '')); $api_key = trim((string) ($_POST['api_key'] ?? '')); diff --git a/tests/lib/service/LoyaltyCalculationTest.php b/tests/lib/service/LoyaltyCalculationTest.php index 855a05e7..75c40655 100644 --- a/tests/lib/service/LoyaltyCalculationTest.php +++ b/tests/lib/service/LoyaltyCalculationTest.php @@ -2,6 +2,7 @@ use Intaro\RetailCrm\Component\Json\Deserializer; use Intaro\RetailCrm\Component\Json\Serializer; +use Intaro\RetailCrm\Model\Api\LoyaltyCalculation; use Intaro\RetailCrm\Model\Api\OrderProduct; use Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse; use Intaro\RetailCrm\Model\Api\SerializedLoyaltyOrder; @@ -107,4 +108,18 @@ public function testReadsOfferId(): void self::assertSame('16', $parsed->offer->externalId); } + + public function testResolveLoyaltyDiscountSubtractsShopDiscount(): void + { + $calculation = new LoyaltyCalculateResponse(); + $privilege = new LoyaltyCalculation(); + $privilege->maximum = true; + $privilege->discount = 300.0; + $calculation->calculations = [$privilege]; + $service = new LoyaltyService(); + $method = new ReflectionMethod(LoyaltyService::class, 'resolveLoyaltyDiscount'); + $method->setAccessible(true); + + self::assertSame(100.0, $method->invoke($service, $calculation, 200.0)); + } } From 78e42fcde852d9818f919594b671ba62c59ea115 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Fri, 13 Nov 2026 17:34:33 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=BA=D0=B8=D0=B4=D0=BA=D0=B0=20=D0=9F?= =?UTF-8?q?=D0=9B=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B8=20=D0=B1=D0=BE=D0=BD=D1=83=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B0=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/component/handlers/eventshandlers.php | 8 + .../lib/service/loyaltyservice.php | 141 +++++++++++------- intaro.retailcrm/options.php | 5 + 3 files changed, 104 insertions(+), 50 deletions(-) diff --git a/intaro.retailcrm/lib/component/handlers/eventshandlers.php b/intaro.retailcrm/lib/component/handlers/eventshandlers.php index e39566ae..05ec0636 100644 --- a/intaro.retailcrm/lib/component/handlers/eventshandlers.php +++ b/intaro.retailcrm/lib/component/handlers/eventshandlers.php @@ -380,6 +380,14 @@ public static function OnSaleOrderSavedHandler(Event $event): void ? self::getBasketPrices($order) : []; + if ($isDataForLoyaltyDiscount && $isBonusesIssetAndAvailable) { + $loyaltyDiscount = $loyaltyService->calculateLoyaltyDiscount($order, $bonusFloat); + + if ($loyaltyDiscount !== null) { + $discountInput = $loyaltyDiscount; + } + } + //Если есть бонусы if ($isBonusesIssetAndAvailable) { $applyBonusResponse = $loyaltyService->applyBonusesInOrder($order, $bonusFloat); diff --git a/intaro.retailcrm/lib/service/loyaltyservice.php b/intaro.retailcrm/lib/service/loyaltyservice.php index 49ee46fd..d29885a6 100644 --- a/intaro.retailcrm/lib/service/loyaltyservice.php +++ b/intaro.retailcrm/lib/service/loyaltyservice.php @@ -497,78 +497,119 @@ public function calculateOrderBasket(array $orderArResult, LoyaltyCalculateRespo public function applyCalculatedDiscounts(Order $order): ?float { try { - $basket = $order->getBasket(); - - if ($basket === null) { - $this->logger->write( - 'Loyalty discount was not applied because the basket is empty.', - Constants::LOYALTY_ERROR - ); + $loaded = $this->loadLoyaltyCalculation($order, 0); + if ($loaded === null) { return null; } - $basketItems = []; - $calculateItems = []; - $shopDiscountTotal = 0.0; - - foreach ($basket as $basketItem) { - $basketItemId = (string) $basketItem->getId(); + $result = (new LoyaltyBasketPriceApplier())->apply($order, $loaded['calculation'], false); - if ($basketItemId === '' || $basketItemId === '0') { - continue; - } + if (!$result->isSuccess()) { + $this->logger->write($result->getErrorMessages(), Constants::LOYALTY_ERROR); - $quantity = (float) $basketItem->getQuantity(); - $shopDiscountTotal += (float) $basketItem->getDiscountPrice() * $quantity; - $basketItems[] = $basketItem; - $calculateItems[] = [ - 'ID' => $basketItemId, - 'XML_ID' => (string) $basketItem->getField('PRODUCT_XML_ID'), - 'BASE_PRICE' => (float) $basketItem->getBasePrice(), - 'PRICE' => (float) $basketItem->getPrice(), - 'QUANTITY' => $quantity, - 'PRICE_TYPE_ID' => $basketItem->getField('PRICE_TYPE_ID'), - ]; + return null; } - if ($basketItems === []) { - $this->logger->write( - 'Loyalty discount was not applied because the basket has no saved items.', - Constants::LOYALTY_ERROR - ); + return $this->resolveLoyaltyDiscount($loaded['calculation'], $loaded['shopDiscountTotal']); + } catch (Exception $exception) { + $this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR); + return null; + } + } + + /** + * @param \Bitrix\Sale\Order $order + * @param float $bonuses + * + * @return float|null + */ + public function calculateLoyaltyDiscount(Order $order, float $bonuses = 0): ?float + { + try { + $loaded = $this->loadLoyaltyCalculation($order, $bonuses); + + if ($loaded === null) { return null; } - $calculation = $this->getLoyaltyCalculate($calculateItems, 0); + return $this->resolveLoyaltyDiscount($loaded['calculation'], $loaded['shopDiscountTotal']); + } catch (Exception $exception) { + $this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR); - if (!$calculation instanceof LoyaltyCalculateResponse - || !$calculation->success - || !isset($calculation->order->items) - ) { - $this->logger->write( - 'Loyalty discount was not applied because CRM calculation failed.', - Constants::LOYALTY_ERROR - ); + return null; + } + } - return null; - } + /** + * @param \Bitrix\Sale\Order $order + * @param float $bonuses + * + * @return array{calculation: \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse, shopDiscountTotal: float}|null + */ + private function loadLoyaltyCalculation(Order $order, float $bonuses): ?array + { + $basket = $order->getBasket(); - $result = (new LoyaltyBasketPriceApplier())->apply($order, $calculation, false); + if ($basket === null) { + $this->logger->write( + 'Loyalty discount was not applied because the basket is empty.', + Constants::LOYALTY_ERROR + ); - if (!$result->isSuccess()) { - $this->logger->write($result->getErrorMessages(), Constants::LOYALTY_ERROR); + return null; + } - return null; + $calculateItems = []; + $shopDiscountTotal = 0.0; + + foreach ($basket as $basketItem) { + $basketItemId = (string) $basketItem->getId(); + + if ($basketItemId === '' || $basketItemId === '0') { + continue; } - return $this->resolveLoyaltyDiscount($calculation, $shopDiscountTotal); - } catch (Exception $exception) { - $this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR); + $quantity = (float) $basketItem->getQuantity(); + $shopDiscountTotal += (float) $basketItem->getDiscountPrice() * $quantity; + $calculateItems[] = [ + 'ID' => $basketItemId, + 'XML_ID' => (string) $basketItem->getField('PRODUCT_XML_ID'), + 'BASE_PRICE' => (float) $basketItem->getBasePrice(), + 'PRICE' => (float) $basketItem->getPrice(), + 'QUANTITY' => $quantity, + 'PRICE_TYPE_ID' => $basketItem->getField('PRICE_TYPE_ID'), + ]; + } + + if ($calculateItems === []) { + $this->logger->write( + 'Loyalty discount was not applied because the basket has no saved items.', + Constants::LOYALTY_ERROR + ); return null; } + + $calculation = $this->getLoyaltyCalculate($calculateItems, $bonuses); + + if (!$calculation instanceof LoyaltyCalculateResponse + || !$calculation->success + || !isset($calculation->order->items) + ) { + $this->logger->write( + 'Loyalty discount was not applied because CRM calculation failed.', + Constants::LOYALTY_ERROR + ); + + return null; + } + + return [ + 'calculation' => $calculation, + 'shopDiscountTotal' => $shopDiscountTotal, + ]; } /** diff --git a/intaro.retailcrm/options.php b/intaro.retailcrm/options.php index 9ab8edde..f97eeec8 100644 --- a/intaro.retailcrm/options.php +++ b/intaro.retailcrm/options.php @@ -266,6 +266,11 @@ //update connection settings if (isset($_POST['Update']) && ($_POST['Update'] === 'Y') && check_bitrix_sessid()) { + if (!$USER->CanDoOperation('edit_other_settings') || $APPLICATION->GetGroupRight($mid) < 'W') { + $APPLICATION->AuthForm(GetMessage('ACCESS_DENIED')); + return; + } + $error = null; $api_host = trim((string) ($_POST['api_host'] ?? '')); $api_key = trim((string) ($_POST['api_key'] ?? ''));