1
0
mirror of synced 2024-12-12 07:06:04 +03:00
bitrix-module/intaro.retailcrm/lib/service/loyaltyservice.php

547 lines
21 KiB
PHP
Raw Normal View History

<?php
/**
* @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\LoyaltyAccountApiFilterType;
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 = new LoyaltyAccountApiFilterType();
$request->filter->id = $idInLoyalty;
$request->filter->sites = is_array($this->site) ? $this->site : [$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;
}
}