Files
kupshop/bundles/KupShop/DropshipBundle/Transfer/GenericTransfer.php
2025-08-02 16:30:27 +02:00

442 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Transfer;
use KupShop\DropshipBundle\Exception\TransferException;
use KupShop\DropshipBundle\TransferInterface;
use KupShop\DropshipBundle\Util\TransferWorker;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\OrderingBundle\Util\Order\OrderImporter;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Query\Operator;
use Symfony\Component\ErrorHandler\ErrorHandler;
class GenericTransfer extends AbstractTransfer implements TransferInterface
{
protected static string $type = 'generic';
protected static string $name = 'Obecný';
/** @required */
public OrderImporter $orderImporter;
/** @required */
public TransferWorker $transferWorker;
public function prepareConfigurationData(array $data): array
{
$groups = [];
foreach ($data['mappingGroups'] ?? [] as $group) {
if (!empty($group['delete'])) {
continue;
}
$deliveries = [];
foreach ($group['deliveries'] ?? [] as $delivery) {
if (!empty($delivery['delete']) || empty($delivery['id_delivery'])) {
continue;
}
$deliveries[] = $delivery;
}
$payments = [];
foreach ($group['payments'] ?? [] as $payment) {
if (!empty($payment['delete']) || empty($payment['id_payment'])) {
continue;
}
$payments[] = $payment;
}
// empty values to the end
uasort($deliveries, fn ($x) => empty($x['value']) ? 1 : -1);
// empty values to the end
uasort($payments, fn ($x) => empty($x['value']) ? 1 : -1);
$group['deliveries'] = $deliveries;
$group['payments'] = $payments;
$groups[] = $group;
}
$data['mappingGroups'] = $groups;
return $data;
}
protected function getExternalData(\SimpleXMLElement $xml): array
{
return [
(string) $xml->EXTERNAL->ID,
(array) $xml->EXTERNAL,
];
}
protected function getDeliveryTypeByConfiguration(\SimpleXMLElement $order): ?\DeliveryType
{
$delivery = (string) $order->DELIVERY;
$payment = (string) $order->PAYMENT;
$country = (string) $order->DELIVERY_ADDRESS->COUNTRY;
$mappingGroup = $this->getMappingGroup($order);
$deliveryId = null;
foreach ($mappingGroup['deliveries'] ?? [] as $deliveryConfig) {
if ((empty($deliveryConfig['value']) || $deliveryConfig['value'] == $delivery) && (empty($deliveryConfig['country']) || $deliveryConfig['country'] == $country)) {
$deliveryId = $deliveryConfig['id_delivery'];
break;
}
}
$paymentId = null;
foreach ($mappingGroup['payments'] ?? [] as $paymentConfig) {
if ((empty($paymentConfig['value']) || $paymentConfig['value'] == $payment) && (empty($paymentConfig['country']) || $paymentConfig['country'] == $country)) {
$paymentId = $paymentConfig['id_payment'];
break;
}
}
return $this->findDeliveryType((int) $deliveryId, (int) $paymentId);
}
protected function getMappingGroup(\SimpleXMLElement $order): ?array
{
$mappingGroup = null;
$default = null;
foreach ($this->getConfiguration()['mappingGroups'] ?? [] as $group) {
// if filter is set, then do check
if ($filterTag = $order->xpath($group['filter']['tag'] ?? '')[0] ?? null) {
$filterTagValue = (string) $filterTag;
$filterValues = array_map('trim', explode(',', $group['filter']['value'] ?? ''));
if (in_array($filterTagValue, $filterValues)) {
$mappingGroup = $group;
}
}
// if the group has no filter set, it is the default group
if (empty($group['filter']['tag'])) {
$default = $group;
}
}
return $mappingGroup ?: $default;
}
public function in(array $config): void
{
$orders = $this->transformXML();
foreach ($orders->ORDER ?? [] as $xml) {
[$externalId, $externalData] = $this->getExternalData($xml);
if (empty($externalId)) {
$this->addActivityLog(
'Nepodařilo se naimportovat objednávku do e-shopu, protože nemá externí ID. V XML souboru chybí "EXTERNAL/ID" element!',
);
continue;
}
if (!$this->isDropshipOrderValidToImport($xml)) {
continue;
}
// mapping group not found and ignore orders without mapping is enabled, so skip order
if (($this->getConfiguration()['ignore_on_mapping_not_found'] ?? 'N') === 'Y' && !$this->getMappingGroup($xml)) {
continue;
}
// pokud objednavka uz existuje, tak provedeme pouze aktualizaci
if ($order = $this->getOrderByExternalId($externalId)) {
$this->updateDropshipOrder($order, $xml);
continue;
}
try {
$order = sqlGetConnection()->transactional(function () use ($externalId, $externalData, $xml) {
$currencyContext = Contexts::get(CurrencyContext::class);
// nactu zakladni data o objednavce pomoci orderImporter servisy
$data = $this->orderImporter->getOrderBaseData($xml);
// nastavim jazyk objednavky
if (findModule(\Modules::TRANSLATIONS)) {
$groupSettings = $this->getMappingGroup($xml)['settings'] ?? [];
$data['id_language'] = !empty($groupSettings['id_language']) ? $groupSettings['id_language'] : Contexts::get(LanguageContext::class)->getDefaultId();
}
// pokud neni vyplnena currency, tak nastavim vychozi currency
if (empty($data['currency'])) {
$data['currency'] = $currencyContext->getDefaultId();
}
// nactu si informace o mene, pokud mena neexistuje a mam vypnutou prices_to_default_currency, tak vyhazuju chybu
$currencyInfo = $this->getCurrencyInfo($data['currency']);
// pokud nemam currency rate, tak ho doplnim
if (empty($data['currency_rate'])) {
$data['currency_rate'] = $currencyInfo->rate;
}
$noteAdmin = json_decode($data['note_admin'] ?? '', true) ?: [];
// najdu a vlozim dopravu k objednavce
if ($deliveryType = $this->getDeliveryTypeByConfiguration($xml)) {
$data['id_delivery'] = $deliveryType->id;
// delivery point - napr.v pripade, ze se jedna o zasilkovnu
$deliveryPoint = (string) $xml->DELIVERY_POINT;
if (!empty($deliveryPoint) && method_exists($deliveryType->getDelivery(), 'getInfo')) {
$noteAdmin['delivery_data'] = $deliveryType->getDelivery()
->setPointId($deliveryPoint)
->getInfo();
}
}
// ceny se budou konvertovat do vychozi meny, takze si pro to pripravim data
if ($this->isPriceConvertionEnabled() && $currencyInfo->getCurrencyCode() !== $currencyContext->getDefaultId()) {
$data['currency'] = $currencyContext->getDefaultId();
}
$data['note_admin'] = json_encode($noteAdmin);
$order = $this->createDropshipOrder($xml, $data);
// obecny feed muze obsahovat i marketplace info, takze v tu chvili chci k objednavce zalogovat o jaky marketplace se jedna
$this->logOrderMarketplaceInfo($order, $xml, $externalData);
$lastItemTax = \DecimalConstants::zero();
foreach ($xml->ITEMS->ITEM as $item) {
// zkontroluju, ze jsou vyplneny vsechny povinne udaje pro polozku objednavky
if (!$this->checkRequiredXMLData($item, $this->getRequiredOrderItemFields())) {
throw new TransferException(
sprintf('Nepodařilo se naimportovat objednávku "%s": data položek objednávky nejsou validní', $externalId),
(array) $item
);
}
// zkusim najit produkt a variantu
[$productId, $variationId] = $this->getProductByItem($item);
$isPriceWithVat = ((string) ($item->PIECE_PRICE->attributes()['with_vat'] ?? null)) === 'true';
$pieces = toDecimal((string) $item->PIECES);
$piecePrice = $this->convertPrice(toDecimal((string) $item->PIECE_PRICE), $currencyInfo);
// pokud je cena uvedena s DPH, tak DPH odectu
if ($isPriceWithVat) {
$piecePrice = $piecePrice->removeVat((string) $item->VAT);
}
$totalPrice = toDecimal($piecePrice)->mul($pieces);
// odecist skladovost produktu
if ($productId) {
$product = \Variation::createProductOrVariation($productId, $variationId);
$product->createFromDB();
$product->sell($variationId, $pieces->asFloat());
}
$itemData = $this->modifyItem(
[
'id_order' => $order->id,
'id_product' => $productId,
'id_variation' => $variationId,
'pieces' => $pieces,
'pieces_reserved' => $pieces,
'piece_price' => $piecePrice,
'total_price' => $totalPrice,
'tax' => (string) $item->VAT,
'descr' => (string) $item->NAME,
'note' => json_encode(['item_type' => OrderItemInfo::TYPE_PRODUCT]),
],
$item
);
$lastItemTax = toDecimal($itemData['tax']);
// vytvorim polozku objednavky
sqlQueryBuilder()
->insert('order_items')
->directValues($itemData)
->execute();
$itemData['id'] = sqlInsertId();
$this->itemCreatedEvent(
product: $product ?? null,
idVariation: (int) $variationId,
piecePrice: $piecePrice,
pieces: (int) $pieces->asInteger(),
data: [
'row' => $itemData,
'items_table' => 'order_items',
],
order: $order
);
unset($product);
}
$deliveryPrice = (string) $xml->DELIVERY_PRICE;
$paymentPrice = (string) $xml->PAYMENT_PRICE;
$deliveryPrice = !empty($deliveryPrice) ? toDecimal($deliveryPrice) : \DecimalConstants::zero();
$paymentPrice = !empty($paymentPrice) ? toDecimal($paymentPrice) : \DecimalConstants::zero();
$deliveryPaymentPrice = $this->convertPrice($deliveryPrice->add($paymentPrice), $currencyInfo);
// pridani polozky s dopravou a platbou do objednavky
if ($deliveryItem = $this->getDeliveryPaymentItem($order, $deliveryType, $deliveryPaymentPrice, $lastItemTax)) {
sqlQueryBuilder()
->insert('order_items')
->directValues($deliveryItem)
->execute();
}
// prepocitam total price objednavky
$order->recalculate(round: false);
// oznacit objednavku jako zaplacenou, pokud prisel status_payed == 1
if (!$order->isPaid() && $data['status_payed'] == 1) {
$this->payDropshipOrder($order);
}
return $order;
});
$this->modifyInsertedOrder($order, $xml);
} catch (\Throwable $e) {
$this->transferWorker->logException($e, $this);
}
}
}
public function out(array $config): void
{
throw new \RuntimeException('Method "out" is not implemented for generic transfer');
}
protected function updateDropshipOrder(\Order $order, \SimpleXMLElement $xml): void
{
$data = $this->orderImporter->getOrderBaseData($xml, false);
$updateData = [];
if (!empty($data['invoice_dic'])) {
$updateData['invoice_dic'] = $data['invoice_dic'];
}
if (!empty($data['invoice_ico'])) {
$updateData['invoice_ico'] = $data['invoice_ico'];
}
if (!empty($data['delivery_country'])) {
$updateData['delivery_country'] = trim($data['delivery_country']);
}
if (!empty($data['invoice_country'])) {
$updateData['invoice_country'] = trim($data['invoice_country']);
}
// aktualizovat stav zaplaceni
if (!$order->isPaid() && $data['status_payed'] == 1) {
$updateData['status_payed'] = 1;
$this->payDropshipOrder($order);
}
if (!empty($updateData)) {
sqlQueryBuilder()
->update('orders')
->directValues($updateData)
->where(Operator::equals(['id' => $order->id]))
->execute();
}
}
protected function getDeliveryPaymentItem(\Order $order, ?\DeliveryType $deliveryType, \Decimal $price, \Decimal $vat): ?array
{
$deliveryItemName = 'Doprava a platba';
if ($deliveryType) {
$deliveryItemName = $deliveryType->name;
}
if (!$price->isPositive()) {
return null;
}
$price = $price->removeVat($vat);
return [
'id_order' => $order->id,
'id_product' => null,
'id_variation' => null,
'pieces' => 1,
'pieces_reserved' => 1,
'piece_price' => $price,
'total_price' => $price,
'descr' => $deliveryItemName,
'tax' => $vat,
'note' => json_encode(['item_type' => OrderItemInfo::TYPE_DELIVERY]),
];
}
protected function transformXML(): ?\SimpleXMLElement
{
try {
if ($xml = $this->loadXML()) {
if (!empty($this->dropshipment['transformation'])) {
$xsl = new \DOMDocument();
$xsl->loadXML($this->dropshipment['transformation']);
return ErrorHandler::call(fn () => simplexml_import_dom(\AutomaticImportTransform::TransformXml($xsl, $xml)));
}
return $xml;
}
} catch (\Throwable $e) {
if (isLocalDevelopment()) {
throw $e;
}
$this->addActivityLog(
'Nepodařilo se provést transformaci feedu!',
['error' => $e->getMessage()]
);
}
return null;
}
protected function logOrderMarketplaceInfo(\Order $order, \SimpleXMLElement $xml, array $externalData): void
{
// nazev marketplacu, ze ktereho objednavka pochazi
if (!empty($externalData['marketplace'])) {
$order->logHistory('[Dropshipment] Marketplace: '.$externalData['marketplace']);
}
// marketplace muze mit i nejaky vlastni ID - napr. v pripade baselinkeru do EXTERNAL/ID chodi ID baselinkeru
// protoze to je to spravne ID pro pripadnou komunikaci s baselinkerem, ale nekdo muze chtit pracovat
// i primo s ID z daneho marketplacu, takze tady je podpora, aby se pripadne zobrazilo aspon v historii
if (!empty($externalData['marketplace_id'])) {
$order->logHistory('[Dropshipment] Marketplace ID: '.$externalData['marketplace_id']);
}
}
protected function getRequiredOrderItemFields(): array
{
return [
'NAME', 'PIECES', 'PIECE_PRICE', 'VAT',
];
}
private function checkRequiredXMLData(\SimpleXMLElement $xml, array $requiredFields): ?bool
{
foreach ($requiredFields as $field) {
if (!isset($xml->{$field})) {
return false;
}
}
return true;
}
}