Files
kupshop/bundles/External/VarioBundle/Synchronizers/OrderSynchronizer.php
2025-08-02 16:30:27 +02:00

878 lines
28 KiB
PHP

<?php
namespace External\VarioBundle\Synchronizers;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use External\VarioBundle\Exception\SynchronizerException;
use External\VarioBundle\SoapClient;
use External\VarioBundle\Util\VarioConfig;
use External\VarioBundle\Util\VarioHelper;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Psr\Log\LoggerInterface;
use Query\Operator;
use Query\Order;
class OrderSynchronizer extends BaseSynchronizer
{
protected static $type = 'otDocument';
protected static $batch = 50;
protected static $maxBatches = 10;
protected static bool $createUnregUserFromOrder = false;
protected $fields = [
'Number' => 'document',
'DocumentItems' => 'items',
];
protected $sentryLogger;
protected $orderItemInfo;
protected $contextManager;
public function __construct(
SoapClient $client,
VarioHelper $helper,
VarioConfig $config,
LoggerInterface $logger,
SentryLogger $sentryLogger,
OrderItemInfo $orderItemInfo,
ContextManager $contextManager,
) {
parent::__construct($client, $helper, $config, $logger);
$this->sentryLogger = $sentryLogger;
$this->orderItemInfo = $orderItemInfo;
$this->contextManager = $contextManager;
}
public function getEshopID($objectID)
{
$id = $this->helper->getMapping('vario_orders', $objectID);
if (!$id) {
return null;
}
return $id;
}
public function sync()
{
$this->syncOrdersFromVario();
$this->syncOrdersToVario();
}
private function syncOrdersFromVario(): void
{
$loop = 0;
do {
// fetch data
if ($forceSync = getVal('force_sync')) {
$items = $this->client->forceObjectData(static::getType(), $forceSync);
$this->forceEnd = true;
} else {
$items = $this->client->getJobsData(static::getType(), static::$batch);
}
$doneJobs = [];
foreach ($items as $item) {
// Log change
$this->logItem($item);
$eshopID = $this->getEshopID($this->getObjectID($item));
// faktura, na ktery je packageID
if ($eshopID === null && $item->Data->DocumentType == 'FV' && !empty($item->Data->Data2)) {
$this->syncFactorage($item);
}
// eshopID is null
if ($eshopID === null) {
$doneJobs[] = $item->Job->ID;
continue;
}
// delete action
if ($item->Job->Action == 'acDelete') {
$this->syncDelete($eshopID);
$doneJobs[] = $item->Job->ID;
continue;
}
foreach ($this->fields as $originalField => $syncField) {
if (!property_exists($item->Data, $originalField)) {
continue;
}
$value = $item->Data->$originalField;
$column = $originalField;
$method = 'sync'.ucfirst($syncField);
if (!method_exists($this, $method)) {
throw new SynchronizerException(
sprintf('Method %s() for synchronizer type \'%s\' is not defined!', $method, static::getType())
);
}
call_user_func_array([$this, $method], [$value, $eshopID, $column, $item]);
}
$doneJobs[] = $item->Job->ID;
}
$doneJobs = array_filter($doneJobs);
if (!empty($doneJobs)) {
$this->client->setJobsDone($doneJobs);
}
if (static::$maxBatches !== null && static::$maxBatches <= $loop) {
$this->forceEnd = true;
}
$loop++;
} while (count($items) > 0 && $this->forceEnd == false);
}
public function syncDocument($value, $orderId, $column, $item): void
{
$order = \Order::get((int) $orderId);
$this->syncDocumentStatuses($order, $item->Data);
}
public function syncFactorage($item): void
{
$orderNumber = $this->getOriginalNumber($item->Data->OrderNumber);
// try to find order ID by order_no
$orderID = $this->selectSQL('orders', ['order_no' => $orderNumber], ['id'])->fetchColumn();
if (!$orderID) {
return;
}
$order = new \Order();
$order->createFromDB($orderID);
$order->setData('varioNumber', $item->Data->Number);
$this->syncPackageId($item->Data->Data2, $orderID);
}
protected function syncDocumentStatuses(\Order $order, object $item): void
{
}
protected function changeOrderStatus(\Order $order, int $status, ?string $emailType = null, ?string $varioStatus = null): void
{
if ($order->status != $status) {
$order->changeStatus($status, null, $emailType !== null ? true : null, null, $emailType);
$order->logHistory('[Vario] Změna stavu'.($varioStatus ? ': '.$varioStatus : ''));
}
}
public function syncItems($values, $orderID, $column, $item): void
{
if (!$orderID) {
return;
}
$order = new \Order();
$order->createFromDB($orderID);
$cancelledItems = $this->helper->getOrderAdditionalInfo($orderID, 'cancelled_items');
if (!$order->isActive()) {
return;
}
foreach ($values as $item) {
// delete cancelled items from order
if ($this->contains($item->State, 'STORNO')) {
// item is already cancelled, so skip it
if (isset($cancelledItems[$this->helper->trimVarioId($item->ID)]) && $cancelledItems[$this->helper->trimVarioId($item->ID)] == true) {
continue;
}
$itemID = null;
if (!empty($item->ProductID)) {
$productID = $this->helper->getMapping('vario_products', $this->helper->trimVarioId($item->ProductID));
$variantID = null;
if ($productID && !empty($item->VariantID)) {
$variantID = $this->helper->getMapping('vario_variations', $this->helper->trimVarioId($item->VariantID));
if (!$variantID) {
$variantID = null;
}
}
if (!$productID) {
$this->captureOrderItemSyncFailure($orderID, $item);
continue;
}
$itemID = $this->selectSQL('order_items', ['id_order' => $orderID, 'id_product' => $productID, 'id_variation' => $variantID], ['id'])->fetchColumn();
} else {
// try to find item by name
$itemID = $this->selectSQL('order_items', ['id_order' => $orderID, 'descr' => $item->Description], ['id'])->fetchColumn();
}
if (!$itemID && !($itemID = $this->findOrderItemOnShop($orderID, $item))) {
$this->captureOrderItemSyncFailure($orderID, $item);
continue;
}
$orderItem = $this->selectSQL('order_items', ['id' => $itemID])->fetch();
// Sanity check
if ($orderItem['id_order'] != $orderID) {
$this->captureOrderItemSyncFailure($orderID, $item);
continue;
}
$price = toDecimal($orderItem['total_price']);
$vat = toDecimal($orderItem['tax']);
$itemPrice = $price->addVat($vat);
$order->deleteItem($itemID);
$cancelledItems[$this->helper->trimVarioId($item->ID)] = true;
$this->helper->saveOrderAdditionalInfo($orderID, 'cancelled_items', $cancelledItems);
$history = $order->getHistory(true);
$deleteInfo = end($history);
if ($deleteInfo) {
$comment = $deleteInfo['comment'].' s cenou '.printPrice($itemPrice);
$this->updateSQL('orders_history', ['comment' => $comment], ['id' => $deleteInfo['id']]);
}
}
}
}
protected function findOrderItemOnShop($orderID, $item): ?int
{
return null;
}
public function captureOrderItemSyncFailure($orderID, $item): void
{
$this->sentryLogger->captureException(
new SynchronizerException('Failure in order items synchronization! Order ID: '.$orderID),
[
'id_order' => $orderID,
'item' => $item,
]
);
}
public function syncPackageId($value, $orderID): void
{
if (!$orderID) {
return;
}
if (empty($value)) {
$value = null;
}
$packageId = null;
if ($value) {
$data = explode(';', $value);
$packageId = $data[1] ?? null;
}
$this->updateSQL('orders', ['package_id' => $packageId], ['id' => $orderID]);
}
private function contains(string $string, string $word): bool
{
if (strpos(strtolower($string), strtolower($word)) !== false) {
return true;
}
return false;
}
protected function syncOrdersToVario(): void
{
if (isDevelopment()) {
return;
}
$qb = sqlQueryBuilder()->select('o.id')
->from('orders', 'o')
->leftJoin('o', 'vario_orders', 'vo', 'vo.id_order = o.id')
->andWhere(Operator::equals(['o.status_storno' => 0]))
->andWhere('vo.id_vario IS NULL')
->andWhere(Operator::not(Operator::equals(['source' => 'import'])))
->groupBy('o.id');
foreach ($qb->execute() as $order) {
$orderObject = new \Order();
$orderObject->createFromDB($order['id']);
$this->contextManager->activateOrder($orderObject, function () use ($orderObject) {
$orderObject->fetchItems();
$this->syncOrder($orderObject);
});
}
}
public function syncOrder(\Order $order): bool
{
if (isDevelopment()) {
return true;
}
try {
$documentObject = $this->createDocumentObject($order);
$this->logOrderUpload($order, $documentObject);
$varioOrderId = $this->helper->trimVarioId($this->client->createOrUpdateOrder($documentObject));
$this->helper->createMapping('vario_orders', $varioOrderId, $order->id);
$this->resolveOrder($order, $varioOrderId);
} catch (\Exception|\Error|\Throwable $e) {
// try to create missing mapping
if ($e->getMessage() === 'There is already another Document with this Number') {
try {
$this->importMissingOrdersMapping((int) $order->id);
} catch (\Throwable $e) {
$this->helper->logException($e, sprintf('[Vario] Chyba při zápisu objednávky: %s', $order->order_no), [
'orderId' => $order->id,
'exception' => $e->getMessage(),
]);
}
} else {
$this->helper->logException($e, sprintf('[Vario] Chyba při zápisu objednávky: %s', $order->order_no), [
'orderId' => $order->id,
'exception' => $e->getMessage(),
]);
}
return false;
}
return true;
}
public function logOrderUpload(\Order $order, \stdClass $documentObject, bool $update = false): void
{
if (isDevelopment()) {
return;
}
$status = 'Uploading';
if ($update) {
$status = 'Updating';
}
$this->logger->notice(
sprintf('VARIO: %s order to Vario, OrderID: %s', $status, $order->id),
[
'id_order' => $order->id,
'Type' => static::getType(),
'Order' => (array) $documentObject,
]
);
}
public function createDocumentObject(\Order $order): \stdClass
{
$document = new \stdClass();
$customerID = '';
if (!empty($order->id_user)) {
$customerID = sqlQueryBuilder()
->select('id_vario')
->from('vario_users')
->where(
Operator::equals(
[
'id_user' => $order->id_user,
]
)
)->execute()->fetchColumn();
if (empty($customerID)) {
$customerID = '';
}
if (!empty($customerID)) {
// check if user exists
if (!$this->checkVarioUserExists($customerID)) {
$customerID = '';
}
}
}
if (empty($customerID) && $this->createUnregistredUser()) {
$customerID = $this->helper->createUnregistredUserFromOrder($order);
}
$document->ID = '';
$document->Book = $this->getOrderBook($order);
$document->OrderNumber = $this->getOrderNumber($order);
$document->Number = $this->getNumber($order);
$document->DocumentName = $this->getDocumentName($order);
$document->DocumentType = 'ZZ';
$document->Currency = $order->getCurrency();
$document->DontMakeInvoice = false;
$document->VarNumber = $order->order_no;
$document->Comment = $this->getDocumentComment($order);
$document->Status = 'Nová';
$document->Text = $this->getDocumentText($order);
$document->Date = $this->formatDateTime($order->date_created, 'Y-m-d\T00:00:00');
$document->TaxDate = null;
$document->SettlementDate = $this->formatDateTime($order->date_created, 'Y-m-d\T00:00:00');
$document->SettlementMethod = $this->getPaymentName($order);
$document->Delivery = $this->getDeliveryName($order);
$document->PlaceOfSupply = $this->getPlaceOfSupply($order);
$document->IO = 1; // smer toku penez
$document->Rounding = 0;
$document->Payed = 0;
$document->SettlementLeft = 0;
$document->RequestedAdvance = $this->getRequestAdvanced($order);
$document->AdvancePayed = $this->getAdvancePayed($order);
$document->TotalWithoutVAT = $order->total_price_array['value_without_vat']->asFloat();
$document->TotalWithVAT = $order->total_price_array['value_with_vat']->asFloat();
$document->Total = $order->total_price_array['value_with_vat']->asFloat();
$document->SumRoundingPlace = $this->getOrderSummaryRounding($order);
$document->VATRoundingPlace = 0.01;
$document->Interest = 0;
$document->CompanyID = $customerID;
$document->DeliveryCompanyID = '';
if (!empty($order->invoice_firm)) {
$document->CompanyName = $order->invoice_firm;
$document->PersonName = $order->invoice_name.' '.$order->invoice_surname;
} else {
$document->CompanyName = $order->invoice_name.' '.$order->invoice_surname;
$document->PersonName = $order->invoice_name.' '.$order->invoice_surname;
}
$document->Addresses = [];
foreach (['atPrimary', 'atDelivery'] as $address) {
$document->Addresses[] = $this->createAddressObject($order, $address);
}
$document->IC = $order->invoice_ico;
$document->DIC = $order->invoice_dic;
$document->Telephone = $order->invoice_phone;
$document->Email = $order->invoice_email;
$document->BankName = '';
$document->BankBranch = '';
$document->AccountNumber = '';
$document->BankCode = '';
$document->SpecificSymbol = '';
$document->IBAN = '';
$document->SalesAgent = $this->getSalesAgent($order);
$document->Category = '';
$document->DueDateDays = $this->getDueDateDays($order);
$document->PriceGroup = '';
$document->PricelistID = $this->getPriceListID($order);
$document->PricelistName = $this->getPriceListName($order);
$document->Discount = $this->getDiscount($order);
$document->OneDelivery = false;
$document->Data1 = $order->id_user ? (int) $order->id_user : '';
$document->Data2 = '';
$document->Note = $this->getDocumentNote($order);
$document->UserFields = '';
$document->DocumentItems = $this->getDocumentItems($order);
return $document;
}
protected function getDocumentItems(\Order $order)
{
$itemNumber = 1;
$documentItems = [];
foreach ($order->fetchItems() as $item) {
$documentItems[] = $this->createItemObject($item, $order, $itemNumber++);
}
return $documentItems;
}
protected function getPriceListID(\Order $order): string
{
return '';
}
protected function getRequestAdvanced(\Order $order): float
{
$payment = $order->getDeliveryType()->getPayment();
if ($payment && $payment->hasOnlinePayment()) {
return $order->total_price_array['value_with_vat']->asFloat();
}
return 0;
}
protected function getAdvancePayed(\Order $order): float
{
$payment = $order->getDeliveryType()->getPayment();
if ($payment && $payment->hasOnlinePayment()) {
return $order->total_price_array['value_with_vat']->asFloat();
}
return 0;
}
protected function getPriceListName(\Order $order): string
{
return '';
}
protected function getDiscount(\Order $order): string
{
return '0';
}
protected function getDueDateDays(\Order $order): int
{
return 0;
}
protected function getSalesAgent(\Order $order): string
{
return '';
}
protected function getPlaceOfSupply(\Order $order): string
{
return '';
}
protected function getDeliveryName(\Order $order): string
{
$delivery = $order->getDeliveryType()->getDelivery();
if (!empty($delivery->getCustomData()['vario_name'])) {
return $delivery->getCustomData()['vario_name'];
}
return $delivery->name;
}
protected function getPaymentName(\Order $order): string
{
if ($payment = $order->getDeliveryType()->getPayment()) {
if ($payment instanceof \Dobirka) {
return 'Dobírka';
} elseif ($payment instanceof \Prevodem) {
return 'Bankovním převodem';
} elseif ($payment instanceof \GoPay) {
return 'Platba kartou GP';
}
return $payment->getName();
}
return $order->getDeliveryType()->payment;
}
protected function getNumber(\Order $order): string
{
return $this->getOrderNumber($order);
}
protected function getOrderNumber(\Order $order): string
{
return $order->order_no;
}
private function getOriginalNumber(string $number): string
{
$number = explode('-', $number);
return end($number);
}
protected function getOrderBook(\Order $order): string
{
if ($deliveryType = $order->getDeliveryType()) {
if ($delivery = $deliveryType->getDelivery()) {
$bookName = $delivery->getCustomData()['vario_book_name'] ?? null;
if (!empty($bookName)) {
return $bookName;
}
}
}
return 'Zakázky';
}
private function checkVarioUserExists($varioID)
{
if ($this->client->getClient()->GetCustomer($varioID)) {
return true;
}
return false;
}
protected function getDocumentName(\Order $order): string
{
return 'Zakázka';
}
public function createItemObject($item, \Order $order, $documentOrderNumber): \stdClass
{
$itemObject = new \stdClass();
if (!empty($item['id_product'])) {
$product = $this->selectSQL('products', ['id' => $item['id_product']])->fetch();
if (!($productId = $this->helper->getVarioID('vario_products', $item['id_product']))) {
$exception = true;
// if coupon
if ($product) {
if (!empty($product['data'])) {
$data = json_decode($product['data'], true);
if (($data['generate_coupon'] ?? false) == 'Y') {
$productId = null;
$item['id_product'] = null;
$item['id_variation'] = null;
$itemName = $item['descr'];
$exception = false;
}
}
}
if ($exception) {
throw new SynchronizerException(
sprintf('Mapping for product ID %s was not found!', $item['id_product'])
);
}
}
$variationId = '';
if ($item['id_variation'] !== null) {
if (!($variationId = $this->helper->getVarioID('vario_variations', $item['id_variation']))) {
throw new SynchronizerException(
sprintf('Mapping for variation ID %s was not found!', $item['id_variation'])
);
}
}
$itemNumber = '';
$itemName = $itemName ?? null;
} else {
$productId = $this->getNonProductItemID($order, $item);
$variationId = '';
$itemNumber = '';
$itemType = $this->orderItemInfo->getItemType($item);
$itemName = $item['descr'];
if ($itemType) {
$itemNumber = $itemType;
}
}
$itemObject->ID = '';
$itemObject->ProductID = $this->prepareVarioID($productId);
$itemObject->VariantID = $this->prepareVarioID($variationId);
$itemObject->DocumentID = '';
$itemObject->DocumentOrderNumber = $documentOrderNumber;
$itemObject->Description = $itemName;
$itemObject->ItemNumber = $itemNumber;
$itemObject->Quantity = $item['pieces'];
$itemObject->QuantityUnit = 'Ks';
$itemObject->GPL = $item['piece_price']['value_without_vat']->asFloat();
$itemObject->PricePerUnit = $item['piece_price']['value_without_vat']->asFloat();
$itemObject->PriceWithoutVAT = $item['total_price']['value_without_vat']->asFloat();
$itemObject->TotalVAT = $item['total_price']['value_vat']->asFloat();
$itemObject->TotalPrice = $item['total_price']['value_with_vat']->asFloat();
$itemObject->VATRate = (int) $item['vat'];
$itemObject->DiscountRate = $item['discount']->asFloat();
$itemObject->VATType = $this->getVatType($order, $item);
$itemObject->StoreID = $this->getStoreID($order, $item);
$itemObject->State = 'Rezervováno';
$itemObject->OrderID = $order->order_no;
$itemObject->DeliveryDate = null;
$itemObject->QuantityGroups = [];
$itemObject->DeliveryNoteID = '';
$itemObject->DeliveryNoteItemID = '';
$itemObject->CommissionID = '';
$itemObject->CommissionItemID = '';
$itemObject->Note = '';
$itemObject->Data1 = '';
$itemObject->Data2 = '';
$itemObject->Number1 = 0;
$itemObject->Number2 = 0;
$itemObject->AccountCode = '';
$itemObject->ExternID = '';
$itemObject->UserFields = '';
return $itemObject;
}
public function createAddressObject(\Order $order, $type = 'atPrimary'): \stdClass
{
$address = new \stdClass();
if ($type == 'atPrimary') {
$prefix = 'invoice';
$addressName = 'Fakturační adresa';
} elseif ($type == 'atDelivery') {
$prefix = 'delivery';
$addressName = 'Doručovací adresa';
} else {
throw new SynchronizerException('Unknown address type!');
}
$userName = $order->{$prefix.'_name'}.' '.$order->{$prefix.'_surname'};
if (!empty($order->{$prefix.'_firm'})) {
$userName = $order->{$prefix.'_firm'}."\n".$userName;
}
$street = $userName."\n".$order->{$prefix.'_street'};
$city = $order->{$prefix.'_city'};
$zip = $order->{$prefix.'_zip'};
$address->ID = '';
$address->AddressType = $type;
$address->AddressName = $addressName;
$address->Street = $street;
$address->City = $city;
$address->ZIP = $zip;
$address->Country = $order->{$prefix.'_country'};
$address->CountryISO = $order->{$prefix.'_country'};
$address->Address = '';
$address->UseOnDocuments = true;
if ($type == 'atDelivery') {
$address->UseOnDocuments = false;
}
return $address;
}
public function importMissingOrdersMapping(?int $orderId = null): int
{
$qb = sqlQueryBuilder()->select('o.id, o.order_no, o.date_created')
->from('orders', 'o')
->leftJoin('o', 'vario_orders', 'vo', 'vo.id_order = o.id')
->andWhere('vo.id_vario IS NULL AND o.order_no NOT LIKE \'OLD%\'');
if ($orderId) {
$qb->andWhere(Operator::equals(['o.id' => $orderId]));
}
$fixed = 0;
foreach ($qb->execute() as $item) {
$datetime = new \DateTime($item['date_created']);
$date = $datetime->format('Y-m-d');
$varioOrders = $this->client->getClient()->GetDocumentsByCriteria1(null, 'ZZ', $datetime->format('Y-m-d\TH:00:00'), $date.'\T23:59:59');
foreach ($varioOrders as $varioOrder) {
try {
$order = new \Order();
$order->createFromDB($item['id']);
$orderNumber = $this->getOriginalNumber($varioOrder->OrderNumber);
if ($orderNumber === $item['order_no']) {
try {
$this->helper->createMapping('vario_orders', $this->helper->trimVarioId($varioOrder->ID), $item['id']);
$order->logHistory('Objednávka odeslána do Varia');
$fixed++;
} catch (UniqueConstraintViolationException $e) {
continue;
}
}
} catch (\Throwable $e) {
$this->sentryLogger->captureException($e);
}
}
}
return $fixed;
}
private function getOrderSummaryRounding(\Order $order): float
{
switch ($order->getCurrency()) {
case 'CZK':
return 1.00;
case 'EUR':
return 0.01;
}
return 0.01;
}
protected function getNonProductItemID(\Order $order, $item): string
{
return '';
}
protected function getStoreID(\Order $order, $item): string
{
return '';
}
protected function getVatType(\Order $order, $item): string
{
return '';
}
protected function getDocumentComment(\Order $order): string
{
return '';
}
protected function getDocumentText(\Order $order): string
{
$comment = $order->note_user;
if (!empty($comment)) {
return html_entity_decode($comment);
}
return '';
}
public function getDocumentNote(\Order $order): string
{
return '';
}
protected function formatDateTime(\DateTimeInterface $datetime, $format = 'Y-m-d\TH:i:s'): string
{
return $datetime->format($format);
}
protected function resolveOrder(\Order $order, string $varioOrderId): void
{
$order->logHistory(sprintf('[Vario] Objednávka odeslána do Varia; VarioID: %s', $varioOrderId));
}
protected function createUnregistredUser(): bool
{
return self::$createUnregUserFromOrder;
}
}