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; } }