1
0
mirror of synced 2024-11-30 01:06:06 +03:00
bitrix-module/intaro.retailcrm/lib/service/loyaltyservice.php
Сергей Чазов 34e8105583
Loyalty (#241)
* Loyalty program
* fix dot bug for shops-exoprt option
* delete credantials request from services
* add CurlException for credentials catch
* add catching CurlException
* edit error msgs
* add supportind double bonuses
* add any step for bonus-input field in sale.order.ajax template
* recalculate bonuses
* fix bonus rounded
* strtoupper for params in icml
* change delivery service code
2022-03-02 15:40:53 +03:00

546 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* PHP version 7.1
*
* @category Integration
* @package Intaro\RetailCrm\Service
* @author RetailCRM <integration@retailcrm.ru>
* @license MIT
* @link http://retailcrm.ru
* @see http://retailcrm.ru/docs
*/
namespace Intaro\RetailCrm\Service;
use Bitrix\Main\Loader;
use Bitrix\Sale\BasketItemBase;
use Bitrix\Sale\Order;
use Exception;
use Intaro\RetailCrm\Component\ConfigProvider;
use Intaro\RetailCrm\Component\Constants;
use Intaro\RetailCrm\Component\Factory\ClientFactory;
use Intaro\RetailCrm\Component\ServiceLocator;
use Intaro\RetailCrm\Model\Api\LoyaltyAccount;
use Intaro\RetailCrm\Model\Api\OrderProduct;
use Intaro\RetailCrm\Model\Api\PriceType;
use Intaro\RetailCrm\Model\Api\Request\Loyalty\Account\LoyaltyAccountRequest;
use Intaro\RetailCrm\Model\Api\Request\Loyalty\LoyaltyCalculateRequest;
use Intaro\RetailCrm\Model\Api\Request\Order\Loyalty\OrderLoyaltyApplyRequest;
use Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse;
use Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse;
use Intaro\RetailCrm\Model\Api\SerializedOrder;
use Intaro\RetailCrm\Model\Api\SerializedOrderProduct;
use Intaro\RetailCrm\Model\Api\SerializedOrderProductOffer;
use Intaro\RetailCrm\Model\Api\SerializedOrderReference;
use Intaro\RetailCrm\Model\Api\SerializedRelationCustomer;
use Intaro\RetailCrm\Model\Bitrix\OrderLoyaltyData;
use Intaro\RetailCrm\Repository\CurrencyRepository;
use Intaro\RetailCrm\Repository\OrderLoyaltyDataRepository;
use Intaro\RetailCrm\Service\Exception\LpAccountsUnavailableException;
use Logger;
/**
* Class LoyaltyService
*
* @package Intaro\RetailCrm\Service
*/
class LoyaltyService
{
/**
* @var \Intaro\RetailCrm\Component\ApiClient\ClientAdapter
*/
private $client;
/**
* @var mixed
*/
private $site;
/**
* @var \Logger
*/
private $logger;
/**
* LoyaltyService constructor.
*
* @throws \Bitrix\Main\LoaderException
*/
public function __construct()
{
IncludeModuleLangFile(__FILE__);
$this->logger = Logger::getInstance();
$this->client = ClientFactory::createClientAdapter();
$this->site = ConfigProvider::getSitesAvailable();
Loader::includeModule('Catalog');
}
/**
* Выполняет запрос на применение бонусов по программе лояльности
*
* @link https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-orders-loyalty-apply
*
* @param int $orderId ID заказа
* @param float $bonusCount количество бонусов для списания
*
* @return \Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse|mixed|null
*/
public function sendBonusPayment(int $orderId, float $bonusCount): ?OrderLoyaltyApplyResponse
{
$request = new OrderLoyaltyApplyRequest();
$request->order = new SerializedOrderReference();
$request->order->externalId = $orderId;
$request->bonuses = $bonusCount;
$request->site = $this->site;
$result = $this->client->loyaltyOrderApply($request);
Utils::handleApiErrors($result);
return $result;
}
/**
* Возвращает расчет привилегий на основе корзины и количества бонусов для списания
*
* @link https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-loyalty-calculate
*
* @param array $basketItems корзина
* @param float $bonuses количество бонусов для списания
*
* @return \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse|mixed|null
*/
public function getLoyaltyCalculate(array $basketItems, float $bonuses = 0): ?LoyaltyCalculateResponse
{
global $USER;
$request = new LoyaltyCalculateRequest();
$request->order = new SerializedOrder();
$request->order->customer = new SerializedRelationCustomer();
$request->order->customer->id = $USER->GetID();
$request->order->customer->externalId = $USER->GetID();
$request->site = $this->site;
$request->bonuses = $bonuses;
foreach ($basketItems as $item) {
$product = new SerializedOrderProduct();
$fullPrice = $item['BASE_PRICE'] ?? $item['FULL_PRICE'];
$product->initialPrice = $fullPrice; //цена без скидки
if ($fullPrice > 0) {
$product->discountManualAmount = $fullPrice - $item['PRICE'];
}
$product->offer = new SerializedOrderProductOffer();
$product->offer->externalId = $item['ID'];
$product->offer->id = $item['ID'];
$product->offer->xmlId = $item['XML_ID'];
$product->quantity = $item['QUANTITY'];
$prices = ConfigProvider::getCrmPrices();
$product->priceType = new PriceType();
$serializePrice = unserialize($prices);
if (isset($serializePrice[$item['PRICE_TYPE_ID']])) {
$product->priceType->code = $serializePrice[$item['PRICE_TYPE_ID']];
}
$request->order->items[] = $product;
}
$result = $this->client->loyaltyCalculate($request);
if (isset($result->errorMsg) && !empty($result->errorMsg)) {
$this->logger->write($result->errorMsg, Constants::LOYALTY_ERROR);
}
return $result;
}
//TODO доделать метод проверки регистрации в ПЛ
/**
* Возвращает список участий в программе лояльности
*
* @link https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-loyalty-accounts
*
* @param int $idInLoyalty ID участия в программе лояльности
*
* @return null|\Intaro\RetailCrm\Model\Api\LoyaltyAccount
* @throws \Intaro\RetailCrm\Service\Exception\LpAccountsUnavailableException
*/
public function getLoyaltyAccounts(int $idInLoyalty): ?LoyaltyAccount
{
$request = new LoyaltyAccountRequest();
$request->filter->id = $idInLoyalty;
$request->filter->sites = $this->site;
$response = $this->client->getLoyaltyAccounts($request);
if ($response !== null && $response->success) {
if (!isset($response->loyaltyAccounts[0])) {
throw new LpAccountsUnavailableException();
}
return $response->loyaltyAccounts[0];
}
Utils::handleApiErrors($response);
return null;
}
/**
* Повторно отправляет бонусную оплату
*
* Используется при необходимости еще раз отправить смс
*
* @param int $orderId
*
* @return \Intaro\RetailCrm\Model\Bitrix\SmsCookie|bool
*/
public function resendBonusPayment(int $orderId)
{
/** @var CookieService $service */
$service = ServiceLocator::get(CookieService::class);
$bonusCount = $this->getTotalBonusCount($orderId);
if ($bonusCount === false || $bonusCount === 0) {
return false;
}
/** @var OrderLoyaltyApplyResponse $response */
$response = $this->sendBonusPayment($orderId, $bonusCount);
if ($response === null || !($response instanceof OrderLoyaltyApplyResponse)) {
return false;
}
if (
isset($response->verification, $response->verification->checkId)
&& empty($response->verification->verifiedAt)
) {
return $service->setSmsCookie('lpOrderBonusConfirm', $response->verification);
}
if (!empty($response->verification->verifiedAt)) {
$this->saveBonusDiscounts(Order::load($orderId), $response);
$this->setDebitedStatus($orderId, true);
return true;
}
return false;
}
/**
* @param int $orderId
* @param bool $newStatus
*/
public function setDebitedStatus(int $orderId, bool $newStatus): void
{
$repository = new OrderLoyaltyDataRepository();
$products = $repository->getProductsByOrderId($orderId);
if (is_array($products)) {
/** @var OrderLoyaltyData $product */
foreach ($products as $product) {
$product->isDebited = $newStatus;
$repository->edit($product);
}
}
}
/**
* Добавляет данные о программе лояльности в массив корзины
*
* @param array $basketData
* @param \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse $calculate
*
* @return array
*/
public function addLoyaltyToBasket(array $basketData, LoyaltyCalculateResponse $calculate): array
{
$totalRenderData = &$basketData['TOTAL_RENDER_DATA'];
$basketData['LP_CALCULATE_SUCCESS'] = $calculate->success;
$totalRenderData['WILL_BE_CREDITED'] = $calculate->order->bonusesCreditTotal;
foreach ($calculate->calculations as $privilege) {
if ($privilege->maximum && $privilege->creditBonuses === 0.0) {
$totalRenderData['LOYALTY_DISCOUNT']
= round($privilege->discount - $basketData['DISCOUNT_PRICE_ALL'], 2);
$totalRenderData['LOYALTY_DISCOUNT_FORMATED'] = $totalRenderData['LOYALTY_DISCOUNT']
. ' ' . GetMessage($totalRenderData['CURRENCY']);
$totalRenderData['PRICE'] -= $totalRenderData['LOYALTY_DISCOUNT'];//общая сумма со скидкой
$totalRenderData['PRICE_FORMATED'] = $totalRenderData['PRICE']
. ' ' . GetMessage($totalRenderData['CURRENCY']); //отформатированная сумма со скидкой
$totalRenderData['SUM_WITHOUT_VAT_FORMATED'] = $totalRenderData['PRICE_FORMATED'];
$basketData['allSum_FORMATED'] = $totalRenderData['PRICE_FORMATED'];
$basketData['allSum_wVAT_FORMATED'] = $totalRenderData['PRICE_FORMATED'];
$basketData['allSum'] = $totalRenderData['PRICE'];
$totalRenderData['DISCOUNT_PRICE_FORMATED'] = $privilege->discount
. ' ' . GetMessage($totalRenderData['CURRENCY']);
$totalRenderData['LOYALTY_DISCOUNT_DEFAULT'] = $basketData['DISCOUNT_PRICE_ALL']
. ' ' . GetMessage($totalRenderData['CURRENCY']);
}
}
foreach ($basketData['BASKET_ITEM_RENDER_DATA'] as $key => &$item) {
$item['WILL_BE_CREDITED_BONUS'] = $calculate->order->items[$key]->bonusesCreditTotal;
if ($calculate->order->items[$key]->bonusesCreditTotal === 0.0) {
$item['PRICE'] -= $calculate->order->items[$key]->discountTotal
- ($item['SUM_DISCOUNT_PRICE'] / $item['QUANTITY']);
$item['SUM_PRICE'] = $item['PRICE'] * $item['QUANTITY'];
$item['PRICE_FORMATED'] = $item['PRICE'] . ' ' . GetMessage($item['CURRENCY']);
$item['SUM_PRICE_FORMATED'] = $item['SUM_PRICE'] . ' ' . GetMessage($item['CURRENCY']);
$item['SHOW_DISCOUNT_PRICE'] = true;
$item['SUM_DISCOUNT_PRICE'] = $calculate->order->items[$key]->discountTotal
* $item['QUANTITY'];
$item['SUM_DISCOUNT_PRICE_FORMATED'] = $item['SUM_DISCOUNT_PRICE']
. ' '
. GetMessage($item['CURRENCY']);
$item['DISCOUNT_PRICE_PERCENT'] = round($item['SUM_DISCOUNT_PRICE']
/ (($item['FULL_PRICE'] * $item['QUANTITY']) / 100));
$item['DISCOUNT_PRICE_PERCENT_FORMATED'] = $item['DISCOUNT_PRICE_PERCENT'] . '%';
if (isset($item['COLUMN_LIST'])) {
foreach ($item['COLUMN_LIST'] as &$column) {
$column['VALUE'] = $column['CODE'] === 'DISCOUNT'
? $item['DISCOUNT_PRICE_PERCENT_FORMATED'] : $column['VALUE'];
}
unset($column);
}
}
}
unset($item);
return $basketData;
}
/**
* @param array $orderArResult
* @param \Intaro\RetailCrm\Model\Api\Response\Loyalty\LoyaltyCalculateResponse $calculate
*
* @return array
*/
public function calculateOrderBasket(array $orderArResult, LoyaltyCalculateResponse $calculate): array
{
/** @var \Intaro\RetailCrm\Model\Api\LoyaltyCalculation $privilege */
foreach ($calculate->calculations as $privilege) {
if ($privilege->maximum) {
$orderArResult['AVAILABLE_BONUSES'] = $privilege->maxChargeBonuses;
$jsDataTotal = &$orderArResult['JS_DATA']['TOTAL'];
//если уровень скидочный
if ($privilege->maximum && $privilege->discount > 0) {
//Персональная скидка
$jsDataTotal['LOYALTY_DISCOUNT'] = $orderArResult['LOYALTY_DISCOUNT_INPUT']
= round($privilege->discount - $jsDataTotal['DISCOUNT_PRICE'], 2);
//общая стоимость
$jsDataTotal['ORDER_TOTAL_PRICE'] -= $jsDataTotal['LOYALTY_DISCOUNT'];
//обычная скидка
$jsDataTotal['DEFAULT_DISCOUNT'] = $jsDataTotal['DISCOUNT_PRICE'];
$jsDataTotal['ORDER_TOTAL_PRICE_FORMATED'] = round($jsDataTotal['ORDER_TOTAL_PRICE'], 2)
. ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);
$jsDataTotal['DISCOUNT_PRICE'] += $jsDataTotal['LOYALTY_DISCOUNT'];
$jsDataTotal['DISCOUNT_PRICE_FORMATED'] = $jsDataTotal['DISCOUNT_PRICE']
. ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);
$jsDataTotal['ORDER_PRICE'] -= $jsDataTotal['LOYALTY_DISCOUNT'];
$jsDataTotal['ORDER_PRICE_FORMATED'] = $jsDataTotal['ORDER_PRICE']
. ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);
$iterator = 0;
foreach ($orderArResult['JS_DATA']['GRID']['ROWS'] as $key => &$item) {
$item['data']['SUM_NUM'] = $orderArResult['CALCULATE_ITEMS_INPUT'][$key]['SUM_NUM']
= $item['data']['SUM_BASE']
- ($calculate->order->items[$iterator]->discountTotal
* $item['data']['QUANTITY']);
$orderArResult['CALCULATE_ITEMS_INPUT'][$key]['QUANTITY'] = $item['data']['QUANTITY'];
$orderArResult['CALCULATE_ITEMS_INPUT'][$key]['SHOP_ITEM_DISCOUNT']
= round($item['data']['BASE_PRICE'] - $item['data']['PRICE'], 2);
$orderArResult['CALCULATE_ITEMS_INPUT'][$key]['BASE_PRICE']
= $item['data']['BASE_PRICE'];
$item['data']['SUM'] = $item['data']['SUM_NUM']
. ' ' . GetMessage($orderArResult['BASE_LANG_CURRENCY']);
$item['data']['DISCOUNT_PRICE'] = $calculate->order->items[$iterator]->discountTotal;
$iterator++;
}
unset($item);
$orderArResult['CALCULATE_ITEMS_INPUT']
= htmlspecialchars(json_encode($orderArResult['CALCULATE_ITEMS_INPUT']));
}
break;
}
}
$orderArResult['CHARGERATE'] = $calculate->loyalty->chargeRate;
$orderArResult['TOTAL_BONUSES_COUNT'] = $calculate->order->loyaltyAccount->amount;
$orderArResult['LP_CALCULATE_SUCCESS'] = $calculate->success;
$orderArResult['WILL_BE_CREDITED'] = $calculate->order->bonusesCreditTotal;
$currencyRepository = new CurrencyRepository();
$orderArResult['BONUS_CURRENCY'] = html_entity_decode($currencyRepository->getCurrencyFormatString());
return $orderArResult;
}
/**
* @param \Bitrix\Sale\Order $order
* @param array $calculateItemsInput
*/
public function saveDiscounts(Order $order, array $calculateItemsInput): void
{
try {
/** @var BasketItemBase $basketItem */
foreach ($order->getBasket() as $basketItem) {
$calcItemPosition = $calculateItemsInput[$basketItem->getId()];
$calculateItem = $calcItemPosition['SUM_NUM'] / $calcItemPosition['QUANTITY'];
$basketItem->setField('CUSTOM_PRICE', 'Y');
$basketItem->setField('DISCOUNT_PRICE', $basketItem->getBasePrice() - $calculateItem);
$basketItem->setField('PRICE', $calculateItem);
}
$order->save();
} catch (Exception $exception) {
$this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR);
}
}
/**
* @param \Bitrix\Sale\Order $order
* @param \Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse $response
*
* @return void|null
*/
public function saveBonusDiscounts(Order $order, OrderLoyaltyApplyResponse $response): void
{
try {
$basketItems = $order->getBasket();
if ($basketItems === null) {
$this->logger->write('No item in basket', Constants::LOYALTY_ERROR);
}
/** @var BasketItemBase $basketItem */
foreach ($basketItems as $key => $basketItem) {
/** @var OrderProduct $item */
$item = $response->order->items[$key];
$basePrice = $basketItem->getField('BASE_PRICE');
$basketItem->setField('CUSTOM_PRICE', 'Y');
$basketItem->setField('DISCOUNT_PRICE', $item->discountTotal);
$basketItem->setField('PRICE', $basePrice - $item->discountTotal);
}
$order->save();
} catch (Exception $exception) {
$this->logger->write($exception->getMessage(), Constants::LOYALTY_ERROR);
}
}
/**
* @param int $externalId
*
* @return float|null
*/
public function getInitialDiscount(int $externalId): ?float
{
$repository = new OrderLoyaltyDataRepository();
return $repository->getDefDiscountByProductPosition($externalId);
}
/**
* Списаны ли бонусы в заказе
*
* @param $orderId
*
* @return bool|null
*/
public function isBonusDebited($orderId): ?bool
{
$repository = new OrderLoyaltyDataRepository();
$products = $repository->getProductsByOrderId($orderId);
if ($products === null || count($products) === 0) {
return null;
}
foreach ($products as $product) {
if (!empty($product->checkId) && $product->isDebited === false) {
return false;
}
}
return true;
}
/**
* Добавляет оплату бонусами в заказ Битрикс (устанавливает кастомные цены)
*
* @param \Bitrix\Sale\Order $order
* @param float $bonusCount /бонусная скидка в рублях
*
* @return \Intaro\RetailCrm\Model\Api\Response\Order\Loyalty\OrderLoyaltyApplyResponse|null
*/
public function applyBonusesInOrder(Order $order, float $bonusCount): ?OrderLoyaltyApplyResponse
{
$response = $this->sendBonusPayment($order->getId(), $bonusCount);
if ($response->success) {
$this->saveBonusDiscounts($order, $response);
} else {
Utils::handleApiErrors($response);
return null;
}
return $response;
}
/**
* @param $orderId
*
* @return int|null
*/
private function getTotalBonusCount($orderId): ?int
{
$repository = new OrderLoyaltyDataRepository();
$products = $repository->getProductsByOrderId($orderId);
if ($products === null || count($products) === 0) {
return null;
}
foreach ($products as $product) {
if ($product->bonusCountTotal > 0) {
return $product->bonusCountTotal;
}
}
return null;
}
}