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

434 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Transfer;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\DropshipBundle\Entity\CurrencyInfo;
use KupShop\DropshipBundle\Event\DropshipOrderCreatedEvent;
use KupShop\DropshipBundle\Exception\TransferException;
use KupShop\DropshipBundle\TransferInterface;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\OrderingBundle\Event\OrderEvent;
use KupShop\OrderingBundle\Event\OrderItemEvent;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use Query\Operator;
use Query\QueryBuilder;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
abstract class AbstractTransfer implements TransferInterface
{
protected static string $type;
protected static string $name;
protected bool|array|null $tempRestrictions = null;
public array $dropshipment = [];
public array $configuration = [];
/** @required */
public OrderInfo $orderInfo;
/** @required */
public EventDispatcherInterface $eventDispatcher;
/** @required */
public ContextManager $contextManager;
public static function getType(): string
{
return static::$type;
}
public static function getName(): string
{
return static::$name;
}
public function isRunnable(): bool
{
if (!$this->dropshipment) {
return false;
}
return $this->dropshipment['active'] == 1;
}
/**
* Setup transfer before run.
*/
public function setup(array $dropshipment): void
{
$this->tempRestrictions = null;
$this->dropshipment = $dropshipment;
$this->configuration = $dropshipment['configuration'];
}
public function isValidRestrictionByTag(\SimpleXMLElement $order): bool
{
if ($this->tempRestrictions === null) {
$drop = $this->dropshipment;
if (
($restrictions = $drop['data']['restrictions'] ?? false)
&& !empty($restrictions['values'])
&& !empty($restrictions['tagName'])
) {
$values = array_map(fn ($v) => strtolower(trim($v)), explode(',', $restrictions['values']));
$this->tempRestrictions = [$restrictions['tagName'], $values];
} else {
$this->tempRestrictions = false;
}
}
if (is_array($this->tempRestrictions)) {
[$tagName, $values] = $this->tempRestrictions;
$tag = (string) $order->$tagName ?? '';
return !in_array(strtolower($tag), $values, true);
}
return true;
}
public function process(): void
{
$config = $this->getConfiguration();
// zpracovani objednavek z feedu do e-shopu
$this->in($config);
// zpracovani objednavek z e-shopu k externi sluzbe
if ('Y' == ($config['out'] ?? 'N')) {
$this->out($config);
}
}
public function getConfiguration(): array
{
return $this->configuration;
}
public function getConfigurationVariables(): array
{
return [];
}
protected function payDropshipOrder(\Order $order): void
{
$pay_method = \Payment::METHOD_UNKNOWN;
$delivery_type = $order->getDeliveryType();
if ($delivery_type && !empty($delivery_type->payment_class)) {
$pay_method = $delivery_type->payment_class->getPayMethod();
}
$order->insertPayment(
$order->getTotalPrice()->getPriceWithVat(),
'Zaplaceno přes modul dropshipment',
null, false, $pay_method
);
}
protected function findDeliveryType(?int $deliveryId, ?int $paymentId): ?\DeliveryType
{
$deliveryTypeId = sqlQueryBuilder()
->select('id')
->from('delivery_type')
->where(Operator::equals(['id_delivery' => $deliveryId, 'id_payment' => $paymentId]))
->execute()->fetchOne();
if (!$deliveryTypeId) {
return null;
}
return \DeliveryType::get($deliveryTypeId, true);
}
protected function loadXML(): ?\SimpleXMLElement
{
if (!($this->dropshipment['source_url'] ?? null)) {
$this->addActivityLog(
'V nastavení dropshipmentu chybí URL pro zdrojový XML soubor',
$this->configuration
);
return null;
}
try {
$xml = ErrorHandler::call(fn () => simplexml_load_file($this->dropshipment['source_url']));
} catch (\Throwable $e) {
if (isLocalDevelopment()) {
throw $e;
}
$this->addActivityLog(
'Nelze stáhnout objednávkový XML feed.',
['error' => $e->getMessage()]
);
return null;
}
return $xml ?: null;
}
/**
* General method for creating an order. Order without items.
*/
protected function createDropshipOrder(\SimpleXMLElement $xml, array $data): \Order
{
[$externalId, $externalData] = $this->getExternalData($xml);
if ($data['delivery_country'] != '') {
$data['delivery_country'] = trim($data['delivery_country']);
}
if ($data['invoice_country'] != '') {
$data['invoice_country'] = trim($data['invoice_country']);
}
/** @var \Order $order */
$order = sqlGetConnection()->transactional(function () use ($data, $externalId, $externalData) {
sqlQueryBuilder()
->insert('orders')
->directValues(array_merge(
[
'source' => OrderInfo::ORDER_SOURCE_DROPSHIP,
'date_updated' => (new \DateTime())->format('Y-m-d H:i:s'),
],
$data
))
->execute();
$orderId = (int) sqlInsertId();
// vytvorim mapovani na dropshipment
sqlQueryBuilder()
->insert('order_dropshipment')
->directValues(
[
'id_order' => $orderId,
'id_dropshipment' => $this->dropshipment['id'],
'id_external' => $externalId,
'data' => json_encode($externalData),
]
)->execute();
$order = \Order::get($orderId);
// dispatch order created event - order no is generated in event subscriber
$this->eventDispatcher->dispatch(new OrderEvent($order), OrderEvent::ORDER_CREATED);
$countryContext = Contexts::get(CountryContext::class);
if (!empty($data['delivery_country']) && !isset($countryContext->getAll()[$data['delivery_country']])) {
addActivityLog(ActivityLog::SEVERITY_WARNING, ActivityLog::TYPE_SYNC, 'Objednávka s č. '.$order->order_no.' z dropshipmentu byla vytvořena s neznámou zemí.', ['country' => $data['delivery_country']]);
}
return $order;
});
// zalogovat informaci o vytvoreni objednavky
$order->logHistory(
sprintf('[Dropshipment] <a href="javascript:nw(\'Dropshipment\', %s);">%s</a>: %s', $this->dropshipment['id'], $this->dropshipment['name'], $externalId)
);
$this->eventDispatcher->dispatch(new DropshipOrderCreatedEvent($order, $this->dropshipment, $xml));
return $order;
}
/**
* General method for updating an order.
*/
protected function updateDropshipOrder(\Order $order, \SimpleXMLElement $xml): void
{
throw new \RuntimeException('Order update not implemented');
}
/**
* Method for checks if order should be imported or not.
*/
protected function isDropshipOrderValidToImport(\SimpleXMLElement $xml): bool
{
return $this->isValidRestrictionByTag($xml);
}
protected function getProductByItem(\SimpleXMLElement $item): array
{
$code = (string) $item->CODE;
$ean = (string) $item->EAN;
return $this->getProductByCode($code, $ean);
}
protected function getProductByCode(?string $code, ?string $ean): array
{
$notFoundResult = [null, null];
$search = [];
if (!empty($code)) {
$search['code'] = $code;
}
if (!empty($ean)) {
$search['ean'] = $ean;
}
if (empty($search)) {
return $notFoundResult;
}
$foundItem = sqlQueryBuilder()
->select('id as id_variation, id_product')
->from('products_variations')
->where(Operator::equals($search, 'OR'))
->execute()->fetchAssociative();
if (!$foundItem) {
$foundItem = sqlQueryBuilder()
->select('id as id_product, null as id_variation')
->from('products')
->where(Operator::equals($search, 'OR'))
->execute()->fetchAssociative();
}
if ($foundItem) {
return [$foundItem['id_product'], $foundItem['id_variation']];
}
return $notFoundResult;
}
protected function getCurrencyInfo(string $currency): CurrencyInfo
{
$currencyContext = Contexts::get(CurrencyContext::class);
// pokud nemam nastaveno konvertovani cen do defaultni meny, tak provadim validaci meny
if (!$this->isPriceConvertionEnabled() && !($currencyObject = ($currencyContext->getAll()[$currency] ?? null))) {
throw new TransferException(
sprintf('Nepodařilo se naimportovat objednávku: měna "%s" není založena v e-shopu', $currency)
);
}
// pokud nemam currency rate, tak ho zkusim ziskat
try {
$currencyRate = isset($currencyObject) ? $currencyObject->getRate() : \CNB::getCurrency($currency);
} catch (\Exception $e) {
throw new TransferException(
'Nepodařilo se naimportovat objednávku: nepodařilo se získat kurz vůči výchozí měně',
['currencyRateError' => $e->getMessage()]
);
}
if (!isset($currencyObject)) {
$currencyObject = $currencyContext->getDefault();
}
return new CurrencyInfo($currencyObject, toDecimal($currencyRate));
}
protected function convertPrice(\Decimal $price, CurrencyInfo $currencyInfo): \Decimal
{
// pokud mam zapnut prevod do vychozi meny
if ($this->isPriceConvertionEnabled()) {
$price = $price->mul($currencyInfo->rate);
}
return $price;
}
protected function modifyInsertedOrder(\Order $order, \SimpleXMLElement $orderXml): void
{
}
protected function modifyItem(array $item, \SimpleXMLElement $xmlItem): array
{
return $item;
}
protected function getOrderByExternalId(string $externalId): ?\Order
{
$orderId = sqlQueryBuilder()
->select('id_order')
->from('order_dropshipment')
->where(Operator::equals(['id_external' => $externalId, 'id_dropshipment' => $this->dropshipment['id']]))
->execute()->fetchOne();
if (!$orderId) {
return null;
}
return \Order::get((int) $orderId);
}
protected function isPriceConvertionEnabled(): bool
{
return ($this->configuration['prices_to_default_currency'] ?? 'N') === 'Y';
}
protected function addActivityLog(string $message, array $data = [], string $severity = ActivityLog::SEVERITY_ERROR): void
{
addActivityLog(
$severity,
ActivityLog::TYPE_SYNC,
sprintf('[Dropshipment] "%s" (ID: %s): ', $this->dropshipment['name'], $this->dropshipment['id']).$message,
$data
);
}
protected function getLastSyncTime(): ?string
{
return sqlQueryBuilder()
->select('last_sync')
->from('dropshipment')
->where(Operator::equals(['id' => $this->dropshipment['id']]))
->execute()
->fetchOne();
}
protected function getOrdersForUpdate(): QueryBuilder
{
$qb = sqlQueryBuilder()
->select('o.id as id, od.id_external as id_external')
->from('orders', 'o')
->leftJoin('o', 'order_dropshipment', 'od', 'o.id = od.id_order AND od.id_dropshipment = :dropshipment_id')
->andWhere('o.status_storno = 0')
->andWhere(Operator::not(Operator::equalsNullable(['o.package_id' => null])))
->setParameter('dropshipment_id', $this->dropshipment['id'])
->groupBy('o.id');
return $qb;
}
protected function itemCreatedEvent(?\ProductBase $product, int $idVariation, \Decimal $piecePrice, int $pieces, ?array $data, \Order $order): void
{
if (empty($product)) {
return;
}
$this->eventDispatcher->dispatch(new OrderItemEvent($product, $idVariation, $piecePrice, $pieces, $data, $order), OrderItemEvent::ITEM_CREATED);
}
/**
* Prepares configuration data during save in admin.
*/
public function prepareConfigurationData(array $data): array
{
return $data;
}
/**
* Returns array in format: [externalId, data].
*/
abstract protected function getExternalData(\SimpleXMLElement $xml): array;
abstract protected function getDeliveryTypeByConfiguration(\SimpleXMLElement $order): ?\DeliveryType;
}