553 lines
21 KiB
PHP
553 lines
21 KiB
PHP
<?php
|
|
|
|
namespace KupShop\OrderDiscountBundle\Util;
|
|
|
|
use KupShop\KupShopBundle\Query\JsonOperator;
|
|
use KupShop\OrderDiscountBundle\Actions\Frontend\HandlerInterface;
|
|
use KupShop\OrderDiscountBundle\Actions\Frontend\HandlerWithPurchaseStateInterface;
|
|
use KupShop\OrderDiscountBundle\Entity\OrderDiscount;
|
|
use KupShop\OrderDiscountBundle\OrderDiscountLocator;
|
|
use KupShop\OrderDiscountBundle\Translations\OrderDiscountsTranslation;
|
|
use KupShop\OrderDiscountBundle\Triggers\GeneratedCouponTrigger;
|
|
use KupShop\OrderDiscountBundle\Triggers\UsesCountTrigger;
|
|
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
|
|
use KupShop\OrderingBundle\Exception\CartValidationException;
|
|
use KupShop\OrderingBundle\Util\Purchase\PurchaseUtil;
|
|
use Query\Operator;
|
|
use Query\Translation;
|
|
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
|
|
use Symfony\Component\Serializer\Serializer;
|
|
|
|
class DiscountManager
|
|
{
|
|
/**
|
|
* @var OrderDiscountLocator
|
|
*/
|
|
private $orderDiscountLocator;
|
|
/** @var PurchaseUtil */
|
|
private $purchaseUtil;
|
|
|
|
/** @var PurchaseState */
|
|
private $purchaseState;
|
|
|
|
private $actionHandlersData = [];
|
|
|
|
/** @var HandlerInterface[] */
|
|
private $actionHandlers = [];
|
|
|
|
private array $userMessages = [];
|
|
|
|
public function __construct(OrderDiscountLocator $orderDiscountLocator, PurchaseUtil $purchaseUtil)
|
|
{
|
|
$this->orderDiscountLocator = $orderDiscountLocator;
|
|
$this->purchaseUtil = $purchaseUtil;
|
|
}
|
|
|
|
public function getUserMessages(): array
|
|
{
|
|
return $this->userMessages;
|
|
}
|
|
|
|
public function setUserMessages(array $userMessages): void
|
|
{
|
|
$this->userMessages = $userMessages;
|
|
}
|
|
|
|
public function getOrderDiscountById(int $id): ?OrderDiscount
|
|
{
|
|
$orderDiscount = sqlQueryBuilder()
|
|
->select('*')
|
|
->from('order_discounts')
|
|
->where(Operator::equals(['id' => $id]))
|
|
->execute()->fetchAssociative();
|
|
|
|
if (!$orderDiscount) {
|
|
return null;
|
|
}
|
|
|
|
return $this->loadDiscountsEntities([$id => $orderDiscount])[$id] ?? null;
|
|
}
|
|
|
|
public function getOrderDiscount(array $data): OrderDiscount
|
|
{
|
|
$normalizers = [new ObjectNormalizer()];
|
|
$serializer = new Serializer($normalizers);
|
|
|
|
return $serializer->denormalize($data, OrderDiscount::class, null);
|
|
}
|
|
|
|
public function getActionHandlers()
|
|
{
|
|
return $this->actionHandlers;
|
|
}
|
|
|
|
public function getTriggerHandlers()
|
|
{
|
|
$handlers = [];
|
|
$discountsToCheck = $this->getDiscountForCheck();
|
|
$discountsEntities = $this->loadDiscountsEntities($discountsToCheck);
|
|
|
|
foreach ($discountsEntities as $Discount) {
|
|
foreach ($Discount->getTriggers() as $trigger) {
|
|
if (empty($handlers[$trigger['type']])) {
|
|
$triggerService = $this->orderDiscountLocator->getTrigger($trigger['type']);
|
|
if ($handler = $triggerService->getFrontendHandler()) {
|
|
$handlers[$trigger['type']] = $handler;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $handlers;
|
|
}
|
|
|
|
/**
|
|
* Check discounts triggers and apply actions.
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function recalculate($discountsToCheck = null): PurchaseState
|
|
{
|
|
if (is_null($discountsToCheck)) {
|
|
$discountsToCheck = $this->getDiscountForCheck();
|
|
}
|
|
|
|
$discountsEntities = $this->loadDiscountsEntities($discountsToCheck);
|
|
|
|
$userMessages = [];
|
|
$delayed_actions = [];
|
|
foreach ($discountsEntities as $Discount) {
|
|
$applicable = true;
|
|
$multiplier = null;
|
|
foreach ($Discount->getTriggers() as $trigger) {
|
|
$triggerService = $this->orderDiscountLocator->getTrigger($trigger['type']);
|
|
$applicable = $triggerService->isApplicable($this->purchaseState,
|
|
$Discount,
|
|
$trigger['data'],
|
|
$triggerService->getHandlerData($this->purchaseState));
|
|
if (!$applicable) {
|
|
/*if ($error_message = $triggerService->getErrorMessage($this->purchaseState, $trigger['data'])) {
|
|
$id = $triggerService->getType().$trigger['id'].'_warning';
|
|
$userMessages[$id] = ['message' => $error_message, 'severity' => 'warning'];
|
|
}*/
|
|
break;
|
|
}
|
|
if ($mul = $triggerService->getMultiplier($this->purchaseState, $Discount, $trigger['data'])) {
|
|
$trigger_type = $trigger['type'];
|
|
$multiplier = array_map(function ($v) use ($trigger_type) {
|
|
return [$trigger_type => $v];
|
|
}, $mul);
|
|
}
|
|
}
|
|
|
|
if ($applicable) {
|
|
foreach ($Discount->getActions() as $action) {
|
|
$actionService = $this->orderDiscountLocator->getAction($action['type']);
|
|
|
|
if ($actionService->getFrontendHandler()) {
|
|
$handler = clone $actionService->getFrontendHandler();
|
|
if ($handler instanceof HandlerWithPurchaseStateInterface) {
|
|
$handler->setPurchaseState($this->purchaseState);
|
|
}
|
|
$handler->setActionId($action['id'])
|
|
->setOrderDiscount($Discount)
|
|
->setActionData($action['data']);
|
|
|
|
$data = $this->actionHandlersData[$action['id']] ?? [];
|
|
|
|
// add handled data to action data
|
|
$action['data']['handled'] = $handler->handleData($data);
|
|
|
|
$this->actionHandlers[$action['id']] = $handler;
|
|
}
|
|
|
|
if ($delayed = $actionService->getDelayedExecution()) {
|
|
$delayed_actions[$delayed][$action['id']] = [$actionService, $Discount, $action, $multiplier];
|
|
continue;
|
|
}
|
|
|
|
if ($multiplier) {
|
|
$actionService->applyMultiple($this->purchaseState, $Discount, $action['data'], $multiplier);
|
|
} else {
|
|
$actionService->applyResult($this->purchaseState, $Discount, $action['data']);
|
|
}
|
|
|
|
$userMessages = array_merge($userMessages, $actionService->getUserMessages($action['id']));
|
|
$this->purchaseState->addAction($action['type'], $action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// po aplikovani vsech slev, jako posledni aplikujeme akce 'Zisk bodů' a potom akce 'Přidat body'
|
|
ksort($delayed_actions);
|
|
foreach ($delayed_actions as $actions) {
|
|
foreach ($actions as $item) {
|
|
[$actionService, $Discount, $action, $multiplier] = $item;
|
|
if ($multiplier) {
|
|
$actionService->applyMultiple($this->purchaseState, $Discount, $action['data'], $multiplier);
|
|
} else {
|
|
$actionService->applyResult($this->purchaseState, $Discount, $action['data']);
|
|
}
|
|
|
|
$userMessages = array_merge($userMessages, $actionService->getUserMessages($action['id']));
|
|
$this->purchaseState->addAction($action['type'], $action);
|
|
}
|
|
}
|
|
|
|
$this->setUserMessages($userMessages);
|
|
|
|
return $this->purchaseUtil->recalculateTotalPrices($this->purchaseState);
|
|
}
|
|
|
|
public function addUserMessages()
|
|
{
|
|
foreach ($this->userMessages as $key => $message) {
|
|
$message['data'] = ['id' => $key];
|
|
addUserMessage(...$message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return OrderDiscount[] $discountsEntities
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function loadDiscountsEntities($discountsArray)
|
|
{
|
|
$triggers = sqlQueryBuilder()->select('*')
|
|
->from('order_discounts_triggers')
|
|
->where(Operator::inIntArray(array_keys($discountsArray), 'id_order_discount'))
|
|
->execute()->fetchAll();
|
|
|
|
foreach ($triggers as $trigger) {
|
|
$trigger['data'] = json_decode($trigger['data'] ?? '', true) ?: [];
|
|
$discountsArray[$trigger['id_order_discount']]['triggers'][] = $trigger;
|
|
}
|
|
|
|
$actions = sqlQueryBuilder()->select('*')
|
|
->from('order_discounts_actions')
|
|
->where(Operator::inIntArray(array_keys($discountsArray), 'id_order_discount'))
|
|
->execute()->fetchAll();
|
|
|
|
foreach ($actions as $action) {
|
|
$action['data'] = json_decode($action['data'] ?? '', true) ?: [];
|
|
$discountsArray[$action['id_order_discount']]['actions'][] = $action;
|
|
}
|
|
|
|
return array_map(function ($discount) {
|
|
return $this->getOrderDiscount($discount);
|
|
}, $discountsArray);
|
|
}
|
|
|
|
public function getDiscountForCheck(?array $ignoreTriggers = null): array
|
|
{
|
|
$discountToCheck = sqlQueryBuilder()->select('od.*')
|
|
->from('order_discounts', 'od')
|
|
->orderBy('od.position');
|
|
|
|
$discountToSubstract = sqlQueryBuilder()
|
|
->select('odt.id_order_discount')
|
|
->from('order_discounts_triggers', 'odt');
|
|
|
|
foreach ($this->getTriggers() as $trigger) {
|
|
if ($ignoreTriggers && in_array($trigger::getType(), $ignoreTriggers)) {
|
|
continue;
|
|
}
|
|
$discountToSubstract->orWhere(
|
|
$trigger->getDiscountFilterSpec($this->purchaseState)
|
|
);
|
|
}
|
|
|
|
$discountToSubstract->andWhere($this->getActiveSpec());
|
|
|
|
$discountToCheck
|
|
->andWhere("od.id NOT IN ({$discountToSubstract->getSQL()})")
|
|
->andWhere($this->getActiveSpec())
|
|
->addParameters($discountToSubstract->getParameters(), $discountToSubstract->getParameterTypes())
|
|
->andWhere(Translation::coalesceTranslatedFields(OrderDiscountsTranslation::class));
|
|
|
|
return sqlFetchAll($discountToCheck->execute(), 'id');
|
|
}
|
|
|
|
public function setActionHandlersData(array $actionHandlersData)
|
|
{
|
|
$this->actionHandlersData = $actionHandlersData;
|
|
}
|
|
|
|
public function setPurchaseState(PurchaseState $purchaseState): void
|
|
{
|
|
$this->purchaseState = $purchaseState;
|
|
}
|
|
|
|
public function getActiveSpec($field = 'od.active')
|
|
{
|
|
$active = ['Y'];
|
|
if (getAdminUser()) {
|
|
$active[] = 'A';
|
|
}
|
|
|
|
return Operator::inStringArray($active, $field);
|
|
}
|
|
|
|
/**
|
|
* @param array $order_discounts
|
|
*
|
|
* @return bool
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function couponNumberExists(string $coupon, &$order_discounts = [])
|
|
{
|
|
// CouponTrigger
|
|
$SQL = sqlQueryBuilder()
|
|
->select('od.id, od.display_name, od.name')
|
|
->from('order_discounts_triggers', 'odt')
|
|
->leftJoin('odt', 'order_discounts', 'od', 'odt.id_order_discount = od.id')
|
|
->where(Operator::equals(['odt.type' => 'coupon']))
|
|
->andWhere($this->getActiveSpec())
|
|
->andWhere(JsonOperator::search('odt.data', $coupon))
|
|
->orderBy('od.position')
|
|
->execute();
|
|
|
|
if ($SQL->rowCount() > 0) {
|
|
$order_discounts = sqlFetchAll($SQL, 'id');
|
|
|
|
return true;
|
|
}
|
|
|
|
// GeneratedCouponTrigger
|
|
$SQL = sqlQueryBuilder()
|
|
->select('d.id, dc.id_order_purchased')
|
|
->from('discounts_coupons', 'dc')
|
|
->join('dc', 'discounts', 'd', 'd.id = dc.id_discount')
|
|
->where('dc.date_activated <= NOW() AND (dc.date_from IS NULL OR dc.date_from <= NOW()) AND (dc.date_to IS NULL OR dc.date_to >= NOW())')
|
|
->andWhere(Operator::equals(['dc.code' => $coupon, 'dc.used' => 'N']))
|
|
->execute();
|
|
|
|
$row = $SQL->fetch();
|
|
if (!$row) {
|
|
return false;
|
|
}
|
|
|
|
if ($id_order_purchased = $row['id_order_purchased']) {
|
|
$order = new \Order($id_order_purchased);
|
|
$order->createFromDB($id_order_purchased);
|
|
|
|
if (!$this->isOrderFinished($order)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ($generate_coupon = $row['id']) {
|
|
$SQL = sqlQueryBuilder()
|
|
->select('od.id, od.display_name, od.name')
|
|
->from('order_discounts_triggers', 'odt')
|
|
->leftJoin('odt', 'order_discounts', 'od', 'odt.id_order_discount = od.id')
|
|
->where(Operator::equals(['odt.type' => 'generated_coupon']))
|
|
->andWhere($this->getActiveSpec())
|
|
->andWhere(Operator::equals([JsonOperator::value('odt.data', 'generate_coupon') => $generate_coupon]))
|
|
->orderBy('od.position')
|
|
->execute();
|
|
|
|
if ($SQL->rowCount() > 0) {
|
|
$order_discounts = sqlFetchAll($SQL, 'id');
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return bool|null
|
|
*
|
|
* @throws CartValidationException
|
|
*/
|
|
public function isCouponValid(string $coupon, ?array $order_discounts = null)
|
|
{
|
|
if (is_null($order_discounts)) {
|
|
$this->couponNumberExists($coupon, $order_discounts);
|
|
}
|
|
|
|
if (!$order_discounts) {
|
|
return false;
|
|
}
|
|
|
|
$this->purchaseState->addCoupon($coupon);
|
|
|
|
$discountsToCheck = $this->getDiscountForCheck();
|
|
|
|
if ($intersect = array_intersect_key($discountsToCheck, $order_discounts)) {
|
|
$order_discounts = $intersect;
|
|
} else {
|
|
$discountsToCheck = $order_discounts;
|
|
}
|
|
|
|
$triggers_types = sqlQueryBuilder()->select('DISTINCT odt.type')
|
|
->from('order_discounts_triggers', 'odt')
|
|
->where(Operator::inIntArray(array_keys($discountsToCheck), 'odt.id_order_discount'))
|
|
->andWhere(Operator::not(Operator::equals(['odt.type' => UsesCountTrigger::getType()])))
|
|
->execute()->fetchAll();
|
|
$triggers_types = array_column($triggers_types, 'type');
|
|
|
|
$triggers_specs = [];
|
|
foreach ($this->orderDiscountLocator->getTriggers() as $trigger) {
|
|
if (in_array($trigger->getType(), $triggers_types)) {
|
|
$triggers_specs[] = $trigger->getDiscountFilterSpec($this->purchaseState);
|
|
}
|
|
}
|
|
$triggers_specs = array_filter($triggers_specs);
|
|
|
|
$false_triggers = [];
|
|
if (!empty($triggers_specs)) {
|
|
$triggers_specs = Operator::orX($triggers_specs);
|
|
$false_triggers = sqlFetchAll(
|
|
sqlQueryBuilder()->select('odt.id')
|
|
->from('order_discounts_triggers', 'odt')
|
|
->where(Operator::inIntArray(array_keys($discountsToCheck), 'odt.id_order_discount'))
|
|
->andWhere(Operator::not(Operator::equals(['odt.type' => UsesCountTrigger::getType()])))
|
|
->andWhere($triggers_specs)
|
|
->execute(),
|
|
'id');
|
|
}
|
|
|
|
$discountsEntities = $this->loadDiscountsEntities($discountsToCheck);
|
|
$valid = false;
|
|
$error_message = '';
|
|
foreach ($discountsEntities as $Discount) {
|
|
$applicable = true;
|
|
foreach ($Discount->getTriggers() as $trigger) {
|
|
$triggerService = $this->orderDiscountLocator->getTrigger($trigger['type']);
|
|
if (array_key_exists($trigger['id'], $false_triggers)) {
|
|
$applicable = false;
|
|
if (!$intersect || array_key_exists($Discount->getId(), $order_discounts)) {
|
|
$error_message = $triggerService->getErrorMessage($this->purchaseState, $trigger['data']);
|
|
if (empty($error_message)) {
|
|
$error_message = translate_shop('error', 'order')['coupon_not_valid'];
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
$applicable = $triggerService->isApplicable($this->purchaseState, $Discount, $trigger['data']);
|
|
if (!$applicable) {
|
|
if (!$intersect || array_key_exists($Discount->getId(), $order_discounts)) {
|
|
$error_message = $triggerService->getErrorMessage($this->purchaseState, $trigger['data']);
|
|
if (empty($error_message)) {
|
|
$error_message = translate_shop('error', 'order')['coupon_not_valid'];
|
|
}
|
|
}
|
|
if (!$intersect) {
|
|
throw new CartValidationException($error_message);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if ($applicable && $intersect) {
|
|
$this->purchaseState->addUsedDiscount($Discount->getId());
|
|
if (array_key_exists($Discount->getId(), $order_discounts)) {
|
|
$valid = true;
|
|
// proverit jestli je zakazano uplatnit více kódů na jednu objednávku
|
|
foreach ($Discount->getTriggers() as $trigger) {
|
|
if ($trigger['type'] == 'coupon') {
|
|
$multiple = $trigger['data']['multiple'] ?? 'N';
|
|
if ($multiple == 'N') {
|
|
$codes = array_map('strtolower', $trigger['data']['codes']);
|
|
$activeCoupons = array_map('strtolower', $this->purchaseState->getActiveCoupons());
|
|
$codes = array_intersect($activeCoupons, $codes);
|
|
$valid = (count($codes) == 1);
|
|
break;
|
|
}
|
|
}
|
|
if ($trigger['type'] == 'generated_coupon') {
|
|
$multiple = $trigger['data']['multiple'] ?? 'N';
|
|
if ($multiple == 'N') {
|
|
/** @var GeneratedCouponTrigger $triggerService */
|
|
$triggerService = $this->orderDiscountLocator->getTrigger($trigger['type']);
|
|
$valid = count($triggerService->getActiveCoupons($this->purchaseState, $trigger['data'])) < 2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$valid && !empty($error_message)) {
|
|
throw new CartValidationException($error_message);
|
|
}
|
|
|
|
return $valid;
|
|
}
|
|
|
|
protected function isOrderFinished(\Order $order)
|
|
{
|
|
if (!$order->isActive() || !$order->isPaid()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function getTriggers(): array
|
|
{
|
|
if (!$this->purchaseState) {
|
|
return [];
|
|
}
|
|
|
|
$triggers = [];
|
|
foreach ($this->orderDiscountLocator->getTriggers() as $trigger) {
|
|
if ($handler = $trigger->getFrontendHandler()) {
|
|
$handler->setPersistentData($this->purchaseState);
|
|
$persistentData = $handler->handleData($this->actionHandlersData);
|
|
$this->purchaseState->setCustomData([$trigger::getType() => $persistentData]);
|
|
}
|
|
$triggers[] = $trigger;
|
|
}
|
|
|
|
return $triggers;
|
|
}
|
|
|
|
public function setActionsFromUsedDiscounts(PurchaseState $purchaseState): void
|
|
{
|
|
$purchaseState->setActions([]);
|
|
foreach ($purchaseState->getUsedDiscounts() as $id_discount) {
|
|
if ($discount = $this->getOrderDiscountById($id_discount)) {
|
|
foreach ($discount->actions as $action) {
|
|
$purchaseState->addAction($action['type'], $action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function applyActions(PurchaseState $purchaseState, array $actions): PurchaseState
|
|
{
|
|
foreach ($actions as $action) {
|
|
if ($discount = $this->getOrderDiscountById($action['id_order_discount'])) {
|
|
$actionService = $this->orderDiscountLocator->getAction($action['type']);
|
|
$actionService->applyResult($purchaseState, $discount, $action['data'] ?? []);
|
|
}
|
|
}
|
|
|
|
return $purchaseState;
|
|
}
|
|
|
|
public function getCurrentActions($types = []): array
|
|
{
|
|
$actions = [];
|
|
|
|
$discountsToCheck = $this->getDiscountForCheck();
|
|
$discountsEntities = $this->loadDiscountsEntities($discountsToCheck);
|
|
foreach ($discountsEntities as $Discount) {
|
|
foreach ($Discount->getActions() as $action) {
|
|
if (empty($types) || in_array($action['type'], $types)) {
|
|
$actions[$action['type']][] = $action;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $actions;
|
|
}
|
|
}
|