Files
kupshop/bundles/External/PompoBundle/DRS/Synchronizer/OrderSynchronizer.php
2025-08-02 16:30:27 +02:00

764 lines
29 KiB
PHP

<?php
declare(strict_types=1);
namespace External\PompoBundle\DRS\Synchronizer;
use External\PompoBundle\DRS\Exception\DRSException;
use External\PompoBundle\Email\OrderInPersonReadyEmail;
use External\PompoBundle\Email\OrderInProgressEmail;
use External\PompoBundle\Email\OrderNotCompleteEmail;
use External\PompoBundle\Email\OrderSentEmail;
use External\PompoBundle\Util\Configuration;
use External\PompoBundle\Util\Order\OrderSynchronizerTrait;
use External\PompoBundle\Util\Order\PompoOrderUtil;
use External\PompoBundle\Util\Ordering\OrderingUtil;
use External\PompoBundle\Util\Ordering\OrderType;
use KupShop\OrderingBundle\Entity\Order\OrderItem;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use KupShop\SellerBundle\Utils\SellerUtil;
use Query\Operator;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\Service\Attribute\Required;
/**
* Synchronizace objednávek do DRSu a zpětné stahování stavů z DRSu do e-shopu.
*/
class OrderSynchronizer extends AbstractSynchronizer
{
use OrderSynchronizerTrait;
protected static $type = 'order';
/** @required */
public Configuration $configuration;
/** @required */
public OrderItemInfo $orderItemInfo;
/** @required */
public OrderingUtil $orderingUtil;
#[Required]
public PompoOrderUtil $pompoOrderUtil;
private ?SellerUtil $sellerUtil = null;
/** @required */
public function setSellerUtil(?SellerUtil $sellerUtil): void
{
$this->sellerUtil = $sellerUtil;
}
public function process(): void
{
$this->processOrdersToDRS();
$this->processOrdersFromDRS();
}
public function processOrdersToDRS(): void
{
if (isDevelopment()) {
return;
}
$forceSendSpec = 'JSON_VALUE(o.note_admin, "$.sendToDRS") = 1';
$qb = sqlQueryBuilder()
->select('o.id')
->from('orders', 'o')
->leftJoin('o', 'drs_orders', 'do', 'o.id = do.id_order')
// neni zapsana v DRSu
->andWhere('do.id_drs IS NULL')
// nestornovane objednavky
->andWhere(
Operator::orX(
Operator::equals(['o.status_storno' => 0]),
$forceSendSpec
)
)
// neni vyrizena
->andWhere(
Operator::not(
Operator::inIntArray(getStatuses('handled'), 'o.status')
)
)
// objednavka nema flag NP - Nakup na prodejne, nebo O - Historicka objednavka
->andWhere(
Operator::not(
Operator::findInSet(['NP', 'O'], 'o.flags', 'OR')
)
)
// Do DRSu zapisujeme ihned pouze ciste rezervace
// Nebo zapiseme objednavky, ktere maji nastaveny v datech sendToDRS = 1 - to muze nastavit DataGo synchronizace, aby se
// objednavka zapsala do DRSu
->andWhere(
Operator::orX(
$this->getOrderTypeSpec([OrderType::ORDER_RESERVATION]),
$forceSendSpec
)
);
foreach ($qb->execute() as $item) {
$order = \Order::get((int) $item['id']);
if (($order->getFlags()['NP'] ?? false) || ($order->getFlags()['O'] ?? false)) {
continue;
}
// kolikaty pokus zalozeni objednavky
if (!($insertTry = $order->getData('drsInsertTry'))) {
$insertTry = 0;
}
$insertTry++;
// nastavim $result na null, aby se mi do ActivityLogu nezapsal $result z predesle objednavky
$result = null;
try {
$operation = 'BookDocument';
$result = $this->drsApi->createOrder(
$this->getOrderData($order),
$operation
);
$documentNumber = $result['DocumentBookResult']['Result']['@attributes']['DocumentNumber'] ?? null;
sqlQueryBuilder()
->insert('drs_orders')
->directValues(
[
'id_drs' => $documentNumber,
'id_order' => $order->id,
'data' => json_encode(['response' => $result]),
]
)->execute();
$this->updateOrderStatusAfterDRSInsert($order, $documentNumber);
} catch (\Throwable $e) {
// objednavka je v DRSu uz zapsana - DRS nam mohl vratit nejakou chybu, ale objednavku potom zapsa, takze si potrebuju doplnit mapovani
if ($e instanceof DRSException && strpos($e->getMessage(), 'duplicate key') !== false) {
sqlQueryBuilder()
->insert('drs_orders')
->directValues(
[
'id_drs' => $order->order_no,
'id_order' => $order->id,
'data' => [],
]
)->execute();
$this->updateOrderStatusAfterDRSInsert($order, $order->order_no);
continue;
}
// aktualizuju pocet pokusu o zalozeni objednavky
$order->setData('drsInsertTry', $insertTry);
// logovat zacinam az kdyz se to nepovede po vice jak 2 pokusech, protoze se muze stat, ze se to na poprve
// zapise, ale kvuli DRSu, kterej nevrati info o tom, ze to zapsal, si nezapiseme mapovani a v dalsim
// runu to zkusime zapsat znova, kde zjistime, ze tam objednavka uz je a vytvorime mapovani, ale v ActivityLogu
// pak zbytecne otravuje chybo hlaska o nezapsani objednavky
if ($insertTry > 2) {
$this->logger->logException(
$e,
sprintf('[DRS] Objednávku %s se nepodařilo zapsat', $order->order_no),
[
'message' => $e->getMessage(),
'result' => $result ?? [],
]
);
}
}
}
}
public function processOrdersFromDRS(): void
{
$qb = sqlQueryBuilder()
->select('o.id, o.order_no, do.id_drs')
->from('orders', 'o')
->join('o', 'drs_orders', 'do', 'do.id_order = o.id')
->andWhere(Operator::equals(['o.status_storno' => 0]))
->andWhere('JSON_VALUE(COALESCE(do.data, "{}"), "$.completed") IS NULL')
// objednavka nema flag NP - Nakup na prodejne, nebo O - Historicka objednavka
->andWhere(
Operator::orX(
Operator::not(
Operator::findInSet(['NP', 'O'], 'o.flags', 'OR')
),
// TODO: tohle by se mohlo dat casem pryc
// Aktualizace stavu objednavek ze stareho e-shopu - aktualizuje to pouze dva mesice zpatky od spusteni
// pompa u nas
Operator::andX(
// beru pouze objednavky s flagem O
Operator::findInSet(['O'], 'o.flags'),
// objednavka nesmi byt ukoncena
Operator::not(
Operator::equals(['o.status' => $this->configuration->getOrderFinalStatus()])
),
// dva mesice zpet od spusteni eshopu u nas
'TIMESTAMPDIFF(MONTH, date_created, "2022-10-05 00:00:00") <= 2'
)
)
);
$orders = [];
foreach ($qb->execute() as $item) {
$orders[(int) $item['id']] = $item['order_no'];
}
// 10 Přijatá
// 50 Expedovaná
// 60 Připravená k osobnímu odběru
// 70 Hotová a ukončená
// 80 Zrušená
// 90 Převoz
// 91 Nekompletní
foreach (array_chunk(array_values($orders), 1000) as $chunk) {
foreach ($this->drsApi->getOrdersInfo($chunk) as $item) {
if (!($id = array_flip($orders)[$item['number']] ?? null)) {
continue;
}
$order = \Order::get((int) $id);
// prislo cislo baliku
if (!empty($item['packageNumber'])) {
$this->setOrderPackageNumber($order, $item['packageNumber']);
}
switch ($item['status']) {
// Objednavka se zpracovava
case '40':
$this->changeOrderStatus($order, 2, OrderInProgressEmail::getType());
break;
// Prevoz
case '90':
$this->changeOrderStatus($order, 3);
break;
// Objednavka pripravena k osobnimu vyzvednuti
case '60':
$this->changeOrderStatus($order, 5, OrderInPersonReadyEmail::getType());
break;
// Nekompletní - objednavka je pripravena k osobnimu prevzeti, ale neni kompletni, protoze nejake zbozi chybi
case '91':
// docasne vypnuto
// $this->updateOrderItemsFromDRS($order);
// pokud uz je ve finalnim stavu, tak ji neprepinam zpatky to nekompletniho stavu
if ($order->status != $this->configuration->getOrderFinalStatus()) {
$this->changeOrderStatus($order, 7, OrderNotCompleteEmail::getType());
}
break;
// Expedovaná
case '50':
$email = OrderSentEmail::getType();
if ($this->isOrderInPerson($order)) {
$email = null;
}
$this->changeOrderStatus($order, 4, $email);
break;
// Hotová a ukončená
case '70':
$order->logHistory('[DRS] Objednávka kompletní - Hotová a ukončená');
$this->changeOrderStatus($order, $this->configuration->getOrderFinalStatus());
$this->setOrderCompleted($order);
break;
// Objednavka zrusena
case '80':
$this->setOrderCompleted($order);
$order->storno(false);
break;
}
}
}
}
public function getOrderData(\Order $order): array
{
$branchId = $this->getOrderDestinationBranchId($order);
$data = [
'login' => [
'@attributes' => [
'LoggedUser' => '',
'user' => '0',
'passwd' => '',
'branch' => $this->getLoginBranch($order),
'CreationBranch' => $this->getCreationBranchId(),
],
'command' => [
'@attributes' => [
'action' => 'create',
],
'document' => [
'@attributes' => $this->getDocumentAttributes($order),
'text' => [
'@value' => $this->getOrderNumber($order),
'@attributes' => [
'number' => 1,
],
],
'item' => [],
'memo' => [
$this->getXMLMemoItem('Email', $order->invoice_email),
$this->getXMLMemoItem('Order_Branch', $branchId),
$this->getXMLMemoItem('Order_Paid', $order->isPaid() ? 'true' : 'false'),
$this->getXMLMemoItem('NameDelivery', $this->getDeliveryName($order)),
$this->getXMLMemoItem('NamePayment', $this->getPaymentName($order)),
$this->getXMLMemoItem('Puvod_zbozi', $this->getOriginOfGoods($order)),
$this->getXMLMemoItem('Poznamka', $this->getDRSOrderNote($order)),
$this->getXMLMemoItem('WayOfPayment', $this->getDeliveryTypeCode($order)),
$this->getXMLMemoItem('WayOfTransport', $this->getDeliveryTypeCode($order)),
// Natvrdo tady posilam CZK, protoze maji v DRSu nastaveny automaticky procesy, ktery s jinyma hodnotama asi nepocataji
// a zacalo to kvuli tomu delat nejaky problemy v DRSu
$this->getXMLMemoItem('CurrencyForeignISOCode', 'CZK'),
$this->getXMLMemoItem('CurrencyHomeISOCode', 'CZK'),
$this->getXMLMemoItem('CurrencyRate', '1'),
// $this->getXMLMemoItem('Status_WEB_objednavky', 'Přijatá', '10'),
$this->getXMLMemoItem('ContainsCorrectPrices', 'True'),
$this->getXMLMemoItem('InvoiceName', implode(' ', array_filter([$order->invoice_name, $order->invoice_surname]))),
$this->getXMLMemoItem('InvoiceCompany', $order->invoice_firm),
$this->getXMLMemoItem('InvoiceStreet', $order->invoice_street),
$this->getXMLMemoItem('InvoiceCity', $order->invoice_city),
$this->getXMLMemoItem('InvoiceZIP', $order->invoice_zip),
$this->getXMLMemoItem('InvoiceCountryCode', $order->invoice_country),
$this->getXMLMemoItem('InvoiceTIN', ''),
$this->getXMLMemoItem('InvoiceIN', ''),
$this->getXMLMemoItem('PhoneNumber', $order->invoice_phone),
$this->getXMLMemoItem('DeliveryName',
implode(' ', array_filter([$order->delivery_name, $order->delivery_surname]))),
$this->getXMLMemoItem('DeliveryCompany', $order->delivery_firm),
$this->getXMLMemoItem('DeliveryStreet', $order->delivery_street),
$this->getXMLMemoItem('DeliveryCity', $order->delivery_city),
$this->getXMLMemoItem('DeliveryZIP', $order->delivery_zip),
$this->getXMLMemoItem('DeliveryCountryCode', $order->delivery_country),
],
'Addresses' => [
'Address' => [
'@attributes' => [
'TypeRecUID' => '00000000-0000-0000-0000-000000000000',
'Name' => implode(' ', array_filter([$order->invoice_name, $order->invoice_surname])),
'Street' => $order->invoice_street,
'Country' => $order->invoice_country,
'HouseNumber' => '',
'City' => $order->invoice_city,
'Zip' => $order->invoice_zip,
'MatchCode' => '',
'AccountNr' => '',
'Note1' => '',
'Note2' => '',
'Note3' => '',
],
],
],
],
],
],
];
// Pokud je to objednavka se zavozem na prodejnu, tak musim plnit datum zavozu
if ($this->orderingUtil->getOrderType($order) === OrderType::ORDER_TRANSPORT_RESERVATION) {
// deliveryDate mam ulozeny v datech delivery_data na objednavce
$deliveryData = $order->getData('delivery_data');
if ($deliveryData['deliveryDate'] ?? false) {
$data['login']['command']['document']['memo'][] = $this->getXMLMemoItem(
'Datum_zavozu',
$deliveryData['deliveryDate'],
$deliveryData['deliveryDate']
);
}
}
// Pokud ma objednavka nastaveno sendToDRS - znamena to, ze je ve stavu prevoz nebo zrusena z DataGo, takze stav rovnou nastavim
if ($order->getData('sendToDRS')) {
if ($order->status_storno == 1) {
$data['login']['command']['document']['memo'][] = $this->getXMLMemoItem('Status_WEB_objednavky', 'Zrušená', '80');
} else {
$data['login']['command']['document']['memo'][] = $this->getXMLMemoItem('Status_WEB_objednavky', 'Převoz', '90');
}
}
$deliveryItem = null;
$index = 1;
// Pridam polozky objednavky
foreach ($order->fetchItems() as $orderItem) {
$isChargeItem = !empty($orderItem->getNote()['charge']);
if (!$isChargeItem && $this->orderItemInfo->getItemType($orderItem) === OrderItemInfo::TYPE_DELIVERY) {
$deliveryItem = $orderItem;
continue;
}
$data['login']['command']['document']['item'][] = $this->getXMLItem($order, $orderItem, $index++);
}
// Pridam dopravu a platbu do polozek
foreach ($this->getDeliveryAndPaymentItem($order, $deliveryItem) as $type => $item) {
$data['login']['command']['document']['item'][] = $this->getXMLDeliveryItem($order, $item, $type, $index++);
}
return $data;
}
protected function processItem(array $item): void
{
// TODO: Implement processItem() method.
}
protected function getItems(): iterable
{
return [];
}
public function setOrderCompleted(\Order $order): void
{
// mark as completed
sqlQueryBuilder()
->update('drs_orders')
->set('data', 'JSON_SET(COALESCE(data, "{}"), "$.completed", 1)')
->where(
Operator::equals(
[
'id_order' => $order->id,
]
)
)->execute();
}
public function getLoginBranch(\Order $order): string
{
if ($this->configuration->isNejhracka()) {
if ($order->getLanguage() === 'sk') {
return '404';
}
return '403';
}
if ($order->getLanguage() === 'sk') {
return '402';
}
return '401';
}
protected function updateOrderItemsFromDRS(\Order $order): void
{
$diffResult = $this->pompoOrderUtil->getOrderItemDifference($order);
if (!$diffResult['hasDiff']) {
return;
}
$this->pompoOrderUtil->updateOrderByDifference(
$order,
$diffResult
);
}
private function updateOrderStatusAfterDRSInsert(\Order $order, string $documentNumber): void
{
if ($order->status > 1) {
// odesilam ji do DRSu pozdeji - uz neni ve stavu nova, ale treba ve stavu prevoz
$order->logHistory(
sprintf('[DRS] Objednávka byla úspěšně nahrána do DRSu: %s', $documentNumber)
);
} else {
// odesilam do DRSu novou objednavku
$order->changeStatus(
1,
sprintf('[DRS] Objednávka byla úspěšně nahrána do DRSu: %s', $documentNumber),
false
);
}
}
private function getDRSOrderNote(\Order $order): string
{
$note = [];
if ($partId = $order->getData('orderPartId')) {
$note[] = 'Část '.$partId;
}
$note[] = $this->getOrderNote($order);
return implode('; ', array_filter($note));
}
private function getDeliveryAndPaymentItem(\Order $order, ?OrderItem $deliveryItem): array
{
$result = $this->splitDeliveryItem($order, $deliveryItem);
if ($result['delivery'] ?? false) {
$result['delivery']['code'] = $this->isOrderInPerson($order) ? '995003' : '995001';
$result['delivery']['ean'] = $this->isOrderInPerson($order) ? '2500000251117' : '2500000251115';
$result['delivery']['descr'] = $this->isOrderInPerson($order) ? 'Manipulační poplatek' : 'Poštovné a balné';
}
if ($result['payment'] ?? false) {
$result['payment']['code'] = '995002';
$result['payment']['ean'] = '2500000251116';
$result['payment']['descr'] = 'Doběrečné';
}
return $result;
}
private function getOrderDestinationBranchId(\Order $order): string
{
// $branchId = $this->getOrderDefaultBranchId($order);
$branchId = '';
// select branchId by selected seller
if ($seller = $this->orderingUtil->getOrderSeller($order)) {
if (!empty($seller['data']['branchId'])) {
$branchId = (string) $seller['data']['branchId'];
}
}
// for testing use branchId 9998
if (isDevelopment()) {
$branchId = '9998';
}
return $branchId;
}
private function getOriginOfGoods(\Order $order): string
{
if ($this->orderingUtil->getOrderType($order) === OrderType::ORDER_RESERVATION) {
return 'Prodejna';
}
return 'Centrala';
}
private function getDeliveryTypeCode(\Order $order): string
{
if ($deliveryType = $order->getDeliveryType()) {
return $deliveryType->getCustomData()['drs_code'] ?? '';
}
return '';
}
private function getDeliveryName(\Order $order): string
{
if ($deliveryType = $order->getDeliveryType()) {
if ($delivery = $deliveryType->getDelivery()) {
return $delivery->name;
}
}
return '';
}
private function getPaymentName(\Order $order): string
{
if ($deliveryType = $order->getDeliveryType()) {
if ($payment = $deliveryType->getPayment()) {
return $payment->getName();
}
}
return '';
}
private function getCreationBranchId(): string
{
if ($this->configuration->isNejhracka()) {
return '1673';
}
return '1363';
}
private function getOrderDefaultBranchId(\Order $order): string
{
if ($this->configuration->isNejhracka()) {
// nejhracka - SK
if ($order->getLanguage() === 'sk') {
return '404';
}
// nejhracka - CZ
return '403';
}
// pompo - SK
if ($order->getLanguage() === 'sk') {
return '402';
}
// pompo - CZ
return '401';
}
private function getXMLItem(\Order $order, OrderItem $item, int $index): array
{
$code = $item->getCode();
$ean = $item->getEAN();
$descr = $item->getDescr();
if ($this->isOrderItemTransportCharge($item)) {
$code = '995003';
$ean = '2500000251117';
$descr = '';
}
if ($chargeCode = $this->pompoOrderUtil->getOrderItemChargeCode($item)) {
$code = $chargeCode;
}
$attributes = [
[
'@attributes' => [
'Name' => 'ItemID',
'Value' => $item->getId(),
],
],
];
$itemInfo = $order->getItemInfo($item);
// pridam informaci o variante do poznamky polozky
if ($itemInfo['selectedVariation'] ?? false) {
$attributes[] = [
'@attributes' => [
'Name' => 'Poznamka_polozka',
'Value' => $itemInfo['selectedVariation'],
],
];
}
return [
'@attributes' => [
'ItemNumber' => $index,
'RecUID' => Uuid::v4()->toRfc4122(),
'CreationTime' => $order->date_created->format('m/d/Y H:i:s'),
'ProducerColor' => $item->getId(),
'article' => $code,
'ean_code' => $ean,
'MainPrice' => 'RetailPrice',
'RetailPrice' => $item->getPiecePrice()->getPriceWithVat()->asFloat(),
'PriceEntered' => $item->getPiecePrice()->getPriceWithVat()->asFloat(),
'main_price' => $item->getPiecePrice()->getPriceWithVat()->asFloat(),
'vat_percent' => $item->getVat(),
'vat_value' => $item->getPiecePrice()->getVatValue()->asFloat(),
'amount' => $item->getPieces(),
'Description' => $descr,
],
'Attribute' => $attributes,
];
}
private function getXMLDeliveryItem(\Order $order, array $item, string $type, int $index): array
{
return [
'@attributes' => [
'ItemNumber' => $index,
'RecUID' => Uuid::v4()->toRfc4122(),
'CreationTime' => $order->date_created->format('m/d/Y H:i:s'),
'article' => $item['code'],
'ean_code' => $item['ean'],
'MainPrice' => 'RetailPrice',
'RetailPrice' => $item['price'],
'PriceEntered' => $item['price'],
'main_price' => $item['price'],
'vat_percent' => $item['vat'],
'amount' => $item['quantity'],
'Description' => $item['descr'],
],
'Attribute' => [
[
'@attributes' => [
'Name' => 'ItemID',
'Value' => $item['id'],
],
],
],
];
}
private function getDocumentAttributes(\Order $order): array
{
$customerId = '0';
if ($order->id_user) {
$drsId = sqlQueryBuilder()
->select('id_drs')
->from('drs_users')
->where(Operator::equals(['id_user' => $order->id_user]))
->execute()->fetchOne();
if ($drsId) {
$customerId = $drsId;
}
}
// GUID objednavky - kazdy objednavce ho vygeneruju jen pri prvnim zapisu, protoze se stalo, ze DRS nevratil odpoved, ale
// objednavku zapsal, ale e-shop se ji pak pokusil zapsat znovu... a DRS vratil odpoved treba az na po treti, takze
// objednavka byla v DRSu zapsana 3x
if (!($guid = $order->getData('drsGuid'))) {
$guid = Uuid::v4()->toRfc4122();
$order->setData('drsGuid', $guid);
}
return [
'GUID' => $guid,
'address' => $customerId,
'DocumentCreator' => 'eshop',
'date' => $order->date_created->format('d.m.Y'),
'time' => $order->date_created->format('H:i:s'),
'branch_from' => $this->getOrderDefaultBranchId($order),
'branch_to' => '0',
'POSFlowStockEffect' => '-1',
'POSFlowSubType' => '106',
'type' => '27',
'book' => 'True',
'AbonentFirstName' => '',
'AbonentSurName' => '',
'AbonentNumber' => '0',
'FinancialEffect' => '-1',
'FinancialEffectCaption' => '',
'LinkedDocRecUID' => '00000000-0000-0000-0000-000000000000',
'AutoprocessNumber' => '-1',
// Natvrdo tady posilam CZK, protoze maji v DRSu nastaveny automaticky procesy, ktery s jinyma hodnotama asi nepocataji
// a zacalo to kvuli tomu delat nejaky problemy v DRSu
'CurrencyForeignISOCode' => 'CZK', // $order->getCurrency(),
'CurrencyHomeISOCode' => 'CZK', // $order->getCurrency(),
'CurrencyRate' => '1', // $order->currency_rate,
'PriceWithVAT' => 'TRUE',
'HasFixedDocumentNumber' => 'False',
'TakoverWorkflowID' => '',
'TotalPrice' => $order->getTotalPrice()->getPriceWithVat()->asFloat(),
];
}
private function getAttributes(array $attributes): array
{
$result = [];
foreach ($attributes as $key => $value) {
$result[$key] = $value;
}
return $result;
}
private function getXMLMemoItem(string $key, string $value, ?string $shortText = null): array
{
return [
'@attributes' => [
'key' => $key,
'Text' => $shortText ?: $value,
'LongText' => $value,
],
];
}
}