timestamp = $this->getLastTimestamp(); // zpracovat objednavky foreach ($this->getItems() as $index => $item) { try { $this->processItem($item); } catch (UniqueConstraintViolationException $e) { } $this->timestamp = max((int) ($item['header']['TimeStamp'] ?? 0), 0); if (($index % 500) == 0) { $this->setLastTimestamp($this->timestamp); } } // ulozit timestamp if ($this->timestamp) { $this->setLastTimestamp($this->timestamp); } } /** * Kvuli doimportovani starych objednavek z prodejen. */ public function processWithCustomTimestamp(int $timestamp, callable $timestampUpdate): void { $this->timestamp = $timestamp; // zpracovat objednavky foreach ($this->getItems() as $index => $item) { try { $this->processItem($item); } catch (UniqueConstraintViolationException $e) { } $this->timestamp = max((int) ($item['header']['TimeStamp'] ?? 0), 0); if (($index % 500) == 0) { $timestampUpdate($this->timestamp); } } } public function processOrder(array $order, bool $force = true): void { if ($force) { $order['header']['force'] = true; } $this->processItem($order); } public function processSingleItem(int $drsId, bool $force = false): void { if ($order = $this->drsApi->getStoreOrder($drsId)) { if ($force) { $order['header']['force'] = true; } $this->processItem($order); } } /** * Zpracuje prodejku z DRSu. Bud vytvori novou objednavku, nebo aktualizuje jiz existujici objednavku na eshopu (rezervace). */ protected function processItem(array $item, bool $isOldOrder = false): void { if (!$isOldOrder && (empty($item['header']['DocumentNumber']) || (($item['header']['DocumentTypeDesc'] ?? '') !== 'SALE'))) { return; } $orderNumber = $item['header']['Description'] ?? ''; $description = explode('ESHOP:', $orderNumber); if ($description[1] ?? false) { $orderNumber = $description[1]; } // Pokud Je tam jen text nebo mezery, tak to necham fallbacknout na DocumentNumber if (!preg_match('~[0-9]+~', $orderNumber) || strpos($orderNumber, ' ') !== false) { $orderNumber = null; } $oldReceiptOrderNumber = 'P'.$item['header']['DocumentNumber']; if (empty($orderNumber)) { $dateCreated = new \DateTime($item['header']['RecCreated']); $orderNumber = 'P'.$dateCreated->format('ymd').'-'.$item['header']['DocumentNumber']; } // Objednavka z nejhracky, skipuju if ($isOldOrder && (StringUtil::startsWith($orderNumber, '2') || StringUtil::startsWith($orderNumber, '4'))) { return; } if (($item['data']['InvoiceCountryCode'] ?? 'CZ') === 'SK') { $item['header']['CurrencyISOCode'] = 'EUR'; } // mena objednavky $currency = $this->getOrderCurrency($item['header']['CurrencyISOCode'] ?? ''); $orX = [Operator::equals(['order_no' => $orderNumber])]; if (StringUtil::startsWith($oldReceiptOrderNumber, 'P')) { $orX[] = Operator::equals(['order_no' => $oldReceiptOrderNumber]); } $foundOrder = sqlQueryBuilder() ->select('id, order_no') ->from('orders') ->andWhere( Operator::orX($orX) ) ->execute()->fetchAssociative(); $orderId = $foundOrder['id'] ?? null; // import starych objednavek - muzou tam prijit novy objednavky, ktery vznikly uz u nas... a ty chci preskocit if ($isOldOrder && (StringUtil::startsWith($orderNumber, '122') || StringUtil::startsWith($orderNumber, '322'))) { return; } // objednavku uz mame v e-shopu, protoze je to rezervace provedena na e-shopu if ($orderId) { // pokud je to stara objednavak (ze stareho eshopu) if ($isOldOrder) { $this->updateOldOrder((int) $orderId, $item); } else { $this->updateShopOrder((int) $orderId, $item, $currency); // Zpetna aktualizace, aby se opravili cisla objednavek if ($foundOrder['order_no'] !== $orderNumber) { $this->updateOrderNumber((int) $orderId, $orderNumber); } } return; } // Vytvorim objednavku sqlGetConnection()->transactional(function () use ($item, $orderNumber, $currency, $isOldOrder) { // Nactu spravny deliveryType $deliveryTypeName = strip_tags($item['data']['NameDelivery'] ?? '').' - '.strip_tags($item['data']['NamePayment'] ?? ''); $deliveryType = $this->getDeliveryType(); if ($isOldOrder) { $deliveryType = null; } if ($deliveryType) { $deliveryTypeName = $deliveryType->name ?? ''; } // Najdu uzivatele $userId = null; $userName = ''; $userSurname = ''; if (!empty($item['data']['InvoiceName'])) { $nameParts = explode(' ', $item['data']['InvoiceName']); $userName = $nameParts[0]; unset($nameParts[0]); $userSurname = implode(' ', $nameParts); } if ($item['customer']['CustomerNumber'] ?? false) { if ($user = $this->getUser((string) $item['customer']['CustomerNumber'])) { $userId = $user->id; $userName = $user->name ?? $userName; $userSurname = $user->surname ?? $userSurname; } } if (empty($userName) && empty($userSurname)) { $userName = 'Prodej'; $userSurname = 'prodejna ('.$item['header']['DocumentNumber'].')'; } // Stav objednavky $status = $this->getOrderStatus($item, $isOldOrder); $dateHandle = null; // datum vyrizeni if ($status == $this->configuration->getOrderFinalStatus()) { try { $dateHandle = (new \DateTime($item['data']['DueDate'] ?? 'now'))->format('Y-m-d H:i:s'); } catch (\Throwable $e) { $dateHandle = (new \DateTime())->format('Y-m-d H:i:s'); } } $sellerId = $this->getSellerByBranchId($item['header']['SourceBranch'] ?? ''); // Vytvorim objednavku sqlQueryBuilder() ->insert('orders') ->directValues( [ 'id_user' => $userId, 'id_language' => $currency->getId() === 'EUR' ? 'sk' : 'cs', 'order_no' => $orderNumber, 'date_created' => $item['header']['RecCreated'], 'date_accept' => $dateHandle, 'date_handle' => $dateHandle, 'date_updated' => $item['header']['RecCreated'], 'currency' => $currency->getId(), 'status' => $status, 'status_storno' => ($item['header']['StornoDocument'] === '1' || ($item['data']['Status_WEB_objednavky'] ?? null) == 80) ? 1 : 0, 'status_payed' => 1, 'id_delivery' => $deliveryType ? $deliveryType->id : null, 'delivery_type' => $deliveryTypeName, 'flags' => $isOldOrder ? 'O' : 'NP', 'invoice_email' => $item['data']['Email'] ?? '', 'invoice_name' => $userName, 'invoice_surname' => $userSurname, 'invoice_country' => $item['data']['InvoiceCountryCode'] ?? 'CZ', 'note_admin' => json_encode([ 'transactionPrintout' => $item['data']['TransactionPrintout'] ?? null, 'delivery_data' => [ 'seller_id' => $sellerId, ], ]), ] )->execute(); $orderId = (int) sqlInsertId(); // Vytvarim mapping do tabulky sqlQueryBuilder() ->insert('drs_orders') ->directValues( [ 'id_drs' => $orderNumber, 'id_order' => $orderId, 'data' => json_encode( [ 'originalDocumentNumber' => $item['header']['DocumentNumber'], 'type' => 'sale', 'hasReceipt' => 1, 'completed' => 1, 'recId' => $item['header']['TimeStamp'], ] ), ] )->execute(); // insertnu polozky objednavky $this->insertShopOrderItems($orderId, $item, $currency); $order = new \Order(); $order->createFromDB($orderId); // ulozim k objednavce kupon, ktery v ni byl pouzit $this->setOrderCoupon($order, $item, $sellerId); // zavolam recalculate na objednavce, aby sedela total price $order->recalculate(); if ($isOldOrder) { $order->logHistory('Stará objednávka'); } else { $order->logHistory('[DRS] Nákup přes pokladnu'); } // zavolam update order, ktery prida body za objednavku $this->updateShopOrder($orderId, $item, $currency, true); return $order; }); } protected function getItems(): iterable { if ($this->timestamp === null) { // od roku 2020 $this->timestamp = 38096494; } do { $hasData = false; foreach ($this->drsApi->getStoreOrders($this->timestamp) as $item) { $hasData = true; yield $item; } } while ($hasData === true); } private function getOrderStatus(array $item, bool $isOldOrder): int { if ($isOldOrder) { switch ($item['data']['Status_WEB_objednavky'] ?? null) { case null: case '': return 0; case 10: // Přijatá return 1; case 40: // Zpracovává se return 2; case 50: // Vyřízeno return 4; case 60: // Připraveno k osobnímu odběru return 5; case 70: // ukončená return 6; case 80: // stornovaná return 6; case 90: // převoz return 3; case 91: // nekompletní return 7; } } return $this->configuration->getOrderFinalStatus(); } private function getSellerByBranchId(string $branchId): ?int { $sellerId = sqlQueryBuilder() ->select('id') ->from('sellers') ->where( Operator::equals( [ JsonOperator::value('data', 'branchId') => $branchId, ] ) )->execute()->fetchOne(); if (!$sellerId) { return null; } return (int) $sellerId; } private function getUser(string $customerId): ?\User { $data = sqlQueryBuilder() ->select('u.*') ->from('drs_users', 'du') ->join('du', 'users', 'u', 'du.id_user = u.id') ->where(Operator::equals(['du.id_drs' => $customerId])) ->execute()->fetchAssociative(); if (!$data) { return null; } $user = new \User(); $user->loadData($data); return $user; } private function getDeliveryType(): ?\DeliveryType { foreach (\DeliveryType::getAll() as $deliveryType) { if ($deliveryType->getDelivery() instanceof \OdberNaProdejne && $deliveryType->getPayment() instanceof \Hotovost) { return $deliveryType; } } return null; } /** * Aktualizuje uz existujici objednavku v e-shopu. * * $isNewOrder - znamená, zda je to nákup vytvořený na prodejně a nebo rezervace vytvořená přes e-shop */ private function updateShopOrder(int $orderId, array $item, Currency $currency, bool $isNewOrder = false): void { $order = \Order::get($orderId); // objednavka uz byla sesynchronizovana z pokladny if ($order->getData('drsPos') && !($item['header']['force'] ?? false)) { return; } $orderDateHandle = $order->date_handle; // Tohle nedelam u objednavek, ktere importuju primo z DRSu (prodejky). Tuhle cast musim volat pouze u rezervaci, ktere byly // vytvoreny pres e-shop if (!$isNewOrder) { // ulozim uctenku $order->setData('transactionPrintout', $item['data']['TransactionPrintout'] ?? null); // pokud objednavka nema uzivatele, ale na prodejne nakoupila na zakaznikou kartu, tak potrebuju k objednavce uzivatele pridat if (!$order->id_user && !empty($item['customer']['CustomerNumber'])) { if ($user = $this->getUser((string) $item['customer']['CustomerNumber'])) { sqlQueryBuilder() ->update('orders') ->directValues( [ 'id_user' => $user->id, ] ) ->where(Operator::equals(['id' => $order->id])) ->execute(); $order->id_user = $user->id; } } // ulozit poukaz, ktery byl pouzity na pokladne $this->setOrderCoupon($order, $item); // aktualizovat polozky objednavky podle dat v prodejce $this->updateShopOrderItems($order, $item, $currency); // zaloguju s cim to bylo sparovane $order->logHistory( sprintf('[DRS] Objednávka byla spárovaná s prodejkou %s', $item['header']['DocumentNumber'] ?? '') ); // prepnu objednavku do finalniho stavu, pokud v nem jeste neni a neni to prave vytvorena objednavka $order->changeStatus( $this->configuration->getOrderFinalStatus(), null, false ); } // Starsim objednavkam body nechceme pricitat, protoze to jsou historicke objednavky, ktere jsme jen doimportovali do shopu // pokud je to ale stara objednavka, ktera jeste nebyla vyrizena, tak tam ty body pricist chceme if ($this->bonusComputer && $order->date_created > (new \DateTime('2022-10-05 00:00:00')) || (($order->getFlags()['O'] ?? false) && !$orderDateHandle) || ($item['header']['forcePoints'] ?? false)) { $this->bonusComputer->checkBonusPointsEarningDiscount($order); $this->bonusComputer->updateBonusPoints($order); $discountsToDelete = array_map(fn ($action) => $action['id_order_discount'], $order->getPurchaseState()->getActions('bonus_points') ?: []); $discounts = $order->getData('discounts'); if ($discounts && is_null($discounts['extra_bonus_points'] ?? null)) { sqlQueryBuilder() ->delete('order_discounts_orders') ->where(Operator::equals(['id_order' => $orderId])) ->andWhere(Operator::inIntArray($discountsToDelete, 'id_order_discount')) ->execute(); } $points = $this->bonusComputer->countBonusPoints($order->getPurchaseState()); $received = sqlQueryBuilder() ->select('bp.id') ->from('bonus_points', 'bp') ->andWhere(Operator::equals(['bp.id_order' => $order->id, 'bp.id_user' => $order->id_user])) ->andWhere('bp.points > 0') ->sendToMaster() ->execute()->fetchOne(); if ($received) { // body byly pripsany standardne (inactive) - potrebujeme je smazat // a pridat pomoci setOrderBonusPoints, aby byly Aktivní a s jinou poznamkou sqlQueryBuilder()->delete('bonus_points')->where(Operator::equals(['id' => $received]))->execute(); } else { // body nebyly pripsany, protoze viz engine/bundles/External/PompoBundle/BonusProgram/Utils/BonusComputer.php:32 } $this->setOrderBonusPoints($order, $points, $isNewOrder); } // oznacim si, ze jsem objednavku uz sesynchronizoval z pokladny $order->setData('drsPos', 1); } public function updateShopOrderItems(\Order $order, array $drsOrder, Currency $currency): void { $diff = $order->getTotalPrice()->getPriceWithVat()->sub($this->getDRSOrderTotalPrice($drsOrder, $currency))->abs()->asFloat(); // nothing changed so do not process update if ($diff < 0.1 && empty($drsOrder['header']['force'])) { return; } sqlGetConnection()->transactional(function () use ($order, $drsOrder, $currency) { $currentItems = $order->getItems(); sqlQueryBuilder() ->delete('order_items') ->where(Operator::equals(['id_order' => $order->id])) ->execute(); $this->insertShopOrderItems($order->id, $drsOrder, $currency, $currentItems); $order->recalculate(); $order->logHistory('[DRS] Byla provedena aktualizace položek podle prodejky '.($drsOrder['header']['DocumentNumber'] ?? '')); }); // reload order items because items of order changed $order->setPurchaseState(null); $order->items = []; QueryHint::withRouteToMaster(fn () => $order->fetchItems()); } /** * @param OrderItem[]|null $currentItems */ public function insertShopOrderItems(int $orderId, array $drsOrder, Currency $currency, ?array $currentItems = null): void { foreach ($drsOrder['items'] ?? [] as $orderItem) { // polozka s 0 ks me nezajima if (((float) $orderItem['Amount']) == 0) { continue; } $info = $this->getPOSOrderItemInfo($orderItem, $currency); $note = [ 'isPOSItem' => true, 'isPOSDiscounted' => $this->isItemDiscounted($info), ]; // ulozim puvodni cenu a celkovou slevu do poznamky, aby se ta sleva zobrazila i v adminu if ($this->isItemDiscounted($info)) { $note['priceWithoutDiscounts'] = $info['originalPiecePriceWithVat']->asFloat(); $note['totalDiscount'] = $info['originalPiecePriceWithVat']->sub($info['piecePriceWithVat'])->asFloat(); } $orderItemId = null; // pokud znam aktualni polozku, tak z ni budu chtit zachovat udaje (id polozky, notu) if ($currentItem = ($currentItems[$orderItem['EshopItemID']] ?? null)) { $orderItemId = $currentItem->getId(); $note = array_merge($note, $currentItem->getNote()); } $note = json_encode($note); $insertData = [ 'id_order' => $orderId, 'id_product' => $info['productId'], 'pieces' => $info['pieces'], 'piece_price' => $info['price']->div($info['pieces']), 'total_price' => $info['price'], 'tax' => $orderItem['VATPercent'] ?? getAdminVat()['value'], 'descr' => $orderItem['Description'] ?? 'Položka', 'note' => $note, ]; if ($orderItemId) { $insertData['id'] = $orderItemId; } sqlQueryBuilder() ->insert('order_items') ->directValues($insertData) ->execute(); } } private function getDRSOrderTotalPrice(array $drsOrder, Currency $currency): \Decimal { $totalPrice = \DecimalConstants::zero(); foreach ($drsOrder['items'] ?? [] as $orderItem) { // polozka s 0 ks me nezajima if (((float) $orderItem['Amount']) == 0) { continue; } $info = $this->getPOSOrderItemInfo($orderItem, $currency); $totalPrice = $totalPrice->add($info['priceWithVat']); } return $totalPrice; } /** * Teoreticky se bude moct jednou smazat, protoze to slouzi pouze k doimportovani starych objednavek. * * Aktualizace staré objednavky v e-shopu. */ private function updateOldOrder(int $orderId, array $item): void { sqlQueryBuilder() ->update('orders') ->directValues( [ 'invoice_email' => $item['data']['Email'] ?? '', 'status' => $this->getOrderStatus($item, true), 'status_storno' => ($item['header']['StornoDocument'] === '1' || ($item['data']['Status_WEB_objednavky'] ?? null) == 80) ? 1 : 0, ] ) ->andWhere(Operator::equals(['id' => $orderId])) ->andWhere(Operator::findInSet(['O'], 'flags')) ->execute(); } private function updateOrderNumber(int $orderId, string $orderNumber): void { sqlQueryBuilder() ->update('orders') ->directValues( [ 'order_no' => $orderNumber, ] ) ->where(Operator::equals(['id' => $orderId])) ->execute(); } /** * Nastavi objednavce body podle obsahu objednavky. */ private function setOrderBonusPoints(\Order $order, \Decimal $points, bool $isNewOrder = false): void { if (!$order->id_user) { return; } sqlQueryBuilder() ->insert('bonus_points') ->directValues( [ 'id_user' => $order->id_user, 'points' => $points, 'date_created' => (new \DateTime())->format('Y-m-d H:i:s'), 'note' => $isNewOrder ? 'Body za nákup na prodejně' : 'Body za vyzvednutou rezervaci', 'status' => 'active', 'id_order' => $order->id, ] )->execute(); $this->eventDispatcher->dispatch( new UserBonusPointsUpdatedEvent($order->id_user) ); $order->logHistory(sprintf('Za tuto objednávku bylo na účet uživatele přičteno %s bodů', $points->asFloat())); } /** * Ulozi k objednavce pouzitou slevu / kupon, a u kuponu prida objednavku, ve ktery byl uplatnen. */ public function setOrderCoupon(\Order $order, array $item, ?int $sellerId = null): void { $coupons = $item['data']['ShopVoucherNumber'] ?? []; if (empty($coupons)) { return; } $orderId = $order->id; foreach ($coupons as $coupon) { try { $foundCoupon = sqlQueryBuilder() ->select('id, id_discount') ->from('discounts_coupons') ->where( Operator::equals( [ 'code' => $coupon, 'used' => 'Y', ] ) )->execute()->fetchAssociative(); if ($foundCoupon) { // aktualizuje u kuponu, ze byl pouziti v tehle objednavce sqlQueryBuilder() ->update('discounts_coupons') ->directValues( [ 'id_order_used' => $orderId, ] ) ->set('data', 'JSON_SET(COALESCE(data, "{}"), "$.sellerId", :sellerId)') ->where(Operator::equals(['id' => $foundCoupon['id']])) ->setParameter('sellerId', $sellerId) ->execute(); $usedOrderDiscountId = sqlQueryBuilder() ->select('id_order_discount') ->from('order_discounts_triggers') ->where( Operator::equals(['JSON_VALUE(data, \'$.generate_coupon\')' => $foundCoupon['id_discount']]) ) ->execute()->fetchOne(); // ulozim k objednavce, ze tam byla uplatnena sleva if ($usedOrderDiscountId) { sqlQueryBuilder() ->insert('order_discounts_orders') ->directValues( [ 'id_order' => $orderId, 'id_order_discount' => $usedOrderDiscountId, ] )->execute(); } // zaloguju do historie objednavky info o tom, ktery pouakz byl uplatneny $order->logHistory( comment: '[DRS] V objednávce byl uplatněný poukaz: '.$coupon ); } } catch (\Throwable $e) { $this->sentryLogger->captureException($e); } } } /** * Vrati informace o produktu v DRS prodejce, abych to byl schopny vlozit do nasi objednavky. */ private function getPOSOrderItemInfo(array $item, Currency $currency): array { $productId = (int) sqlQueryBuilder() ->select('id') ->from('products') ->where(Operator::equals(['code' => $item['VirtualArticleNumber'] ?? null])) ->execute()->fetchOne(); if (!$productId) { // Kdyz nenajdu produkt, tak to naparuju na produkt, aby tam byla aspon nejaka vazba kvuli pocitani bodu $productId = $this->getDummyProductId((string) ($item['VirtualArticleNumber'] ?? '')); if (in_array($productId, ['995003', '995001', '995002'])) { $productId = null; } } $vat = toDecimal((float) ($item['VATPercent'] ?? getAdminVat()['value'])); $roundPrice = function (\Decimal $value, bool $round = true) use ($currency): \Decimal { if (!$round) { return $value; } return roundPrice($value, -1, 'DB', null, $currency); }; $originalPriceWithVat = $roundPrice(toDecimal((float) $item['OriginalRetailPriceWithVAT'] ?? 0), false); $priceWithVat = $roundPrice(toDecimal((float) $item['RetailPriceWithVAT'] ?? 0), false); $originalPrice = $originalPriceWithVat->removeVat($vat); $price = $priceWithVat->removeVat($vat); $pieces = toDecimal((float) $item['Amount'] ?? 1); return [ 'productId' => $productId, 'originalPrice' => $originalPrice, 'originalPiecePrice' => $originalPrice->div($pieces), 'originalPriceWithVat' => $originalPriceWithVat, 'originalPiecePriceWithVat' => $originalPriceWithVat->div($pieces), 'price' => $price, 'piecePrice' => $price->div($pieces), 'priceWithVat' => $priceWithVat, 'piecePriceWithVat' => $priceWithVat->div($pieces), 'pieces' => $pieces, 'vat' => $vat, ]; } /** * Nektere produkty nemusi byt na e-shopu, takze jsou na e-shopu vytvorene "Dummy" produkty, ktere tyhle produkty reprezentuji. Je to primarne * kvuli pocitani bodu, abych to mel jednodussi. */ private function getDummyProductId(string $code): int { // pokud je to lego produkt if (StringUtil::startsWith($code, '22')) { return 45476; } return 45475; } #[Required] public function setBonusComputer(?BonusComputer $bonusComputer = null): void { $this->bonusComputer = $bonusComputer; } private function isItemDiscounted(array $info): bool { if ($info['piecePriceWithVat']->lowerThan($info['originalPiecePriceWithVat'])) { $diff = $info['originalPiecePriceWithVat']->sub($info['piecePriceWithVat']); if (!$diff->lowerThanOrEqual(toDecimal(0.1))) { return true; } } return false; } private function getOrderCurrency(string $currency): Currency { return Contexts::get(CurrencyContext::class)->getOrDefault($currency); } }