first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Admin\Actions;
use KupShop\AdminBundle\Admin\Actions\AbstractAction;
use KupShop\AdminBundle\Admin\Actions\ActionResult;
use KupShop\AdminBundle\Admin\Actions\IAction;
use KupShop\DropshipBundle\Util\TransferWorker;
use Symfony\Contracts\Service\Attribute\Required;
class DropshipRunAction extends AbstractAction implements IAction
{
#[Required]
public TransferWorker $transferWorker;
public function getTypes(): array
{
return ['Dropshipment'];
}
public function getName(): string
{
return 'Spustit dropshipment';
}
public function execute(&$data, array $config, string $type): ActionResult
{
$this->transferWorker->run(
(int) $this->getId()
);
return new ActionResult(true);
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Admin;
use KupShop\DropshipBundle\Util\TransferLocator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\System\BundleFinder;
class Dropshipment extends \Window
{
protected $required = [
'name' => true,
'source_url' => true,
];
protected $defaults = [
'active' => 1,
];
protected TransferLocator $transferLocator;
public function __construct()
{
$this->transferLocator = ServiceContainer::getService(TransferLocator::class);
}
public function processFormData(): array
{
$data = parent::processFormData();
$this->unserializeCustomData($data);
$r = array_filter($data['data']['restrictions'] ?? [], fn ($v) => !empty($v));
if (count($r) > 0) {
if (empty($r['values']) || empty($r['tagName'])) {
$this->addError(translate('restrictionsValidationError'));
}
if (!empty($r['values'])) {
$data['data']['restrictions']['values'] = preg_replace('/\s+/', ' ', trim($r['values']));
}
}
$this->serializeCustomData($data);
return $data;
}
public function get_vars()
{
$vars = parent::get_vars();
$vars['body']['configurationTemplate'] = $this->getConfigurationTemplate($vars['body']['data']['type'] ?? null);
$vars['body']['types'] = [];
foreach ($this->transferLocator->getTransfers() as $type => $transfer) {
$vars['body']['types'][$type] = $transfer::getName();
}
if ($this->getAction() == 'edit') {
$vars['body']['configurationData'] = $this->transferLocator->getTransfer($vars['body']['data']['type'])->getConfigurationVariables();
}
if (findModule(\Modules::INVOICES)) {
$vars['body']['invoices'] = sqlQueryBuilder()
->select('id, name')
->from('invoice_numbers')
->execute()
->fetchAllKeyValue();
$vars['body']['invoices'] = ['' => 'Žádná fakturační řada'] + $vars['body']['invoices'];
}
$vars['body']['data']['configuration'] = json_decode($vars['body']['data']['configuration'] ?? '', true) ?: [];
$this->unserializeCustomData($vars['body']['data']);
return $vars;
}
public function getData()
{
$data = parent::getData();
if (getVal('Submit')) {
$data['active'] = $data['active'] === 'Y' ? 1 : 0;
if (!empty($data['configuration'])) {
$data['configuration'] = $this->transferLocator->getTransfer($data['type'])
->prepareConfigurationData($data['configuration']);
}
$data['configuration'] = json_encode($data['configuration'] ?? []);
$this->serializeCustomData($data);
}
return $data;
}
protected function getConfigurationTemplate(?string $type): ?string
{
$bundleFinder = ServiceContainer::getService(BundleFinder::class);
$template = 'dropshipment.configuration.'.$type.'.tpl';
if (file_exists($bundleFinder->getBundlesPath('Admin/templates/window/')['DropshipBundle'].$template)) {
return $template;
}
return null;
}
}
return Dropshipment::class;

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\WindowTab;
class DropshipmentRestrictionsTab extends WindowTab
{
protected $title = 'flapRestrictions';
protected $template = 'window/dropshipment.restrictions.tpl';
public static function getTypes()
{
return [
'Dropshipment' => 98,
];
}
public function isVisible()
{
return $this->getAction() === 'edit';
}
public function getLabel()
{
return translate('restrictions', 'Dropshipment');
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Admin\Tabs;
use KupShop\AdminBundle\Admin\WindowTab;
use KupShop\DropshipBundle\Transfer\GenericTransfer;
use Query\Operator;
class DropshipmentTransformationTab extends WindowTab
{
protected $title = 'flapTransformation';
protected $template = 'window/dropshipment.transformation.tpl';
public static function getTypes()
{
return [
'Dropshipment' => 99,
];
}
public function isVisible()
{
if ($id = getVal('ID')) {
$type = sqlQueryBuilder()
->select('type')
->from('dropshipment')
->where(Operator::equals(['id' => $id]))
->execute()->fetchOne();
return $type === GenericTransfer::getType();
}
return false;
}
public function getLabel()
{
return translate('transformation', 'Dropshipment');
}
}

View File

@@ -0,0 +1,109 @@
<?php
$txt_str['Dropshipment'] = [
'menu' => 'Dropshipment',
'activityAdded' => 'Přidán nový dropshipment: %s',
'activityEdited' => 'Upraven dropshipment: %s',
'titleAdd' => 'Nastavení dropshipmentu',
'titleEdit' => 'Upravit nastavení dropshipmentu',
'toolbar_list' => 'Dropshipment',
'toolbar_add' => 'Přidat',
'type' => 'Typ',
'name' => 'Název',
'active' => 'Aktivní',
'lastSync' => 'Poslední synchronizace',
'generic' => 'Obecné',
'apiKey' => 'API klíč',
'companyId' => 'ID firmy',
'projectId' => 'ID projektu',
'flapDropshipment' => 'Dropshipment',
'flapMall' => 'MALL',
'flapExpando' => 'Expando',
'configuration' => 'Konfigurace',
'configurationInfo' => 'Konfiguraci lze nastavovat až po uložení.',
'sourceUrl' => 'Zdrojový XML soubor',
'settings' => 'Nastavení',
'orderLanguage' => 'Jazyk objednávky',
'deliveries' => 'Dopravy',
'deliveryType' => 'Způsob doručení',
'payments' => 'Platby',
'stores' => 'Prodejny',
'addDeliveryMapping' => 'Přidat párování',
'addMapping' => 'Přidat párování',
'mallDeliveryId' => 'MALL ID',
'delivery' => 'Doprava',
'allCountries' => 'Všechny země',
'country' => 'Země',
'out' => 'Odesílat aktualizace stavů objednávek',
'doNotStornoOrders' => 'Nestornovat objednávky v Channable',
'importStatuses' => 'Stavy k importu',
'importStatusesInfo' => 'Vyberte stavy objednávek, které chcete importovat do e-shopu. Pokud ponecháte prázdné, tak se použije výchozí nastavení e-shopu, kdy se do e-shopu importují pouze objednávky ve stavu "Unshipped".',
'expandoOutTooltip' => 'Odešle do Expanda informaci o vyřízení objednávky společně s URL adresou pro sledování zásilky.',
'channableOutTooltip' => 'Odešle do Channable informaci s číslem zásilky.',
'ordersUpdate' => 'Aktualizovat objednávky z Expanda',
'ordersUpdateTooltip' => 'Aktualizace objednávek v e-shopu podle Expanda.',
'ordersStatusUpdateShipped' => 'po expedici',
'ordersStatusUpdateShippedTooltip' => 'Aktualizace objednávky po expedici kvůli případné úpravě DIČ a DPH.',
'ordersStatusUpdateStorno' => 'storno',
'ordersStatusUpdateStornoTooltip' => 'Pokud se objednávka v Expandu stornuje, tak se storno přenese na objednávku v e-shopu.',
'value' => 'Hodnota',
'valueDeliveryInfo' => 'Do pole "Hodnota" vyplňte hodnotu, která se vyskytuje ve feedu pod elementem <DELIVERY>. Pokud necháte prázdno, tak se bude toto párování vztahovat an všechny hodnoty.',
'valuePaymentInfo' => 'Do pole "Hodnota" vyplňte hodnotu, která se vyskytuje ve feedu pod elementem <PAYMENT>. Pokud necháte prázdno, tak se bude toto párování vztahovat an všechny hodnoty.',
'selectDelivery' => 'Vyberte dopravu',
'selectPayment' => 'Vyberte platbu',
'transformation' => 'Transformace',
'updateTitle' => 'Aktualizace',
'mappingTitle' => 'Mapování',
'convertToDefaultCurrency' => 'Převádět ceny do výchozí měny',
'convertToDefaultCurrencyInfo' => 'Pokud je v transformaci specifikované měna (CURRENCY) a bude jiná, než je výchozí měna, tak se ceny automaticky převedou do výchozí měny. Zároveň se u cen nastaví výchozí DPH.',
'genericSourceInfo' => 'Validní XML příklad můžete stáhnout <a href="/admin/static/files/dropshipment_order.xml" target="_blank">zde</a>.
Pokud vaše struktura neodpovídá, tak můžete použít transformaci, která vaše XML převede na vyžadovanou strukturu.',
'genericMappingInfo' => '<p>Mapovaní doprav a plateb, které přijdou ve feedu v elementech <strong>DELIVERY</strong> nebo
<strong>PAYMENT</strong>. Do pole hodnota je potřeba vyplnit hodnotu, která se vyskytuje ve feedu, pak
případně vybrat ještě zemi, která se vyskytuje v <strong>DELIVERY_COUNTRY</strong> a nakonec je potřeba
zvolit dopravu, na kterou se má daná kombinace navázat na e-shopu.</p>
<p><strong>POZOR</strong>, že pro dané kombinace doprav a plateb musí existovat vytvořený způsob doručení.</p>',
'invoice_number' => 'Fakturační řada',
'groupName' => 'Název',
'ignoreOnMappingNotFound' => 'Ignorovat objednávku pokud není nalezeno mapování',
'ignoreOnMappingNotFoundTooltip' => 'Pokud jsou nastaveny mapování pouze s filtrem a import narazí na objednávku, ke které se nepodaří dohledat žádné mapování, tak se objednávka bude ignorovat. Pokud je tato funkce vypnuta, tak se objednávka založí bez vazby na dopravu a platbu.',
'filter' => 'Filtr',
'filterTag' => 'Název elementu',
'filterValues' => 'Hodnoty',
'filterTooltip' => 'Filtr, který určuje element a hodnotu v XML podle kterého se mapování přiřadí a aplikuje u dané objednávky. Např. název elementu `EXTERNAL/marketplace` a hodnota `kaufland` řekne, že se daná pravidla mapování mají aplikovat pouze pro objednávky s marketplace `kaufland`. Pokud necháte prázdné, tak se pravidla mapování aplikují na všechny objendávky.',
'restrictions' => 'Omezení',
'tagValuesRestriction' => 'Omezení podle hodnot v tagu',
'restrictionsTagName' => 'Tag',
'restrictionsTagNameInfo' => 'Hodnota tagu je brána přímo z feedu. Ve značkách <tag>. Tag se bere pouze z první úrovně feedu.',
'restrictionsValues' => 'Hodnoty',
'restrictionsValuesInfo' => 'Hodnoty musí být odděleny čárkou, hodnota musí odpovídat.',
'restrictionsValidationError' => 'Musí být vyplněny obě hodnoty omezení, Tag i Hodnoty.',
'statuses' => 'Stav objednávek pro odeslání informací o dopravě do Channable',
'use_channable_order_no' => 'Přebírat číslo objednávky z channable',
'useBaselinkerIntegration' => 'Použít napojení ze strany Baselinkeru',
'useBaselinkerIntegrationInfo' => 'Na straně e-shopu není potřeba žádné nastavování, protože je napojení nastaveno kompletně na straně Baselinkeru, který s e-shopem komunikuje pomocí API.',
];

View File

@@ -0,0 +1,90 @@
<?php
$txt_str['Dropshipment'] = [
'menu' => 'Dropshipment',
'activityAdded' => 'Přidán nový dropshipment: %s',
'activityEdited' => 'Upraven dropshipment: %s',
'titleAdd' => 'Nastavení dropshipmentu',
'titleEdit' => 'Upravit nastavení dropshipmentu',
'toolbar_list' => 'Dropshipment',
'toolbar_add' => 'Přidat',
'type' => 'Typ',
'name' => 'Název',
'active' => 'Active',
'lastSync' => 'Poslední synchronizace',
'generic' => 'Obecné',
'apiKey' => 'API key',
'flapDropshipment' => 'Dropshipment',
'flapMall' => 'MALL',
'flapExpando' => 'Expando',
'configuration' => 'Konfigurace',
'configurationInfo' => 'Konfiguraci lze nastavovat až po uložení.',
'sourceUrl' => 'Zdrojový XML soubor',
'settings' => 'Nastavení',
'orderLanguage' => 'Jazyk objednávky',
'deliveries' => 'Dopravy',
'payments' => 'Platby',
'stores' => 'Prodejny',
'addDeliveryMapping' => 'Přidat párování',
'addMapping' => 'Přidat párování',
'mappingTitle' => 'Delivery type mapping',
'mallDeliveryId' => 'MALL ID',
'delivery' => 'Doprava',
'allCountries' => 'Všechny země',
'country' => 'Země',
'out' => 'Send order status updates',
'doNotStornoOrders' => 'Storno orders in Channable',
'expandoOutTooltip' => 'Odešle do Expanda informaci o vyřízení objednávky společně s URL adresou pro sledování zásilky.',
'ordersUpdate' => 'Update orders from Expando',
'ordersUpdateTooltip' => 'Update orders in the e-shop by Expando.',
'ordersStatusUpdateShipped' => 'after shipping',
'ordersStatusUpdateShippedTooltip' => 'Update orders after shipping for possible VAT and VAT adjustment',
'ordersStatusUpdateStorno' => 'cancelled',
'ordersStatusUpdateStornoTooltip' => 'If the order is cancelled in Expando, the cancellation is transferred to the order in the e-shop',
'value' => 'Hodnota',
'valueDeliveryInfo' => 'Do pole "Hodnota" vyplňte hodnotu, která se vyskytuje ve feedu pod elementem <DELIVERY>. Pokud necháte prázdno, tak se bude toto párování vztahovat an všechny hodnoty.',
'valuePaymentInfo' => 'Do pole "Hodnota" vyplňte hodnotu, která se vyskytuje ve feedu pod elementem <PAYMENT>. Pokud necháte prázdno, tak se bude toto párování vztahovat an všechny hodnoty.',
'selectDelivery' => 'Vyberte dopravu',
'selectPayment' => 'Vyberte platbu',
'transformation' => 'Transformace',
'convertToDefaultCurrency' => 'Převádět ceny do výchozí měny',
'convertToDefaultCurrencyInfo' => 'Pokud je v transformaci specifikované měna (CURRENCY) a bude jiná, než je výchozí měna, tak se ceny automaticky převedou do výchozí měny.',
'genericSourceInfo' => 'Validní XML příklad můžete stáhnout <a href="/admin/static/files/dropshipment_order.xml" target="_blank">zde</a>.
Pokud vaše struktura neodpovídá, tak můžete použít transformaci, která vaše XML převede na vyžadovanou strukturu.',
'genericMappingInfo' => '<p>Mapovaní doprav a plateb, které přijdou ve feedu v elementech <strong>DELIVERY</strong> nebo
<strong>PAYMENT</strong>. Do pole hodnota je potřeba vyplnit hodnotu, která se vyskytuje ve feedu, pak
případně vybrat ještě zemi, která se vyskytuje v <strong>DELIVERY_COUNTRY</strong> a nakonec je potřeba
zvolit dopravu, na kterou se má daná kombinace navázat na e-shopu.</p>
<p><strong>POZOR</strong>, že pro dané kombinace doprav a plateb musí existovat vytvořený způsob doručení.</p>',
'invoice_number' => 'Invoice number',
'restrictions' => 'Restrictions',
'restrictionsTagName' => 'Tag',
'restrictionsTagNameInfo' => 'The tag value is taken directly from the feed. In <tag> tags. The tag is taken only from the first level of the feed.',
'restrictionsValues' => 'Values',
'restrictionsValuesInfo' => 'Values must be separated by a comma, the value must match.',
'restrictionsValidationError' => 'Both Tag and Value constraint values must be filled in.',
'companyId' => 'Company ID',
'projectId' => 'Project ID',
'statuses' => 'Send tracking info to channable if order has this status',
'use_channable_order_no' => 'Use Channable order number',
'tagValuesRestriction' => 'Restrictions by tag values',
];

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Admin\lists;
use KupShop\AdminBundle\AdminList\BaseList;
use KupShop\DropshipBundle\Util\TransferLocator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
class DropshipmentList extends BaseList
{
protected $tableDef = [
'id' => 'd.id',
'fields' => [
'ID' => ['field' => 'd.id'],
'name' => ['translate' => true, 'field' => 'd.name'],
'type' => ['translate' => true, 'field' => 'd.type', 'render' => 'renderDropshipmentType'],
'active' => ['translate' => true, 'field' => 'd.active', 'render' => 'renderBoolean'],
'lastSync' => ['translate' => true, 'field' => 'd.last_sync', 'render' => 'renderDateTime'],
],
];
protected $tableName = 'dropshipment';
protected ?string $tableAlias = 'd';
protected TransferLocator $transferLocator;
public function __construct()
{
$this->transferLocator = ServiceContainer::getService(TransferLocator::class);
}
public function renderDropshipmentType(array $values): string
{
$transfer = $this->transferLocator->getTransfer($values['type']);
return $transfer::getName();
}
}
return DropshipmentList::class;

View File

@@ -0,0 +1,4 @@
{extends "[AdminBundle]actions/baseAction.tpl"}
{block actionContent}
{/block}

View File

@@ -0,0 +1,26 @@
{extends "[DropshipBundle]window/dropshipment.configuration.generic.tpl"}
{block 'configuration'}
<div class="form-group">
<div class="col-md-2 control-label">
<label>{'useBaselinkerIntegration'|translate}</label>
<a class="help-tip" data-toggle="tooltip" title=""
data-original-title="{'useBaselinkerIntegrationInfo'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][use_baselinker_integration]" value=$body.data.configuration.use_baselinker_integration}
</div>
</div>
<div id="baselinker-deprecated-config">
{$smarty.block.parent}
</div>
<script>
$('input[name="data[configuration][use_baselinker_integration]"]').change(function() {
$('#baselinker-deprecated-config').toggle(!$(this).is(':checked'));
}).change();
</script>
{/block}

View File

@@ -0,0 +1,286 @@
<div id="configurationChannable" class="tab-pane fade in boxFlex">
<div class="form-group form-group-flex">
<div class="col-md-2 control-label">
<label>{'apiKey'|translate}</label>
</div>
<div class="col-md-10">
<input class="form-control input-sm" type="text" name="data[source_url]"
value="{$body.data.source_url}" required>
</div>
</div>
<div class="form-group form-group-flex">
<div class="col-md-2 control-label">
<label>{'companyId'|translate}</label>
</div>
<div class="col-md-3">
<input class="form-control input-sm" type="text" name="data[data][company_id]"
value="{$body.data.data.company_id}" required>
</div>
<div class="col-md-2 control-label">
<label>{'projectId'|translate}</label>
</div>
<div class="col-md-3">
<input class="form-control input-sm" type="text" name="data[data][project_id]"
value="{$body.data.data.project_id}" required>
</div>
</div>
{get_contexts language=1 assign='contexts'}
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'settings'|translate}</h1>
</div>
</div>
<div class="col-md-3 control-label">
<label>
{'out'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'channableOutTooltip'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][out]" value=$body.data.configuration.out}
</div>
<div class="col-md-3 control-label">
<label>{'doNotStornoOrders'|translate}</label>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][do_not_storno_orders]" value=$body.data.configuration.do_not_storno_orders}
</div>
<div class="col-md-3 control-label">
<label>
{'use_channable_order_no'|translate}
<a class="help-tip" data-toggle="tooltip" title="Přebírat číslo objednávky z channable">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][use_marketplace_order_no]" value=$body.data.configuration.use_marketplace_order_no}
</div>
<div class="wpj-form-group col-md-12">
<label>{'statuses'|translate}</label>
<div class="input-group col-md-2">
{print_select name="data[configuration][statuses]" var=$cfg.Order.Status.global selected=$body.data.configuration.statuses}
</div>
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'mappingTitle'|translate}</h1>
</div>
</div>
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-marketplace="add" class="btn btn-success btn-block"><span
class="glyphicon glyphicon-plus"></span>&nbsp;Přidat marketplace</a>
</div>
</div>
<div id="marketplaces" class="panel-group panel-group-lists ui-sortable">
{foreach array_merge([[]], $body.data.configuration.marketplaces|default:[]) as $key => $marketplace}
<div {if $key == 0}data-marketplace="template" style="display: none" {else}data-marketplace="item"
style="margin-bottom: 20px"{/if}>
<div class="panel">
<div class="panel-heading">
<div class="row">
<div class="col-md-3">
<input name="data[configuration][marketplaces][{$key}][name]" class="form-control input-sm"
type="text"
value="{$marketplace.name}"
placeholder="{if $key == 0}Marketplace...{else}Všechny marketplace{/if}">
<a class="help-tip" data-toggle="tooltip" title=""
data-original-title="Název marketplacu. Pokud nevyplníte, tak se bude nastavení vztahovat na všechny marketplacy.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-9">
<a class="btn-sm btn btn-danger pull-right" data-marketplace="delete">
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
</div>
<div class="panel-group panel-group-lists panel">
<div class="panel-body border"
style="border:1px solid rgba(0, 0, 0, 0.1);border-top:none;border-radius:3px;">
{ifmodule TRANSLATIONS}
<div class="row">
<div class="col-md-1 control-label">
<label>{'orderLanguage'|translate}</label>
</div>
<div class="col-md-3">
<select name="data[configuration][marketplaces][{$key}][settings][id_language]" class="selecter">
<option value="">-- výchozí jazyk --</option>
{foreach $contexts.language->getSupported() as $lang}
<option value="{$lang->getId()}"
{if $marketplace.settings.id_language == $lang->getId()}selected{/if}>{$lang->getName()}</option>
{/foreach}
</select>
</div>
</div>
{/ifmodule}
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'deliveries'|translate}</strong></h1>
<hr>
</div>
</div>
<div id="deliveryForm_{$key}">
{$deliveries = $marketplace.deliveries}
{if empty($deliveries)}
{$deliveries = []}
{/if}
{foreach array_merge([[]], $deliveries) as $dKey => $delivery}
<div id="deliveryRow_{$dKey}" {if $dKey == 0}data-form-new="" style="display: none"
{else}data-form-item=""{/if}>
<div class="row bottom-space">
<div class="col-md-1 control-label">
<label>{'country'|translate}</label>
</div>
<div class="col-md-3">
<select
name="data[configuration][marketplaces][{$key}][deliveries][{$dKey}][country]"
class="selecter"
data-autocomplete="countries" data-preload="countries">
<option value="">{'allCountries'|translate}</option>
{if $delivery.country}
<option value="{$delivery.country}" selected>{$delivery.country}</option>
{/if}
</select>
</div>
<div class="col-md-1 control-label">
<label>{'delivery'|translate}</label>
</div>
<div class="col-md-3">
<select
name="data[configuration][marketplaces][{$key}][deliveries][{$dKey}][id_delivery]"
class="selecter"
data-autocomplete="deliveries" data-preload="deliveries">
{if $delivery.id_delivery}
<option value="{$delivery.id_delivery}"
selected>{$delivery.id_delivery}</option>
{/if}
</select>
</div>
<div class="col-md-1">
<a class="btn-sm btn btn-danger" data-form-delete>
<input class="hidden" type="checkbox"
name="data[configuration][marketplaces][{$key}][deliveries][{$dKey}][delete]"/>
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
{/foreach}
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-form-add="">
<span class="glyphicon glyphicon-plus"></span>&nbsp;Přidat párování
</a>
</div>
</div>
{if $key != 0}
<script>
initForm({
selector: '#deliveryForm_{$key}',
beforeAdd: function (original) {
console.log("original: ", original);
var $addedItem = original();
window.preloadAutocompletes($addedItem);
}
});
</script>
{/if}
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'payments'|translate}</strong></h1>
<hr>
</div>
</div>
<div class="row bottom-space">
<div class="col-md-1 control-label">
<label>Platba</label>
</div>
<div class="col-md-4">
<select name="data[configuration][marketplaces][{$key}][payments][default]" class="selecter"
data-autocomplete="payments" data-preload="payments">
{if $marketplace.payments.default}
<option value="{$marketplace.payments.default}"
selected>{$marketplace.payments.default}</option>
{/if}
</select>
</div>
</div>
</div>
</div>
</div>
{/foreach}
</div>
<script type="application/javascript">
var index = 1;
var $marketplaceWrapper = $('#marketplaces');
var $template = $('[data-marketplace="template"]').clone();
$('[data-marketplace="template"]').remove();
$('[data-marketplace="add"]').on('click', function (e) {
console.log('hello')
var $item = $template.clone();
$item.removeAttr('style');
$item.removeAttr('data-marketplace');
$item.attr('data-marketplace', 'item')
$item.css('margin-bottom', '20px');
replaceAttribute($item.find('input, select'), 'name', '[marketplaces][0]', '[marketplaces][-' + index + ']');
index++;
$item.prependTo($marketplaceWrapper);
window.preloadAutocompletes($item);
initForm({
selector: $item,
beforeAdd: function (original) {
var $addedItem = original();
window.preloadAutocompletes($addedItem);
}
});
e.preventDefault();
return false;
});
$marketplaceWrapper.on('click', '[data-marketplace="delete"]', function (e) {
$(this).parents('[data-marketplace="item"]').remove();
e.preventDefault();
return false;
});
</script>
</div>

View File

@@ -0,0 +1,324 @@
<div id="configurationExpando">
<div class="form-group form-group-flex">
<div class="col-md-2 control-label">
<label>{'sourceUrl'|translate}</label>
</div>
<div class="col-md-10">
<input class="form-control input-sm" type="text" name="data[source_url]"
value="{$body.data.source_url}" required>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>{'convertToDefaultCurrency'|translate}</label>
<a class="help-tip"
data-toggle="tooltip" title=""
data-original-title="{'convertToDefaultCurrencyInfo'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][prices_to_default_currency]" value=$body.data.configuration.prices_to_default_currency}
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>
{'importStatuses'|translate}
<a class="help-tip"
data-toggle="tooltip" title=""
data-original-title="{'importStatusesInfo'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
<div class="col-md-10">
<select class="selecter" name="data[configuration][import_statuses][]" multiple>
<option value="*" {'*'|selected:$body.data.configuration.import_statuses}>Všechny stavy</option>
<option value="pending" {'pending'|selected:$body.data.configuration.import_statuses}>Pending</option>
<option value="unshipped" {'unshipped'|selected:$body.data.configuration.import_statuses}>Unshipped</option>
<option value="shipped" {'shipped'|selected:$body.data.configuration.import_statuses}>Shipped</option>
<option value="canceled" {'canceled'|selected:$body.data.configuration.import_statuses}>Canceled</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'updateTitle'|translate}</h1>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>{'apiKey'|translate}</label>
</div>
<div class="col-md-6">
<input type="text" class="form-control input-sm" name="data[configuration][api_key]"
value="{$body.data.configuration.api_key}">
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>
{'out'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'expandoOutTooltip'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][out]" value=$body.data.configuration.out}
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>
{'ordersUpdate'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'ordersUpdateTooltip'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
<div class="col-md-8">
<div class="checkbox pull-left">
<input type="checkbox" id="update-shipped" name="data[configuration][update][shipped]" value="shipped"
class="check"
{if isset($body.data.configuration.update['shipped'])}checked{/if}>
<label for="update-shipped">
{'ordersStatusUpdateShipped'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'ordersStatusUpdateShippedTooltip'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
<div class="checkbox pull-left">
<input type="checkbox" id="update-canceled" name="data[configuration][update][canceled]" value="canceled"
class="check"
{if isset($body.data.configuration.update['canceled'])}checked{/if}>
<label for="update-canceled">
{'ordersStatusUpdateStorno'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'ordersStatusUpdateStornoTooltip'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'mappingTitle'|translate}</h1>
</div>
</div>
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-marketplace="add" class="btn btn-success btn-block"><span
class="glyphicon glyphicon-plus"></span>&nbsp;Přidat marketplace</a>
</div>
</div>
{get_contexts language=1 assign='contexts'}
<div id="marketplaces" class="panel-group panel-group-lists ui-sortable">
{foreach array_merge([[]], $body.data.configuration.marketplaces|default:[]) as $key => $marketplace}
<div {if $key == 0}data-marketplace="template" style="display: none" {else}data-marketplace="item"
style="margin-bottom: 20px"{/if}>
<div class="panel">
<div class="panel-heading">
<div class="row">
<div class="col-md-3">
<input name="data[configuration][marketplaces][{$key}][name]" class="form-control input-sm"
type="text"
value="{$marketplace.name}"
placeholder="{if $key == 0}Marketplace...{else}Všechny marketplace{/if}">
<a class="help-tip" data-toggle="tooltip" title=""
data-original-title="Název marketplacu, který se nachází ve feedu v tagu <marketplace>. Pokud nevyplníte, tak se bude nastavení vztahovat na všechny marketplacy.">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-9">
<a class="btn-sm btn btn-danger pull-right" data-marketplace="delete">
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
</div>
<div class="panel-group panel-group-lists panel">
<div class="panel-body border"
style="border:1px solid rgba(0, 0, 0, 0.1);border-top:none;border-radius:3px;">
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'settings'|translate}</strong></h1>
<hr>
</div>
</div>
{ifmodule TRANSLATIONS}
<div class="row">
<div class="col-md-1 control-label">
<label>{'orderLanguage'|translate}</label>
</div>
<div class="col-md-3">
<select name="data[configuration][marketplaces][{$key}][settings][id_language]" class="selecter">
<option value="">-- výchozí jazyk --</option>
{foreach $contexts.language->getAll() as $lang}
<option value="{$lang->getId()}" {if $marketplace.settings.id_language == $lang->getId()}selected{/if}>{$lang->getName()}</option>
{/foreach}
</select>
</div>
</div>
{/ifmodule}
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'deliveries'|translate}</strong></h1>
<hr>
</div>
</div>
<div id="deliveryForm_{$key}">
{$deliveries = $marketplace.deliveries}
{if empty($deliveries)}
{$deliveries = []}
{/if}
{foreach array_merge([[]], $deliveries) as $dKey => $delivery}
<div id="deliveryRow_{$dKey}" {if $dKey == 0}data-form-new="" style="display: none"
{else}data-form-item=""{/if}>
<div class="row bottom-space">
<div class="col-md-1 control-label">
<label>{'country'|translate}</label>
</div>
<div class="col-md-3">
<select
name="data[configuration][marketplaces][{$key}][deliveries][{$dKey}][country]"
class="selecter"
data-autocomplete="countries" data-preload="countries">
<option value="">{'allCountries'|translate}</option>
{if $delivery.country}
<option value="{$delivery.country}" selected>{$delivery.country}</option>
{/if}
</select>
</div>
<div class="col-md-1 control-label">
<label>{'delivery'|translate}</label>
</div>
<div class="col-md-3">
<select
name="data[configuration][marketplaces][{$key}][deliveries][{$dKey}][id_delivery]"
class="selecter"
data-autocomplete="deliveries" data-preload="deliveries">
{if $delivery.id_delivery}
<option value="{$delivery.id_delivery}"
selected>{$delivery.id_delivery}</option>
{/if}
</select>
</div>
<div class="col-md-1">
<a class="btn-sm btn btn-danger" data-form-delete>
<input class="hidden" type="checkbox"
name="data[configuration][marketplaces][{$key}][deliveries][{$dKey}][delete]"/>
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
{/foreach}
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-form-add="">
<span class="glyphicon glyphicon-plus"></span>&nbsp;Přidat párování
</a>
</div>
</div>
{if $key != 0}
<script>
initForm({
selector: '#deliveryForm_{$key}',
beforeAdd: function (original) {
var $addedItem = original();
window.preloadAutocompletes($addedItem);
}
});
</script>
{/if}
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'payments'|translate}</strong></h1>
<hr>
</div>
</div>
<div class="row bottom-space">
<div class="col-md-1 control-label">
<label>Platba</label>
</div>
<div class="col-md-4">
<select name="data[configuration][marketplaces][{$key}][payments][default]" class="selecter"
data-autocomplete="payments" data-preload="payments">
{if $marketplace.payments.default}
<option value="{$marketplace.payments.default}"
selected>{$marketplace.payments.default}</option>
{/if}
</select>
</div>
</div>
</div>
</div>
</div>
{/foreach}
</div>
<script type="application/javascript">
var index = 1;
var $marketplaceWrapper = $('#marketplaces');
var $template = $('[data-marketplace="template"]').clone();
$('[data-marketplace="template"]').remove();
$('[data-marketplace="add"]').click(function (e) {
var $item = $template.clone();
$item.removeAttr('style');
$item.removeAttr('data-marketplace');
$item.attr('data-marketplace', 'item')
$item.css('margin-bottom', '20px');
replaceAttribute($item.find('input, select'), 'name', '[marketplaces][0]', '[marketplaces][-' + index + ']');
index++;
$item.prependTo($marketplaceWrapper);
window.preloadAutocompletes($item);
initForm({
selector: $item,
beforeAdd: function (original) {
var $addedItem = original();
window.preloadAutocompletes($addedItem);
}
});
e.preventDefault();
return false;
});
$marketplaceWrapper.on('click', '[data-marketplace="delete"]', function (e) {
$(this).parents('[data-marketplace="item"]').remove();
e.preventDefault();
return false;
});
</script>
</div>

View File

@@ -0,0 +1,318 @@
{block 'configuration'}
<div class="form-group form-group-flex">
<div class="col-md-2 control-label">
<label>{'sourceUrl'|translate}</label>
</div>
<div class="col-md-10">
<input class="form-control input-sm" type="text" name="data[source_url]"
value="{$body.data.source_url}" required>
</div>
</div>
<div id="configurationGeneric" class="tab-pane fade in boxFlex">
<div class="form-group">
<div class="col-md-2 control-label">
<label>{'convertToDefaultCurrency'|translate}</label>
<a class="help-tip" data-toggle="tooltip" title=""
data-original-title="{'convertToDefaultCurrencyInfo'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][prices_to_default_currency]" value=$body.data.configuration.prices_to_default_currency}
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>{'ignoreOnMappingNotFound'|translate}</label>
<a class="help-tip" data-toggle="tooltip" title=""
data-original-title="{'ignoreOnMappingNotFoundTooltip'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-1">
{print_toggle nameRaw="data[configuration][ignore_on_mapping_not_found]" value=$body.data.configuration.ignore_on_mapping_not_found}
</div>
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'mappingTitle'|translate}</h1>
</div>
</div>
<div class="infobox">
{'genericMappingInfo'|translate nofilter}
</div>
{get_contexts language=1 assign='contexts'}
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-mapping="add" class="btn btn-success btn-block"><span
class="glyphicon glyphicon-plus"></span>&nbsp;Přidat mapování</a>
</div>
</div>
<div id="mapping-groups" class="panel-group panel-group-lists ui-sortable">
{foreach array_merge([[]], $body.data.configuration.mappingGroups|default:[]) as $key => $group}
<div {if $key == 0}data-mapping="template" style="display: none" {else}data-mapping="item" style="margin-bottom: 20px"{/if}>
<div class="panel">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<a class="btn-sm btn btn-danger pull-right" data-mapping="delete">
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
</div>
<div class="panel-group panel-group-lists panel">
<div class="panel-body border" style="border:1px solid rgba(0, 0, 0, 0.1);border-top:none;border-radius:3px;">
<div class="row bottom-space">
<div class="col-md-1 control-label">
<label>{'groupName'|translate}</label>
</div>
<div class="col-md-3">
<input class="form-control input-sm" type="text"
name="data[configuration][mappingGroups][{$key}][name]"
value="{$group.name}">
</div>
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'settings'|translate}</strong></h1>
<hr>
</div>
</div>
<div class="row bottom-space">
<div class="col-md-1 control-label">
<label>{'filter'|translate}</label>
<a class="help-tip" data-toggle="tooltip" title=""
data-original-title="{'filterTooltip'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-3">
<input class="form-control input-sm" type="text"
name="data[configuration][mappingGroups][{$key}][filter][tag]"
value="{$group.filter.tag}" placeholder="{'filterTag'|translate}">
</div>
<div class="col-md-3">
<input class="form-control input-sm" type="text"ß
name="data[configuration][mappingGroups][{$key}][filter][value]"
value="{$group.filter.value}" placeholder="{'filterValues'|translate}">
</div>
</div>
{ifmodule TRANSLATIONS}
{if empty($group.settings.id_language)}
{$group.settings.id_language = $contexts.language->getDefaultId()}
{/if}
<div class="row">
<div class="col-md-1 control-label">
<label>{'orderLanguage'|translate}</label>
</div>
<div class="col-md-3">
<select name="data[configuration][mappingGroups][{$key}][settings][id_language]" class="selecter"
data-autocomplete="languages" data-preload="languages">
<option value="{$group.settings.id_language}" selected>{$group.settings.id_language}</option>
</select>
</div>
</div>
{/ifmodule}
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'deliveries'|translate}</strong></h1>
<hr>
</div>
</div>
{* Mapovani doprav pro danou skupinu *}
<div data-mapping-form="deliveryForm_{$key}">
{$deliveries = $group.deliveries}
{if empty($deliveries)}
{$deliveries = []}
{/if}
{foreach array_merge([[]], $deliveries) as $dKey => $delivery}
<div id="deliveryRow_{$dKey}" {if $dKey == 0}data-form-new="" style="display: none" {else}data-form-item=""{/if}>
<div class="row bottom-space">
<div class="col-md-4 col-md-offset-1">
<input class="form-control input-sm" type="text"
name="data[configuration][mappingGroups][{$key}][deliveries][{$dKey}][value]"
value="{$delivery.value}" placeholder="{'value'|translate}">
</div>
<div class="col-md-3">
<select name="data[configuration][mappingGroups][{$key}][deliveries][{$dKey}][country]" class="selecter"
data-autocomplete="countries" data-preload="countries">
<option value="">{'allCountries'|translate}</option>
{if $delivery.country}
<option value="{$delivery.country}" selected>{$delivery.country}</option>
{/if}
</select>
</div>
<div class="col-md-3">
<select name="data[configuration][mappingGroups][{$key}][deliveries][{$dKey}][id_delivery]" class="selecter"
data-autocomplete="deliveries" data-preload="deliveries" {'selectDelivery'|translate}>
{if $delivery.id_delivery}
<option value="{$delivery.id_delivery}" selected>{$delivery.id_delivery}</option>
{/if}
</select>
</div>
<div class="col-md-1">
<a class="btn-sm btn btn-danger" data-form-delete>
<input class="hidden" type="checkbox" name="data[configuration][mappingGroups][{$key}][deliveries][{$dKey}][delete]"/>
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
{/foreach}
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-form-add="">
<span class="glyphicon glyphicon-plus"></span>&nbsp;Přidat párování
</a>
</div>
</div>
{if $key != 0}
<script>
initForm({
selector: '[data-mapping-form="deliveryForm_{$key}"]',
beforeAdd: function (original) {
var $addedItem = original();
window.preloadAutocompletes($addedItem);
}
});
</script>
{/if}
</div>
<div class="row">
<div class="col-md-12">
<h1 class="h6"><strong>{'payments'|translate}</strong></h1>
<hr>
</div>
</div>
{* Mapovani plateb pro danou skupinu *}
<div data-mapping-form="paymentForm_{$key}">
{$payments = $group.payments}
{if empty($payments)}
{$payments = []}
{/if}
{foreach array_merge([[]], $payments) as $pKey => $payment}
<div id="paymentRow_{$pKey}" {if $pKey == 0}data-form-new="" style="display: none" {else}data-form-item=""{/if}>
<div class="row bottom-space">
<div class="col-md-4 col-md-offset-1">
<input class="form-control input-sm" type="text"
name="data[configuration][mappingGroups][{$key}][payments][{$pKey}][value]"
value="{$payment.value}" placeholder="{'value'|translate}">
</div>
<div class="col-md-3">
<select name="data[configuration][mappingGroups][{$key}][payments][{$pKey}][country]" class="selecter"
data-autocomplete="countries" data-preload="countries">
<option value="">{'allCountries'|translate}</option>
{if $payment.country}
<option value="{$payment.country}" selected>{$payment.country}</option>
{/if}
</select>
</div>
<div class="col-md-3">
<select name="data[configuration][mappingGroups][{$key}][payments][{$pKey}][id_payment]" class="selecter"
data-autocomplete="payments" data-preload="payments" {'selectDelivery'|translate}>
{if $payment.id_payment}
<option value="{$payment.id_payment}" selected>{$payment.id_payment}</option>
{/if}
</select>
</div>
<div class="col-md-1">
<a class="btn-sm btn btn-danger" data-form-delete>
<input class="hidden" type="checkbox" name="data[configuration][mappingGroups][{$key}][payments][{$pKey}][delete]"/>
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
{/foreach}
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-form-add="">
<span class="glyphicon glyphicon-plus"></span>&nbsp;Přidat párování
</a>
</div>
</div>
{if $key != 0}
<script>
initForm({
selector: '[data-mapping-form="paymentForm_{$key}"]',
beforeAdd: function (original) {
var $addedItem = original();
window.preloadAutocompletes($addedItem);
}
});
</script>
{/if}
</div>
</div>
</div>
</div>
{/foreach}
</div>
<script type="application/javascript">
var index = 1;
var $groupWrapper = $('#mapping-groups');
var $template = $('[data-mapping="template"]').clone();
$('[data-mapping="template"]').remove();
$('[data-mapping="add"]').click(function (e) {
var $item = $template.clone();
$item.removeAttr('style');
$item.removeAttr('data-mapping');
$item.attr('data-mapping', 'item')
$item.css('margin-bottom', '20px');
replaceAttribute($item.find('input, select'), 'name', '[mappingGroups][0]', '[mappingGroups][-' + index + ']');
index++;
$item.prependTo($groupWrapper);
window.preloadAutocompletes($item);
$item.find('[data-mapping-form]').each(function () {
initForm({
selector: $(this),
beforeAdd: function (original) {
var $addedItem = original();
window.preloadAutocompletes($addedItem);
}
});
});
e.preventDefault();
return false;
});
$groupWrapper.on('click', '[data-mapping="delete"]', function (e) {
$(this).parents('[data-mapping="item"]').remove();
e.preventDefault();
return false;
});
</script>
</div>
{/block}

View File

@@ -0,0 +1,101 @@
<div id="configurationHeureka" class="tab-pane fade in boxFlex">
<div class="form-group">
<div class="col-md-2 control-label">
<label>Client ID</label>
</div>
<div class="col-md-6">
<input type="text" class="form-control input-sm" name="data[configuration][api_key]" value="{$body.data.configuration.api_key}">
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>Heureka endpoint</label>
</div>
<div class="col-md-2">
<select name="data[configuration][endpoint]" class="selecter">
<option value="heureka.cz" {if $body.data.configuration.endpoint == "heureka.cz"}selected{/if}>heureka.cz</option>
<option value="heureka.sk" {if $body.data.configuration.endpoint == "heureka.sk"}selected{/if}>heureka.sk</option>
</select>
</div>
<div class="col-md-4">
{get_contexts domain=1 assign='contexts'}
<input type="text" class="form-control input-sm" size="5" name="endpointURL"
value="https://{$contexts.domain->getActiveId()}/_dropshipment/heureka/{$body.data.id}"
placeholder="Heureka endpoint URL" readonly>
</div>
<div class="col-md-1 control-label">
<label>Test</label>
</div>
<div class="col-md-1">
{print_toggle value=$body.data.configuration.test nameRaw="data[configuration][test]"}
</div>
</div>
<div id="heurekaPayments">
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'payments'|translate}</h1>
</div>
</div>
<div class="panel-group panel-group-lists">
<div class="panel">
<div class="row bottom-space">
<div class="col-md-2 control-label">
<label>Platební karta</label>
</div>
<div class="col-md-4">
<select name="data[configuration][payment_online_id]" class="selecter"
data-autocomplete="payments" data-preload="payments">
{if $body.data.configuration.payment_online_id}
<option value="{$body.data.configuration.payment_online_id}"
selected>{$body.data.configuration.payment_online_id}</option>
{/if}
</select>
</div>
</div>
</div>
<div class="panel">
<div class="row bottom-space">
<div class="col-md-2 control-label">
<label>Převod na účet</label>
</div>
<div class="col-md-4">
<select name="data[configuration][payment_bank_id]" class="selecter"
data-autocomplete="payments" data-preload="payments">
{if $body.data.configuration.payment_bank_id}
<option value="{$body.data.configuration.payment_bank_id}"
selected>{$body.data.configuration.payment_bank_id}</option>
{/if}
</select>
</div>
</div>
</div>
</div>
</div>
<div id="heurekaStores">
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'stores'|translate}</h1>
</div>
</div>
<div class="panel-group panel-group-lists">
{foreach $body.configurationData.deliveryTypes as $id => $value}
<div class="form-group">
<label class="col-md-3 control-label">
{$value}
</label>
<div class="col-md-3">
<div class="input-group" data-input-overwrite="">
<input type="text" class="form-control input-sm" size="5" name="data[configuration][deliveryTypes][{$id}]"
value="{$body.data.configuration.deliveryTypes[$id]}" placeholder="HeurekaID">
</div>
</div>
</div>
{/foreach}
</div>
</div>
</div>

View File

@@ -0,0 +1,136 @@
<div class="form-group form-group-flex">
<div class="col-md-2 control-label">
<label>{'sourceUrl'|translate}</label>
</div>
<div class="col-md-10">
<input class="form-control input-sm" type="text" name="data[source_url]"
value="{$body.data.source_url}" required>
</div>
</div>
<div id="configurationMall" class="tab-pane fade in boxFlex">
<div class="form-group">
<div class="col-md-2 control-label">
<label>Client ID</label>
</div>
<div class="col-md-6">
<input type="text" class="form-control input-sm" name="data[configuration][api_key]" value="{$body.data.configuration.api_key}">
</div>
</div>
<div class="form-group">
<div class="col-md-1 col-md-offset-2">
{print_toggle nameRaw="data[configuration][out]" value=$body.data.configuration.out}
</div>
<div class="col-md-5 control-label" style="text-align: left;">
<label>{'out'|translate}</label>
</div>
</div>
<div id="mallDeliveries">
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'deliveries'|translate}</h1>
</div>
</div>
<div class="row bottom-space">
<div class="col-md-3">
<a href="#" data-form-add class="btn btn-success btn-block"><span
class="glyphicon glyphicon-plus"></span>&nbsp;{'addDeliveryMapping'|translate}</a>
</div>
</div>
<div class="panel-group panel-group-lists">
{foreach array_merge([[]], $body.data.configuration.deliveries|default:[]) as $key => $row}
<div class="panel" {if $key == 0}data-form-new style="display:none" {else}data-form-item{/if}>
<div class="row bottom-space">
<div class="col-md-2 control-label">
<label>{'mallDeliveryId'|translate}</label>
</div>
<div class="col-md-1">
<input class="form-control input-sm" type="text" name="data[configuration][deliveries][{$key}][id_external]"
value="{$row.id_external}">
</div>
<div class="col-md-3">
<select name="data[configuration][deliveries][{$key}][country]" class="selecter"
data-autocomplete="countries" data-preload="countries">
<option value="">{'allCountries'|translate}</option>
{if $row.country}
<option value="{$row.country}" selected>{$row.country}</option>
{/if}
</select>
</div>
<div class="col-md-2 control-label">
<label>{'delivery'|translate}</label>
</div>
<div class="col-md-3">
<select name="data[configuration][deliveries][{$key}][id_delivery]" class="selecter"
data-autocomplete="deliveries" data-preload="deliveries">
{if $row.id_delivery}
<option value="{$row.id_delivery}" selected>{$row.id_delivery}</option>
{/if}
</select>
</div>
<div class="col-md-1">
<a class="btn-sm btn btn-danger" data-form-delete>
<input class="hidden" type="checkbox" name="data[configuration][deliveries][{$key}][delete]"/>
<span class="glyphicon glyphicon-remove"></span>
</a>
</div>
</div>
</div>
{/foreach}
</div>
</div>
<div id="mallPayments">
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'payments'|translate}</h1>
</div>
</div>
<div class="panel-group panel-group-lists">
<div class="panel">
<div class="row bottom-space">
<div class="col-md-2 control-label">
<label>Dobírka</label>
</div>
<div class="col-md-4">
<select name="data[configuration][payments][cod]" class="selecter"
data-autocomplete="payments" data-preload="payments">
{if $body.data.configuration.payments.cod}
<option value="{$body.data.configuration.payments.cod}" selected>{$body.data.configuration.payments.cod}</option>
{/if}
</select>
</div>
</div>
</div>
<div class="panel">
<div class="row bottom-space">
<div class="col-md-2 control-label">
<label>Bez dobírky</label>
</div>
<div class="col-md-4">
<select name="data[configuration][payments][paid]" class="selecter"
data-autocomplete="payments" data-preload="payments">
{if $body.data.configuration.payments.paid}
<option value="{$body.data.configuration.payments.paid}" selected>{$body.data.configuration.payments.paid}</option>
{/if}
</select>
</div>
</div>
</div>
</div>
</div>
<script type="application/javascript">
initForm({
selector: '#mallDeliveries',
beforeAdd: function (original) {
var $item = original();
window.preloadAutocompletes($item);
},
});
</script>
</div>

View File

@@ -0,0 +1,40 @@
<div id="flapRestrictions" class="tab-pane fade in boxFlex">
<div class="form-group">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading panel-heading-flex">
<small class="panel-title">{'tagValuesRestriction'|translate}</small>
</div>
<div class="panel-body">
<div class="col-md-12">
<div class="wpj-form-group">
<label>
{'restrictionsTagName'|translate}
<a class="help-tip"
data-toggle="tooltip" title=""
data-original-title="{'restrictionsTagNameInfo'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
<input id="configurationExpando-restrictions-tag" type="text" class="form-control input-sm"
name="data[data][restrictions][tagName]"
value="{$body.data.data.restrictions.tagName}">
</div>
</div>
<div class="col-md-12">
<label>
{'restrictionsValues'|translate}
<a class="help-tip"
data-toggle="tooltip" title=""
data-original-title="{'restrictionsValuesInfo'|translate}">
<i class="bi bi-question-circle"></i>
</a>
</label>
<textarea id="configurationExpando-restrictions-values" rows="2" class="form-control input-sm"
name="data[data][restrictions][values]">{if $body.data.data.restrictions.values}{$body.data.data.restrictions.values}{/if}</textarea>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,95 @@
{extends "[shared]/window.tpl"}
{block js}
{$smarty.block.parent}
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.41.0/codemirror.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.41.0/mode/xml/xml.js"></script>
{/block}
{block css append}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.41.0/codemirror.css">
<style>
.CodeMirror {
height: 100%;
}
</style>
{/block}
{block tabs}
{windowTab id='flapDropshipment' label=translate('flapDropshipment')}
{/block}
{block tabsContent}
<div id="flapDropshipment" class="tab-pane fade active in boxStatic box">
<div class="form-group form-group-flex">
<div class="col-md-2 control-label">
<label>{'name'|translate}</label>
</div>
<div class="col-md-4">
<input class="form-control input-sm" type="text" name="data[name]"
value="{$body.data.name}" required>
</div>
<div class="col-md-2 control-label">
<label>{'type'|translate}</label>
</div>
<div class="col-md-4">
{if $body.acn != 'add'}
<input type="hidden" name="data[type]" value="{$body.data.type}">
{/if}
<select class="selecter" name="data[type]" {if $body.acn != 'add'}disabled{/if}>
{foreach $body.types as $type => $name}
<option value="{$type}" {if $type == $body.data.type}selected{/if}>{$name}</option>
{/foreach}
</select>
</div>
</div>
{if $body.data.type == 'generic'}
<div class="form-group form-group-flex">
<div class="col-md-10 col-md-offset-2">
<small>
{'genericSourceInfo'|translate nofilter}
</small>
</div>
</div>
{/if}
<div class="form-group form-group-flex">
<div class="col-md-2 control-label">
<label>{'active'|translate}</label>
</div>
<div class="col-md-4">
{print_toggle name='active'}
</div>
</div>
{if $body.acn == 'add'}
<div class="infobox">
{'configurationInfo'|translate}
</div>
{elseif $body.configurationTemplate}
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'configuration'|translate}</h1>
</div>
</div>
{include "[DropshipBundle]/window/{$body.configurationTemplate}"}
{/if}
{ifmodule INVOICES}
<div class="row">
<div class="col-md-12">
<h1 class="h4 main-panel-title">{'invoice_number'|translate}</h1>
</div>
</div>
<div class="row">
<div class="col-md-2 control-label">
<label>{'invoice_number'|translate}</label>
</div>
<div class="col-md-4">
{print_select var=$body.invoices selected=$body.data.id_invoice_number name='data[id_invoice_number]'}
</div>
</div>
{/ifmodule}
</div>
{/block}

View File

@@ -0,0 +1,20 @@
<div id="flapTransformation" class="tab-pane fade in boxFlex">
<legend>Transformace</legend>
<div class="row boxFlex box">
<div class="col-md-12 boxFlex box">
<textarea name="data[transformation]" id="transform-textarea" class="form-control input-sm">{$body.data.transformation}</textarea>
</div>
</div>
<script>
$(document).ready(function () {
var editor = CodeMirror.fromTextArea(document.getElementById('transform-textarea'), {
mode: 'xml',
lineNumbers: true
});
$('#windowTables a').on('shown.bs.tab', function () {
editor.refresh();
});
});
</script>
</div>

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\AdminRegister;
use KupShop\AdminBundle\AdminRegister\AdminRegister;
use KupShop\AdminBundle\AdminRegister\IAdminRegisterDynamic;
use KupShop\AdminBundle\AdminRegister\IAdminRegisterStatic;
class DropshipAdminRegister extends AdminRegister implements IAdminRegisterDynamic, IAdminRegisterStatic
{
public function getDynamicMenu(): array
{
return [
self::createMenuItem('settingsMenu', [
'name' => 'Dropshipment',
'title' => translate('menu', 'Dropshipment'),
'left' => 's=menu.php&type=Dropshipment',
'right' => 's=list.php&type=Dropshipment',
]),
];
}
public static function getPermissions(): array
{
return [
self::createPermissions('Dropshipment', [\Modules::DROPSHIP], ['DROPSHIPMENT']),
];
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace KupShop\DropshipBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class DropshipBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(TransferInterface::class)
->addTag('dropship.transfer');
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Entity;
use KupShop\I18nBundle\Entity\Currency;
class CurrencyInfo
{
public Currency $currency;
public \Decimal $rate;
public function __construct(Currency $currency, \Decimal $rate)
{
$this->currency = $currency;
$this->rate = $rate;
}
public function getCurrencyCode(): string
{
return $this->currency->getId();
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace KupShop\DropshipBundle\Event;
use Symfony\Contracts\EventDispatcher\Event;
class DropshipOrderCreatedEvent extends Event
{
public function __construct(
public \Order $order,
public array $dropshipment,
public \SimpleXMLElement $xml,
) {
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace KupShop\DropshipBundle\EventListener;
use KupShop\DropshipBundle\Util\TransferWorker;
use KupShop\KupShopBundle\Event\CronEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CronListener implements EventSubscriberInterface
{
private TransferWorker $worker;
public function __construct(TransferWorker $worker)
{
$this->worker = $worker;
}
public static function getSubscribedEvents()
{
return [
CronEvent::RUN_FREQUENT => [
['handleTransfers', 200],
],
];
}
public function handleTransfers(): void
{
$this->worker->run();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace KupShop\DropshipBundle\Exception;
class TransferException extends \Exception
{
private array $data;
public function __construct($message = '', array $data = [], $code = 0, ?Throwable $previous = null)
{
$this->data = $data;
parent::__construct($message, $code, $previous);
}
public function getData(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace KupShop\DropshipBundle\Exception;
class TransferNotFoundException extends \Exception
{
}

View File

@@ -0,0 +1,10 @@
services:
_defaults:
autowire: true
autoconfigure: true
KupShop\DropshipBundle\:
resource: ../../{Admin/Actions,Admin/Tabs,AdminRegister,EventListener,Transfer,Util}
KupShop\DropshipBundle\Util\TransferLocator:
arguments: [!tagged dropship.transfer]

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Resources\upgrade;
use KupShop\DropshipBundle\Transfer\BaseLinkerTransfer;
use KupShop\DropshipBundle\Transfer\ExpandoTransfer;
use KupShop\DropshipBundle\Transfer\GenericTransfer;
use KupShop\DropshipBundle\Transfer\MallTransfer;
use KupShop\DropshipBundle\Util\TransferLocator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use Query\Operator;
class DropshipUpgrade extends \UpgradeNew
{
public function check_DropshipmentTable(): bool
{
return $this->checkTableExists('dropshipment');
}
/** Add 'dropshipment' table */
public function upgrade_DropshipmentTable(): void
{
$locator = ServiceContainer::getService(TransferLocator::class);
$types = array_map(fn ($x) => '"'.$x.'"', $locator->getTypes());
sqlQuery('CREATE TABLE dropshipment (
id INT(11) PRIMARY KEY AUTO_INCREMENT,
type ENUM('.implode(',', $types).') NOT NULL,
source_url VARCHAR(255) NOT NULL,
name VARCHAR(50) NOT NULL,
active TINYINT DEFAULT 1,
configuration LONGTEXT DEFAULT NULL,
transformation LONGTEXT DEFAULT NULL,
data MEDIUMTEXT DEFAULT NULL,
last_sync DATETIME DEFAULT NULL
)');
$this->upgradeOK();
}
public function check_OrderDropshipment(): bool
{
return $this->checkTableExists('order_dropshipment');
}
/** Add table `order_dropshipment` */
public function upgrade_OrderDropshipment(): void
{
sqlQuery('CREATE TABLE order_dropshipment
(
id_order INT(11) UNSIGNED NOT NULL,
id_dropshipment INT(11),
id_external VARCHAR(250) NOT NULL,
data MEDIUMTEXT DEFAULT NULL,
UNIQUE INDEX `order_dropshipment_id` (id_dropshipment, id_external),
CONSTRAINT `FK_order_dropshipment_id_order` FOREIGN KEY (`id_order`) REFERENCES `orders` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `FK_order_dropshipment_id_dropshipment` FOREIGN KEY (`id_dropshipment`) REFERENCES `dropshipment` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
);');
$this->upgradeOK();
}
public function check_DropshipmentTypeEnum(): bool
{
$locator = ServiceContainer::getService(TransferLocator::class);
return $this->checkEnumOptions('dropshipment', 'type', $locator->getTypes());
}
/** Update dropshipment.type enum */
public function upgrade_DropshipmentTypeEnum(): void
{
$locator = ServiceContainer::getService(TransferLocator::class);
$this->updateEnumOptions('dropshipment', 'type', $locator->getTypes());
$this->upgradeOK();
}
public function check_DropshipmentInSettings(): bool
{
$settings = \Settings::getDefault();
$dropship = $settings->loadValue('dropship');
return !empty($dropship);
}
/** Move dropshipment configuration from `settings` into `dropshipment` table */
public function upgrade_DropshipmentInSettings(): void
{
$settings = \Settings::getDefault();
$dropship = $settings->loadValue('dropship');
// mall to dropshipment table
if (!empty($dropship['mall']['api_key'])) {
$mallDropshipmentId = sqlGetConnection()->transactional(function () use ($dropship) {
sqlQueryBuilder()
->insert('dropshipment')
->directValues(
[
'type' => MallTransfer::getType(),
'source_url' => 'https://partners.mallgroup.com/orders.xml?client_id='.$dropship['mall']['api_key'],
'name' => 'MALL',
'active' => 1,
'configuration' => json_encode($dropship['mall'] ?? []),
]
)->execute();
return (int) sqlInsertId();
});
// naplnim order_dropshipment objednavkama z MALLu
sqlQuery('INSERT IGNORE INTO order_dropshipment (id_order, id_dropshipment, id_external, data)
SELECT
id,
'.$mallDropshipmentId.' as id_dropshipment,
JSON_VALUE(note_admin, \'$.mall.order_id\'),
JSON_EXTRACT(note_admin, \'$.mall\')
FROM orders
WHERE FIND_IN_SET("DSM", flags);');
}
// expando to dropshipment table
if (!empty($dropship['expando']['api_key'])) {
$expandoDropshipmentId = sqlGetConnection()->transactional(function () use ($dropship) {
sqlQueryBuilder()
->insert('dropshipment')
->directValues(
[
'type' => ExpandoTransfer::getType(),
'source_url' => 'https://app.expan.do/api/v2/orderfeed?days=3&access_token='.$dropship['expando']['api_key'],
'name' => 'Expando',
'active' => 1,
'configuration' => json_encode($dropship['expando'] ?? []),
]
)->execute();
return (int) sqlInsertId();
});
// naplnim order_dropshipment objednavkama z Expanda
sqlQuery('INSERT IGNORE INTO order_dropshipment (id_order, id_dropshipment, id_external, data)
SELECT
id,
'.$expandoDropshipmentId.' as id_dropshipment,
JSON_VALUE(note_admin, \'$.expando.orderId\'),
JSON_EXTRACT(note_admin, \'$.expando\')
FROM orders
WHERE FIND_IN_SET("DSE", flags);');
}
$settings->deleteValue('dropship');
$this->upgradeOK();
}
public function check_invoiceNumberIdColumn(): bool
{
return findModule(\Modules::INVOICES) && $this->checkColumnExists('dropshipment', 'id_invoice_number');
}
/** Add dropshipment.id_invoice_number column */
public function upgrade_invoiceNumberIdColumn(): void
{
if (findModule(\Modules::INVOICES)) {
sqlQuery('ALTER TABLE dropshipment ADD COLUMN id_invoice_number INT DEFAULT NULL');
sqlQuery('ALTER TABLE dropshipment ADD FOREIGN KEY (id_invoice_number) REFERENCES invoice_numbers (id) ON DELETE CASCADE ON UPDATE CASCADE');
$this->upgradeOK();
}
}
public function check_GenericTypeMappingGroups(): bool
{
return sqlQueryBuilder()
->select('id')
->from('dropshipment')
->andWhere(Operator::inStringArray([BaseLinkerTransfer::getType(), GenericTransfer::getType()], 'type'))
->andWhere('configuration NOT LIKE "%ignore_on_mapping_not_found%"')
->execute()->rowCount() > 0;
}
/** Update dropship config for types: generic and baselinker */
public function upgrade_GenericTypeMappingGroups(): void
{
$qb = sqlQueryBuilder()
->select('id, configuration')
->from('dropshipment')
->andWhere(Operator::inStringArray([BaseLinkerTransfer::getType(), GenericTransfer::getType()], 'type'))
->andWhere('configuration NOT LIKE "%ignore_on_mapping_not_found%"');
foreach ($qb->execute() as $item) {
$configuration = json_decode($item['configuration'] ?: '', true) ?: [];
$configuration['ignore_on_mapping_not_found'] = 'N';
if (!array_key_exists('mappingGroups', $configuration)
&& (array_key_exists('deliveries', $configuration) || array_key_exists('payments', $configuration))) {
$group = [
'name' => 'Výchozí',
'deliveries' => $configuration['deliveries'],
'payments' => $configuration['payments'],
];
$configuration['mappingGroups'] = [$group];
unset($configuration['deliveries'], $configuration['payments']);
}
sqlQueryBuilder()
->update('dropshipment')
->directValues(['configuration' => json_encode($configuration)])
->where(Operator::equals(['id' => $item['id']]))
->execute();
}
$this->upgradeOK();
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Tests;
use KupShop\DropshipBundle\Transfer\ExpandoTransfer;
class TransferRestrictionsTest extends \DatabaseTestCase
{
public function testEmptyDropshipment(): void
{
$xml = simplexml_load_file(__DIR__.'/files/xml_order_1.xml');
/** @var ExpandoTransfer $dropshipment */
$dropshipment = $this->get(ExpandoTransfer::class);
$this->assertEquals(true, $dropshipment->isValidRestrictionByTag($xml));
}
/**
* @dataProvider provideCases
*/
public function testRestrictions(array $restrictions, bool $isValid): void
{
$xml = simplexml_load_file(__DIR__.'/files/xml_order_1.xml');
/** @var ExpandoTransfer $dropshipment */
$dropshipment = $this->get(ExpandoTransfer::class);
$dropshipment->dropshipment['data']['restrictions'] = $restrictions;
$this->assertEquals($isValid, $dropshipment->isValidRestrictionByTag($xml));
}
public function provideCases(): \Generator
{
yield 'empty restrictions' => [
'restrictions' => [],
'isValid' => true,
];
yield 'single invalid value in tagName' => [
'restrictions' => [
'tagName' => 'marketplace',
'values' => 'AMAZON ES',
],
'isValid' => false,
];
yield 'match case-sensitive case in values (preventive for AMAZON Uk)' => [
'restrictions' => [
'tagName' => 'marketplace',
'values' => 'aMaZoN ES',
],
'isValid' => false,
];
yield 'restriction value => out of range' => [
'restrictions' => [
'tagName' => 'marketplace',
'values' => 'AMAZON UK',
],
'isValid' => true,
];
yield 'tagName change => match value' => [
'restrictions' => [
'tagName' => 'fulfillmentChannel',
'values' => 'FBA',
],
'isValid' => false,
];
yield 'tagName change => unmatch value' => [
'restrictions' => [
'tagName' => 'fulfillmentChannel',
'values' => 'FBAA',
],
'isValid' => true,
];
yield 'tagName does not exists' => [
'restrictions' => [
'tagName' => 'foofoofoo',
'values' => 'FBA',
],
'isValid' => true,
];
}
}

View File

@@ -0,0 +1,124 @@
{
"dropshipment" : [
{
"id" : 2,
"type" : "expando",
"source_url" : "https://api.expan.do/api/v2/orderfeed?access_token=0FC-nKJ~hr.TuX1W2tzLJl&days=5",
"name" : "Expando - Amazon",
"active" : 1,
"configuration" : "{\"prices_to_default_currency\":\"N\",\"api_key\":\"0FC-nKJ~hr.TuX1W2tzLJl\",\"out\":\"Y\",\"update\":{\"shipped\":\"shipped\"},\"marketplaces\":[{\"name\":\"Amazon ES\",\"settings\":{\"id_language\":\"\"},\"deliveries\":[]},{\"name\":\"Amazon IT\",\"settings\":{\"id_language\":\"\"},\"deliveries\":[]},{\"name\":\"Amazon FR\",\"settings\":{\"id_language\":\"\"},\"deliveries\":[]},{\"name\":\"Amazon DE\",\"settings\":{\"id_language\":\"\"},\"deliveries\":[]}]}",
"transformation" : null,
"data" : "{\"restrictions\":{\"tagName\":\"marketplace\",\"values\":\"AMAZON UK, AMAZON SE\"}}",
"last_sync" : "2024-10-17 07:27:44"
}
],
"stores" : [
{
"id" : 7,
"position" : 3,
"name" : "Amazon",
"id_delivery" : null,
"type" : 2,
"figure" : "N",
"data" : null
},
{
"id": 1,
"position": 0,
"name": "Hlavní sklad",
"id_delivery": null,
"type": 1,
"figure": "Y",
"data": null
}
],
"products" : [
{
"id" : 76,
"id_block" : null,
"title" : "Stříbrný prsten se Swarovski® Zirconia",
"code" : "FNJR085sw",
"ean" : 8596012204380,
"short_descr" : "",
"long_descr" : "<p>Zásnubní stříbrný prsten v lesklém provedení je skvostně zkrášlen průzračným a výrazně třpytivým zirkonem 3 mm se Swarovski Zirconia.</p>\r\n\r\n<p>Vhodné pro: Každodenní nošení, zásnubní prsten, dárek pro ženu.<br />\r\nPrstýnek bude hezkým dárkem věnovaným z lásky.<br />\r\n<br />\r\nMateriál: Stříbro ryzost 925/1000<br />\r\nPovrchová úprava: Rhodiovaný povrch - vysoký lesk, vzhled bílého zlata, šperk je odolný vůči oxidaci (nečerná) a je hypoalergenní (mohou ho nosit i lidé s alergií na stříbro). </p>\r\n\r\n<p>Kámen: 3 mm Swarovski® Zirconia</p>",
"parameters" : "",
"price" : 818.1818,
"price_for_discount" : 818.1818,
"price_common" : 0.0000,
"vat" : 1,
"id_cn" : null,
"discount" : 0.00000000,
"producer" : null,
"guarantee" : 0,
"in_store" : 319,
"pieces_sold" : 1701,
"delivery_time" : 0,
"campaign" : "NO",
"updated" : "2024-10-15 08:16:08",
"date_added" : "2016-11-22 07:57:32",
"figure" : "Y",
"show_raw_price" : "N",
"position" : 0,
"meta_title" : "Silvego stříbrný prsten se Swarovski® Zirconia",
"meta_description" : null,
"meta_keywords" : "stříbrný prsten, Swarovski zirconia, zásnubní,dárek",
"show_in_feed" : "Y",
"max_cpc" : 0,
"note" : null,
"weight" : 0.001000,
"data" : "{\"generate_coupon\":\"N\",\"mainPhoto\":\"fnjr085sw-1.jpg\",\"generate_coupon_discount\":\"4\"}",
"bonus_points" : null,
"show_in_search" : "Y",
"width" : null,
"height" : null,
"depth" : null,
"date_stock_in" : "2024-09-06 15:08:40",
"price_buy" : 125.9917
}
],
"products_variations" : [
{
"id" : 401,
"id_product" : 76,
"code" : "FNJR085sw-obvod 58 mm",
"ean" : 8596012211777,
"title" : "Velikost: obvod 58 mm",
"in_store" : 319,
"delivery_time" : 0,
"price" : 818.1818,
"price_for_discount" : 818.1820,
"figure" : "Y",
"updated" : "2024-10-14 14:32:18",
"date_added" : "2017-09-04 08:39:15",
"note" : "R52",
"weight" : 0.002000,
"bonus_points" : null,
"width" : null,
"height" : null,
"depth" : null,
"data" : "{\"mainPhoto\":\"\"}",
"price_common" : null,
"price_buy" : 158.6612
}
],
"stores_items" : [
{
"id" : 565,
"id_store" : 7,
"id_product" : 76,
"id_variation" : 401,
"quantity" : 272,
"min_quantity" : null,
"integrity_unique" : "7-76-401"
},
{
"id": 6952,
"id_store": 1,
"id_product": 76,
"id_variation": 401,
"quantity": 47,
"min_quantity": 0,
"integrity_unique": "1-76-401"
}
]
}

View File

@@ -0,0 +1,70 @@
<?php
namespace KupShop\DropshipBundle\Tests;
use KupShop\DropshipBundle\Transfer\ExpandoTransfer;
use KupShop\DropshipBundle\Util\DropshipmentUtil;
use PHPUnit\DbUnit\DataSet\ArrayDataSet;
use PHPUnit\DbUnit\DataSet\DefaultDataSet;
class TransferTest extends \DatabaseTestCase
{
protected ExpandoTransfer $expandoTransfer;
protected DropshipmentUtil $dropShipmentUtil;
protected function setUp(): void
{
parent::setUp();
$this->expandoTransfer = $this->get(ExpandoTransfer::class);
$this->dropShipmentUtil = $this->get(DropshipmentUtil::class);
}
public function testStoresExpandoTransfer(): void
{
$this->expandoTransfer->setup($this->dropShipmentUtil->getDropshipment(2)); // expando
$this->expandoTransfer->dropshipment['source_url'] = __DIR__.'/files/xml_expando_feed.xml';
$stores_items = sqlQueryBuilder()
->select('*')
->from('stores_items')
->where('id_product = 76 AND id_variation = 401')
->execute()
->fetchAllAssociative();
$variation = sqlQueryBuilder()
->select('*')
->from('products_variations')
->where('id = 401 AND id_product = 76')
->execute()
->fetchAssociative();
$this->assertEquals(47, $stores_items[1]['quantity']);
$this->assertEquals(319, $variation['in_store']);
$this->expandoTransfer->process();
$stores_items = sqlQueryBuilder()
->select('*')
->from('stores_items')
->where('id_product = 76 AND id_variation = 401')
->execute()
->fetchAllAssociative();
$variation = sqlQueryBuilder()
->select('*')
->from('products_variations')
->where('id = 401 AND id_product = 76')
->execute()
->fetchAssociative();
// Kontrola odebraného kusu
$this->assertEquals(46, $stores_items[1]['quantity']);
$this->assertEquals(318, $variation['in_store']);
}
public function getDataSet(): ArrayDataSet|DefaultDataSet
{
return $this->getJsonDataSetFromFile();
}
}

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8"?>
<orders>
<order>
<orderId>304-0019058-9770707</orderId>
<orderStatus>New</orderStatus>
<purchaseDate>2024-10-01 07:48:20</purchaseDate>
<marketplace>AMAZON DE</marketplace>
<venue>AMAZON</venue>
<fulfillmentChannel>FBA</fulfillmentChannel>
<businessOrder>false</businessOrder>
<totalPrice>54.9</totalPrice>
<totalItemTax>8.77</totalItemTax>
<currencyCode>EUR</currencyCode>
<language>DE</language>
<paymentMethod>Other</paymentMethod>
<shippingMethod>Expedited</shippingMethod>
<shipServiceLevel>Expedited</shipServiceLevel>
<deliveryBranchId/>
<shippingPrice>0</shippingPrice>
<latestShipDate>2024-10-01 21:59:59</latestShipDate>
<latestDeliveryDate/>
<isPremiumOrder>false</isPremiumOrder>
<isPrime>false</isPrime>
<isComplete>true</isComplete>
<isRefunded>true</isRefunded>
<invoiceUrls>
<url>https://sellercentral.amazon.de/document/download?v=urn%3aalx%3adoc%3a9d8cab0f-1f96-4462-8642-ace99a371392%3ab43a6044-a30d-4437-bf56-6de3e97e5007&amp;t=EU_Retail_Forward</url>
</invoiceUrls>
<invoices>
<invoice>
<id>INV-DE-157682631-2024-2132</id>
<type>SHIPMENT</type>
<url>https://sellercentral.amazon.de/document/download?v=urn%3aalx%3adoc%3a9d8cab0f-1f96-4462-8642-ace99a371392%3ab43a6044-a30d-4437-bf56-6de3e97e5007&amp;t=EU_Retail_Forward</url>
<date>2024-10-01 00:00:00</date>
</invoice>
</invoices>
<billingAddress>
<companyName/>
<name/>
<email/>
<phone/>
<address1/>
<address2/>
<address3/>
<city/>
<province/>
<zip/>
<countryCode/>
</billingAddress>
<customer>
<companyName>-</companyName>
<firstname>DHL</firstname>
<surname/>
<email>hy43dcn96qblxvl@marketplace.amazon.de</email>
<phone>-</phone>
<taxId/>
<taxCountry/>
<address>
<address1>Packstation 220</address1>
<address2>Sturmstr. 80</address2>
<address3/>
<city>Düsseldorf</city>
<zip>40229</zip>
<stateOrRegion>-</stateOrRegion>
<country>DE</country>
</address>
</customer>
<items>
<item>
<itemId>FNJR085sw_FNJR085sw-obvod 58 mm</itemId>
<externalId/>
<itemPrice>54.9</itemPrice>
<itemQuantity>1</itemQuantity>
<itemName>SILVEGO - FNJR085sw - Verlobungsring aus 925 Sterling Silber mit Swarovski Zirconia</itemName>
<orderItemId>42369721088562</orderItemId>
<itemTax>8.77</itemTax>
<shippingDiscount>0</shippingDiscount>
<shippingDiscountTax>0</shippingDiscountTax>
<promotionDiscount>0</promotionDiscount>
<promotionDiscountTax>0</promotionDiscountTax>
<shippingPrice>0</shippingPrice>
<shippingTax>0</shippingTax>
<lineItemPrice>
<tax>8.77</tax>
<taxRatePercent>19</taxRatePercent>
<withoutTax/>
<withTax>54.9</withTax>
</lineItemPrice>
<lineItemDiscount>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax/>
</lineItemDiscount>
<deliveryPrice>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax/>
</deliveryPrice>
<deliveryDiscount>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax/>
</deliveryDiscount>
<marketplaceCommission>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax/>
</marketplaceCommission>
</item>
</items>
<price>
<delivery>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax/>
</delivery>
<items>
<tax>8.77</tax>
<taxRatePercent/>
<withoutTax/>
<withTax>54.9</withTax>
</items>
<payment>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax/>
</payment>
<total>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax>54.9</withTax>
</total>
<totalDiscount>
<tax/>
<taxRatePercent/>
<withoutTax/>
<withTax/>
</totalDiscount>
</price>
<payment>
<paymentMethod>CreditCard</paymentMethod>
<cashOnDelivery>
<toPay/>
<servicePrice/>
</cashOnDelivery>
</payment>
<delivery>
<shippingCarrier/>
<shippingCarrierService/>
</delivery>
<cancelRequest>
<cancelReason/>
<marketplaceCancelReason/>
</cancelRequest>
<returnLabels>
</returnLabels>
<parcelShop>
<parcelShopIdentification/>
<parcelShopBranchCode/>
</parcelShop>
</order>
</orders>

View File

@@ -0,0 +1,73 @@
<order>
<orderId>404-8297607-3401137</orderId>
<orderStatus>Canceled</orderStatus>
<purchaseDate>2024-02-15 10:54:35</purchaseDate>
<marketplace>AMAZON ES</marketplace>
<venue>AMAZON</venue>
<fulfillmentChannel>FBA</fulfillmentChannel>
<businessOrder>false</businessOrder>
<totalPrice>0</totalPrice>
<totalItemTax>0</totalItemTax>
<currencyCode/>
<language>ES</language>
<paymentMethod>Other</paymentMethod>
<shippingMethod>Standard</shippingMethod>
<shipServiceLevel>Standard</shipServiceLevel>
<deliveryBranchId/>
<shippingPrice>0</shippingPrice>
<latestShipDate>2024-02-23 22:59:59</latestShipDate>
<latestDeliveryDate/>
<isPremiumOrder>false</isPremiumOrder>
<isPrime>false</isPrime>
<isComplete>false</isComplete>
<isRefunded>false</isRefunded>
<invoiceUrls> </invoiceUrls>
<invoices> </invoices>
<billingAddress>
<companyName/>
<name/>
<email/>
<phone/>
<address1/>
<address2/>
<address3/>
<city/>
<province/>
<zip/>
<countryCode/>
</billingAddress>
<customer>
<companyName>-</companyName>
<firstname>-</firstname>
<surname/>
<email>-</email>
<phone>-</phone>
<taxId/>
<taxCountry/>
<address>
<address1/>
<address2/>
<address3/>
<city>-</city>
<zip>-</zip>
<stateOrRegion>-</stateOrRegion>
<country>-</country>
</address>
</customer>
<items>
...
</items>
<price>
...
</price>
<payment>
...
</payment>
<delivery>
...
</delivery>
<cancelRequest>
...
</cancelRequest>
<returnLabels> </returnLabels>
</order>

View File

@@ -0,0 +1,433 @@
<?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;
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Transfer;
class BaseLinkerTransfer extends GenericTransfer
{
protected static string $type = 'baselinker';
protected static string $name = 'BaseLinker';
public function isRunnable(): bool
{
if (($this->configuration['use_baselinker_integration'] ?? 'N') === 'Y') {
return false;
}
return parent::isRunnable();
}
}

View File

@@ -0,0 +1,720 @@
<?php
namespace KupShop\DropshipBundle\Transfer;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\DropshipBundle\TransferInterface;
use KupShop\DropshipBundle\Util\TransferWorker;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Event\OrderEvent;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Psr\Log\LoggerInterface;
use Query\Operator;
use Query\QueryBuilder;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Service\Attribute\Required;
class ChannableTransfer extends AbstractTransfer implements TransferInterface
{
protected static string $type = 'channable';
protected static string $name = 'Channable';
public const API_URL_START = 'https://api.channable.com/v1/companies/';
public ContextManager $contextManager;
private HttpClientInterface $httpClient;
/** @required */
public TransferWorker $transferWorker;
protected LoggerInterface $logger;
public function __construct(HttpClientInterface $httpClient, LoggerInterface $logger)
{
$this->httpClient = $httpClient;
$this->logger = $logger;
}
#[Required]
final public function setContextManager(ContextManager $contextManager)
{
$this->contextManager = $contextManager;
}
public function sendTrackingInfoToChannable(): void
{
$qb = $this->getOrdersForUpdate();
foreach ($qb->execute() as $item) {
$order = new \Order();
$order->createFromDB($item['id']);
$connectionData = $this->getConnectionData();
if (!$connectionData) {
$this->addActivityLog('V nastavení dropshipmentu chybí potřebné údaje pro komunikaci s Channable',
severity: ActivityLog::SEVERITY_WARNING);
return;
}
$orderInfo = ServiceContainer::getService(OrderInfo::class);
$delivery = $order->getDeliveryType()->getDelivery()->class;
$packages = array_keys($orderInfo->getPackages($order));
$connectionData['url'] .= '/'.$item['channable_id'].'/shipment';
// load order items
$connectionData['body']['products'] = json_decode($order->note_admin, true)['channable']['channable_item_ids'] ?? [];
$connectionData['body']['packages'] = $packages[0];
$connectionData['body']['delivery'] = $delivery;
if ($this->sendTrackingInfo($connectionData)) {
$order->logHistory('Informace o balíku byly odeslány do Channable');
$this->addActivityLog('Informace o balíku byly odeslány do Channable - objednávka: '.$order->order_no, $connectionData['body'], severity: ActivityLog::SEVERITY_SUCCESS);
sqlQuery('UPDATE order_dropshipment SET data = JSON_OBJECT("trackingSent", "Y")
WHERE id_order = :order_id AND id_dropshipment = :id_dropshipment',
['order_id' => $item['id'], 'id_dropshipment' => $this->dropshipment['id']]);
continue;
}
$this->addActivityLog('Nepodařilo se odestal informace o balíku do Channable u objednávky č. '.$order->order_no);
$order->logHistory('Nepodařilo se odestal informace o balíku do Channable');
}
}
protected function getExternalData(\SimpleXMLElement $xml): array
{
// TODO: Implement getExternalData() method.
return [];
}
protected function getDeliveryTypeByConfiguration(\SimpleXMLElement $order): ?\DeliveryType
{
// TODO: Implement getDeliveryTypeByConfiguration() method.
return null;
}
public function in(array $config): void
{
$ordersJson = $this->getOrders();
$orders = json_decode($ordersJson, true)['orders'] ?? [];
$countryContext = Contexts::get(CountryContext::class);
foreach ($orders as $orderData) {
$this->contextManager->activateContexts([CountryContext::class => $orderData['data']['billing']['country_code'] ?? $countryContext->getDefaultId()],
function () use ($orderData) {
// log incoming order to Kibana
$this->logger->notice(self::$name.' IN', $orderData);
$externalId = $orderData['channel_id'];
if (!$externalId) {
$this->addActivityLog(
'Nepodařilo se naimportovat objednávku do e-shopu, protože nemá externí ID. V odpovědi z Channable chybí ID',
$orderData
);
return;
}
// hledame i podle platform_id, protoze nektere objednavky to maji nastavene jako id_external
$order = $this->getOrderByExternalId($externalId) ?? $this->getOrderByExternalId($orderData['platform_id'] ?? '');
if ($order) {
$this->updateChannableOrder($order, $orderData);
return;
}
try {
$order = sqlGetConnection()->transactional(function () use ($externalId, $orderData) {
$currencyInfo = $this->getCurrencyInfo($orderData['data']['price']['currency']);
$data = $this->getOrderBaseData($orderData);
$data['currency_rate'] = $currencyInfo->rate->asFloat();
$order = $this->createChannableOrder($data, $externalId);
$orderItems = [];
foreach ($orderData['data']['products'] as $item) {
[$code, $ean] = $this->getProductFromItem($item);
[$productId, $variationId] = $this->getProductByCode($code, $ean ?? null);
$pieces = toDecimal($item['quantity']);
$piecePrice = $this->convertPrice(toDecimal($item['price']), $currencyInfo);
$totalPrice = $this->convertPrice(toDecimal($item['price'])->mul($pieces), $currencyInfo);
if ($productId) {
$product = \Variation::createProductOrVariation($productId, $variationId);
$product->createFromDB();
$product->sell($variationId, $pieces->asFloat());
$piecePrice = $piecePrice->removeVat($product->vat);
$totalPrice = $totalPrice->removeVat($product->vat);
}
$productTitle = empty($product->title) ? $item['title'] : $product->title;
$itemData = [
'id_order' => $order->id,
'id_product' => $product->id ?? null,
'id_variation' => $variationId,
'pieces' => $pieces->asFloat(),
'piece_price' => $piecePrice->asFloat(),
'total_price' => $totalPrice->asFloat(),
'descr' => $productTitle,
'tax' => $item['tax'] ?? $product->vat ?? '',
];
sqlQueryBuilder()
->insert('order_items')
->directValues($itemData)
->execute();
$itemData['id'] = sqlInsertId();
$orderItems[] = $itemData;
$this->itemCreatedEvent(
product: $product ?? null,
idVariation: (int) $variationId,
piecePrice: $piecePrice,
pieces: $pieces->asInteger(),
data: [
'row' => $itemData,
'items_table' => 'order_items',
],
order: $order
);
unset($product);
}
$this->insertDeliveryItem($order, $orderData, $orderItems ?? []);
$order->recalculate(round: false);
if (!$order->isPaid() && $data['status_payed'] == 1) {
$this->payDropshipOrder($order);
}
return $order;
});
} catch (\Throwable $e) {
$this->transferWorker->logException($e, $this);
}
});
}
}
public function out(array $config): void
{
if (isDevelopment()) {
return;
}
$this->sendTrackingInfoToChannable();
$this->sendCancelledOrders();
}
protected function sendCancelledOrders()
{
$configuration = $this->getConfiguration();
if ($configuration['do_not_storno_orders'] == 'Y' ?? false) {
return;
}
$qb = $this->getCancelledOrders();
foreach ($qb->execute() as $cancelledOrder) {
$order = new \Order();
$order->createFromDB($cancelledOrder['id']);
$connectionData = $this->getConnectionData();
if (!$connectionData) {
$this->addActivityLog('V nastavení dropshipmentu chybí potřebné údaje pro komunikaci s Channable',
severity: ActivityLog::SEVERITY_WARNING);
return;
}
$connectionData['url'] .= '/'.$cancelledOrder['channable_id'].'/cancel';
$connectionData['body']['products'] = json_decode($order->note_admin, true)['channable']['channable_item_ids'] ?? [];
if ($this->sendCancelledOrder($connectionData)) {
$order->logHistory('Informace o zrušení objednávky byly odeslány do Channable');
sqlQuery('UPDATE order_dropshipment SET data = JSON_OBJECT("cancelSent", "Y")
WHERE id_order = :order_id AND id_dropshipment = :id_dropshipment',
['order_id' => $cancelledOrder['id'], 'id_dropshipment' => $this->dropshipment['id']]);
continue;
}
$this->addActivityLog('Nepodařilo se odeslat informace o zrušení objednávky do Channable u objednávky č. '.$order->order_no);
$order->logHistory('Nepodařilo se odeslat informace o zrušení objednávky do Channable');
}
}
protected function getOrders(): ?string
{
$connectionData = $this->getConnectionData();
if (!$connectionData) {
$this->addActivityLog('V nastavení dropshipmentu chybí potřebné údaje pro komunikaci s Channable',
severity: ActivityLog::SEVERITY_WARNING);
return null;
}
$lastSyncTime = $this->getLastSyncTime();
// potrebuju prevest $lastSyncTime na UTC cas, jinak se stavalo, ze se objednavky vubec na shop nedostavaly
$time = new \DateTime($lastSyncTime);
$time->setTimezone(new \DateTimeZone('UTC'));
$lastSyncTime = $time->format('Y-m-d H:i:s');
$response = $this->httpClient->request(
'GET',
$connectionData['url'],
[
'headers' => [
'Authorization' => $connectionData['api_key'],
],
'query' => [
'last_modified_after' => $lastSyncTime ?? date('Y-m-d H:i:s', strtotime('-1 day')),
],
]
);
if ($response->getStatusCode() != 200) {
$this->addActivityLog('Nepodařilo se načíst objednávky z Channable', [$response->getContent(false)]);
return '';
}
$logData = ['last_sync_time' => $lastSyncTime];
$this->addActivityLog('Načtení objednávek z Channable proběhlo úspěšně', data: $logData, severity: ActivityLog::SEVERITY_SUCCESS);
return $response->getContent(false);
}
protected function sendTrackingInfo(array $data): bool
{
$response = $this->httpClient->request(
'POST',
$data['url'],
[
'headers' => [
'Authorization' => $data['api_key'],
'Content-Type' => 'application/json',
],
'body' => json_encode([
'order_item_ids' => $data['body']['products'],
'tracking_code' => (string) $data['body']['packages'],
]),
]
);
if ($response->getStatusCode() != 200) {
$this->addActivityLog('[Channable] Odesílání informací o doručení - chybná response', [$response->getContent(false)]);
$this->addActivityLog('Nepodařilo se odeslat informace o balíku do Channable', [$response->getContent(false)]);
return false;
}
$this->addActivityLog('[Channable] Odesílání informací o doručení - response', [$response->getContent(false)], severity: ActivityLog::SEVERITY_SUCCESS);
return true;
}
protected function sendCancelledOrder(array $data): bool
{
$response = $this->httpClient->request(
'POST',
$data['url'],
[
'headers' => [
'Authorization' => $data['api_key'],
'Content-Type' => 'application/json',
],
'body' => json_encode([
'order_item_ids' => $data['body']['products'],
]),
]
);
if ($response->getStatusCode() != 200) {
$this->addActivityLog('Nepodařilo se odeslat informace o zrušení objednávky do Channable', [$response->getContent(false)]);
return false;
}
return true;
}
protected function updateChannableOrder(\Order $order, array $orderData): void
{
$data = $this->getOrderBaseData($orderData);
if (!$order->isPaid() && $data['status_payed'] == 1) {
sqlQueryBuilder()
->update('orders')
->directValues(['status_payed' => 1])
->where(Operator::equals(['id' => $order->id]))
->execute();
$this->payDropshipOrder($order);
}
}
protected function getOrderBaseData(array $order): array
{
$configuration = $this->getConfiguration();
$data = [];
$useMarketplaceOrderNo = $configuration['use_marketplace_order_no'] ?? 'N';
if ($useMarketplaceOrderNo === 'Y') {
$data['order_no'] = $order['channel_id'];
}
$data['note_invoice'] = $order['channel_id'];
$data['date_created'] = $this->dateTimeConvertor($order['created']);
$data['date_updated'] = $this->dateTimeConvertor($order['modified']);
$data['currency'] = $order['data']['price']['currency'];
$data['status'] = 0;
$data['status_payed'] = $order['status_paid'] === 'paid' ? 1 : 0;
$data['total_price'] = 0.0000; // 0 => RECALCULATE
// INVOICE
$data['invoice_name'] = $order['data']['billing']['first_name'].' '.$order['data']['billing']['middle_name'];
$data['invoice_surname'] = $order['data']['billing']['last_name'] ?? '';
$data['invoice_firm'] = $order['data']['billing']['company'] ?? '';
$data['invoice_dic'] = $order['data']['billing']['vat_number'] ?? '';
$data['invoice_street'] = $order['data']['billing']['address1'] ?? '';
$data['invoice_city'] = $order['data']['billing']['city'] ?? '';
$data['invoice_zip'] = $order['data']['billing']['zip_code'] ?? '';
$data['invoice_country'] = $order['data']['billing']['country_code'] ?? '';
$data['invoice_email'] = $order['data']['billing']['email'] ?? '';
$data['invoice_phone'] = $order['data']['customer']['phone'] ?? '';
// DELIVERY
$data['delivery_name'] = $order['data']['shipping']['first_name'].' '.$order['data']['shipping']['middle_name'];
$data['delivery_surname'] = $order['data']['shipping']['last_name'] ?? '';
$data['delivery_firm'] = $order['data']['shipping']['company'] ?? '';
$data['delivery_street'] = $order['data']['shipping']['address1'] ?? '';
$data['delivery_city'] = $order['data']['shipping']['city'] ?? '';
$data['delivery_zip'] = $order['data']['shipping']['zip_code'] ?? '';
$data['delivery_country'] = $order['data']['shipping']['country_code'] ?? '';
$data['delivery_phone'] = $order['data']['customer']['phone'] ?? '';
$deliveryType = $this->getDelivery($order);
$data['delivery_type'] = $deliveryType->name ?? '';
$data['id_delivery'] = $deliveryType->id ?? null;
$data['id_language'] = $this->getOrderLanguage($order, $configuration);
$channableOrderItemIds = [];
foreach ($order['data']['products'] as $item) {
$channableOrderItemIds[] = $item['channable_order_item_id'] ?? null;
}
$data['note_admin'] = json_encode(
[
'channable' => [
'id' => $order['id'],
'channel_name' => $order['channel_name'],
'platform_name' => $order['platform_name'],
'project_id' => $order['project_id'],
'order_config_id' => $order['order_config_id'],
'platform_id' => $order['platform_id'],
'channel_id' => $order['channel_id'],
'channable_item_ids' => $channableOrderItemIds,
],
],
);
$flags = ['DSC'];
$data['flags'] = implode(',', $flags);
return $data;
}
protected function createChannableOrder(array $data, int|string $externalId)
{
/** @var \Order $order */
$order = sqlGetConnection()->transactional(function () use ($data) {
sqlQueryBuilder()
->insert('orders')
->directValues(array_merge(['source' => OrderInfo::ORDER_SOURCE_DROPSHIP], $data))
->execute();
$order = \Order::get((int) sqlInsertId());
// dispatch order created event - order no is generated in event subscriber
$this->eventDispatcher->dispatch(new OrderEvent($order), OrderEvent::ORDER_CREATED);
return $order;
});
// vytvorim mapovani na dropshipment
sqlQueryBuilder()
->insert('order_dropshipment')
->directValues(
[
'id_order' => $order->id,
'id_dropshipment' => $this->dropshipment['id'],
'id_external' => $externalId,
]
)->execute();
// zalogovat informaci o vytvoreni objednavky
$order->logHistory(
sprintf('[Dropshipment] <a href="javascript:nw(\'Dropshipment\', %s);">%s</a>: %s',
$this->dropshipment['id'],
'channable',
$externalId)
);
return $order;
}
protected function getDelivery(array $order): ?\DeliveryType
{
$channelConfiguration = $this->getChannelConfiguration($order);
$deliveryId = null;
$country = $order['data']['shipping']['country_code'];
foreach ($channelConfiguration['deliveries'] ?? [] as $delivery) {
if (empty($delivery['country']) || $delivery['country'] === $country) {
$deliveryId = (int) $delivery['id_delivery'];
}
}
$paymentId = empty($channelConfiguration['payments']['default']) ? null : (int) $channelConfiguration['payments']['default'];
return $this->findDeliveryType($deliveryId, $paymentId);
}
protected function getChannelConfiguration(array $order): array
{
$configuration = $this->getConfiguration();
$channelName = $order['channel_name'];
$channelConfiguration = null;
$default = null;
foreach ($configuration['marketplaces'] ?? [] as $marketplace) {
if (StringUtil::slugify($marketplace['name']) === StringUtil::slugify($channelName)) {
$channelConfiguration = $marketplace;
break;
}
if (empty($marketplace['name'])) {
$default = $marketplace;
}
}
if (!$channelConfiguration && $default) {
$channelConfiguration = $default;
}
return $channelConfiguration ?: [];
}
protected function insertDeliveryItem(\Order $order, array $data, array $orderItems): void
{
$dbcfg = \Settings::getDefault();
$deliveryPrice = $this->getDeliveryPrice($order);
$tax = ($dbcfg->delivery_config['from_products'] ?? 'N') == 'Y' ?
$this->getDeliveryVatFromProducts($orderItems)
: $deliveryPrice->getVat() ?? \DecimalConstants::zero();
$shippingPrice = toDecimal($data['data']['price']['shipping']) ?? $deliveryPrice->getPriceWithVat() ?? \DecimalConstants::zero();
$shippingPrice = $shippingPrice->removeVat($tax);
$insertData = [
'id_order' => $order->id,
'id_product' => null,
'id_variation' => null,
'pieces' => 1,
'pieces_reserved' => 1,
'piece_price' => $shippingPrice ?? $deliveryPrice->getPriceWithoutVat(),
'total_price' => $shippingPrice ?? $deliveryPrice->getPriceWithoutVat(),
'tax' => $tax ?? 0,
'descr' => 'Doprava a platba',
'note' => json_encode(['item_type' => OrderItemInfo::TYPE_DELIVERY]),
];
sqlQueryBuilder()
->insert('order_items')
->directValues($insertData)
->execute();
}
protected function getDeliveryPrice(\Order $order)
{
$deliveryType = $order->getDeliveryType();
$deliveryPrice = $this->contextManager->activateContexts(
[
CurrencyContext::class => $order->currency,
],
function () use ($deliveryType) {
return $deliveryType->getPrice();
}
);
return $deliveryPrice;
}
protected function getOrdersForUpdate(): QueryBuilder
{
$qb = parent::getOrdersForUpdate();
$qb->addSelect('JSON_VALUE(o.note_admin, "$.channable.id") as channable_id');
$qb->andWhere(
Operator::equalsNullable([JsonOperator::value('od.data', 'trackingSent') => null])
)
->andWhere(
Operator::not(Operator::equalsNullable([JsonOperator::value('o.note_admin', 'channable.*') => null]))
)
->andWhere(Operator::inIntArray([(int) str_replace('"', '', $this->getConfigurationStatus())], 'o.status'));
return $qb;
}
protected function getConfigurationStatus(): string
{
return sqlQueryBuilder()
->select('JSON_EXTRACT(configuration, "$.statuses[0]") as statuses')
->from('dropshipment')
->where(Operator::equals(['id' => $this->dropshipment['id']]))
->execute()->fetchOne();
}
protected function getCancelledOrders(): QueryBuilder
{
return sqlQueryBuilder()
->select('o.id as id, od.id_external as id_external, JSON_VALUE(o.note_admin, "$.channable.id") as channable_id')
->from('orders', 'o')
->join('o', 'order_dropshipment', 'od', 'o.id = od.id_order AND od.id_dropshipment = :dropshipment_id')
->where('o.status_storno = 1')
->andWhere(Operator::equalsNullable([JsonOperator::value('od.data', 'cancelSent') => null]))
->andWhere(Operator::equalsNullable([JsonOperator::value('od.data', 'trackingSent') => null]))
->setParameter('dropshipment_id', $this->dropshipment['id']);
}
public function prepareConfigurationData(array $data): array
{
foreach ($data['marketplaces'] ?? [] as $key => $marketplace) {
// zpracovani mapovani doprav
foreach ($marketplace['deliveries'] ?? [] as $dKey => $delivery) {
$delivery = array_filter($delivery);
if (!empty($delivery['delete'])) {
unset($data['marketplaces'][$key]['deliveries'][$dKey]);
continue;
}
if ($dKey <= 0) {
if (!empty($delivery['id_delivery'])) {
$data['marketplaces'][$key]['deliveries'][] = $delivery;
}
unset($data['marketplaces'][$key]['deliveries'][$dKey]);
}
}
}
$data['marketplaces'] = array_values($data['marketplaces'] ?? []);
return $data;
}
public function getConnectionData(): ?array
{
$url = self::API_URL_START;
$data = $this->getChannableData();
$companyId = str_replace('"', '', $data['company_id']);
$project_id = str_replace('"', '', $data['project_id']);
if (!$data['company_id'] || !$data['project_id'] || !$data['api_key']) {
return null;
}
return [
'url' => $url.(int) $companyId.'/projects/'.(int) $project_id.'/orders',
'api_key' => 'Bearer '.$data['api_key'],
];
}
protected function getChannableData()
{
return sqlQueryBuilder()
->select('JSON_EXTRACT(data, \'$.company_id\') as company_id, JSON_EXTRACT(data, \'$.project_id\') as project_id, source_url as api_key')
->from('dropshipment')
->where(Operator::equals(['id' => $this->dropshipment['id']]))
->execute()
->fetchAssociative();
}
protected function getOrderLanguage(array $order, array $configuration): string
{
$languageContext = Contexts::get(LanguageContext::class);
$marketplace = array_filter($configuration['marketplaces'], fn ($marketplace) => strtolower($marketplace['name']) === strtolower($order['channel_name'])) ?? [];
$marketplace = reset($marketplace);
return $marketplace['settings']['id_language'] ?? $languageContext->getDefaultId();
}
protected function getProductFromItem(array $item): array
{
$return = [null, null];
if ($item['id']) {
$return[0] = $item['id'];
}
if ($item['ean']) {
$return[1] = $item['ean'];
}
return $return;
}
protected function getDeliveryVatFromProducts(array $orderItems): int
{
return array_reduce($orderItems, function ($maxVat, $product) {
$vat = (int) $product['tax'] ?? 0;
return ($vat > $maxVat) ? $vat : $maxVat;
}, 0);
}
/**
* Converts datetime to specific timezone.
*/
private function dateTimeConvertor(string $datetime, string $timezone = 'Europe/Prague')
{
$date = new \DateTime($datetime);
$date->setTimezone(new \DateTimeZone($timezone));
return $date->format('Y-m-d H:i:s');
}
}

View File

@@ -0,0 +1,810 @@
<?php
namespace KupShop\DropshipBundle\Transfer;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\DropshipBundle\Exception\TransferException;
use KupShop\DropshipBundle\TransferInterface;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
use KupShop\KupShopBundle\Util\StringUtil;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use KupShop\OrderingBundle\Util\Order\OrderUtil;
use Psr\Log\LoggerInterface;
use Query\Operator;
use Query\QueryBuilder;
class ExpandoTransfer extends AbstractTransfer implements TransferInterface
{
use \DatabaseCommunication;
protected static string $type = 'expando';
protected static string $name = 'Expando';
public const URL_FULFILLMENT = 'https://app.expan.do/api/v2/fulfillment';
protected array $existsOrdersCache = [];
protected OrderUtil $orderUtil;
protected LoggerInterface $logger;
private $sentryLogger;
private $currencyContext;
private $countryContext;
private $vatContext;
public function __construct(
SentryLogger $sentryLogger,
CurrencyContext $currencyContext,
CountryContext $countryContext,
VatContext $vatContext,
LoggerInterface $logger,
) {
$this->sentryLogger = $sentryLogger;
$this->currencyContext = $currencyContext;
$this->countryContext = $countryContext;
$this->vatContext = $vatContext;
$this->logger = $logger;
}
/**
* @required
*/
public function setOrderUtil(OrderUtil $orderUtil): void
{
$this->orderUtil = $orderUtil;
}
public function in(array $config): void
{
$cfg = Config::get();
if (!($xml = $this->loadXML())) {
return;
}
$currencies = $this->currencyContext->getAll();
$update = $config['update'] ?? null;
$orders = [];
foreach ($xml->order as $order) {
$orders[] = $order;
}
$this->loadExistsOrdersCache($orders);
foreach (array_reverse($orders) as $order) {
if (!$this->isDropshipOrderValidToImport($order)) {
// objednavka uz existuje
if ($update && ($orderUpdateObj = $this->getOrderByExternalId((string) $order->orderId))) {
$this->updateDropshipOrder($orderUpdateObj, $order);
}
continue;
}
$currencyInfo = $this->getCurrencyInfo(
$this->getCurrency($order)
);
$currencyCode = $this->isPriceConvertionEnabled() ? $this->currencyContext->getDefaultId() : $currencyInfo->getCurrencyCode();
$status = 0;
$statusStorno = 0;
if (!$this->isOrderStatusValidToImport($order)) {
continue;
}
$statusXML = strtolower((string) $order->orderStatus);
// import cancelled order
if ($statusXML === 'canceled') {
$statuses = array_filter(array_keys($cfg['Order']['Status']['global']), fn ($x) => $x < 100);
$status = end($statuses);
$statusStorno = 1;
}
$totalPrice = (string) $order->totalPrice;
$totalPrice = toDecimal($totalPrice);
$deliveryType = $this->getDeliveryType($order);
$delivery_type = $order->shippingMethod.' - '.$order->paymentMethod;
$id_delivery = null;
if ($deliveryType) {
$delivery_type = $deliveryType->name;
$id_delivery = $deliveryType->id;
}
$customer = $order->customer;
$userData = $this->getUserData($order);
$country = $userData['invoice_country'];
// Dropship Expando flag
$flags = ['DSE'];
// pokud je OSS aktivni, tak nastavit flag
if ($this->vatContext->isCountryOssActive($country) && empty($userData['invoice_dic'])) {
$flags[] = 'OSS';
}
$data = [
'currency' => $currencyCode,
'currency_rate' => $currencies[$currencyCode]->getRate(),
'date_created' => $this->getOrderDateCreated($order)->format('Y-m-d H:i:s'),
'status' => $status,
'status_storno' => $statusStorno,
'total_price' => $totalPrice,
'id_delivery' => $id_delivery,
'delivery_type' => $delivery_type,
'flags' => implode(',', $flags),
'note_invoice' => (string) $order->orderId,
'note_admin' => json_encode(
[
'expando' => [
'orderId' => (string) $order->orderId,
'marketplace' => (string) $order->marketplace,
'country' => $customer->address->country->__toString(),
],
]
),
'source' => OrderInfo::ORDER_SOURCE_DROPSHIP,
];
// nastavim jazyk objednavky
if (findModule(\Modules::TRANSLATIONS)) {
$marketplaceSettings = $this->getMarketplaceConfiguration($order)['settings'] ?? [];
$data['id_language'] = !empty($marketplaceSettings['id_language']) ? $marketplaceSettings['id_language'] : Contexts::get(LanguageContext::class)->getDefaultId();
}
$data = array_merge(
$data,
$userData
);
$orderObj = sqlGetConnection()->transactional(function () use ($order, $data, $currencyInfo) {
$orderObj = $this->createDropshipOrder($order, $data);
$orderObj->logHistory('[Dropshipment] Marketplace: '.$order->marketplace);
// Vytvorit itemy objednavky
$lastItemTax = null;
foreach ($order->items->item as $item) {
if (!$this->isOrderItemValidToImport($item)) {
continue;
}
[$productID, $variationID] = $this->getProductByItem($item);
$itemTax = $this->getItemTax($item);
// pokud je zapnuta konverze do defaultni meny, tak nastavim i defaultni DPH
if ($this->isPriceConvertionEnabled()) {
$itemTax = getAdminVat()['value'];
}
$itemPrice = $this->convertPrice($this->getItemPiecePrice($item), $currencyInfo)->removeVat(toDecimal($itemTax));
$itemQuantity = toDecimal((string) $item->itemQuantity);
if ($variationID) {
$product = new \Variation($productID, $variationID);
} else {
$product = new \Product($productID);
}
// sell product
if ($productID) {
$product->createFromDB($productID);
$product->sell($variationID, $itemQuantity->asInteger());
}
$title = $product->title;
if (!empty($title) && $variationID) {
$title .= ' ('.$product->variationTitle.')';
}
$modifiedItem = $this->modifyItem([
'id_order' => $orderObj->id,
'id_product' => $productID,
'id_variation' => $variationID,
'pieces' => $itemQuantity,
'pieces_reserved' => $itemQuantity,
'piece_price' => $itemPrice,
'total_price' => $itemPrice->mul($itemQuantity),
'descr' => (!empty($title)) ? $title : (string) $item->itemName,
'tax' => $itemTax,
'note' => json_encode(['item_type' => OrderItemInfo::TYPE_PRODUCT]),
], $item);
$this->insertSQL('order_items', $modifiedItem);
$modifiedItem['id'] = sqlInsertId();
$this->itemCreatedEvent(
product: $product,
idVariation: (int) $variationID,
piecePrice: $itemPrice,
pieces: $itemQuantity->asInteger(),
data: [
'row' => $modifiedItem,
'items_table' => 'order_items',
],
order: $orderObj
);
$lastItemTax = $itemTax;
}
// Doprava a platba
$paymentPrice = toDecimal((string) $order->paymentPrice);
$shippingPrice = toDecimal((string) $order->shippingPrice);
$shippingTaxValue = toDecimal((string) $order->price->delivery->tax);
$shippingTax = $this->calculateItemTax($shippingPrice, $shippingTaxValue)->printFloatValue(-2);
if (!$shippingTax && $lastItemTax) {
$shippingTax = $lastItemTax;
}
// pokud je zapnuta konverze do defaultni meny, tak nastavim i defaultni DPH
if ($this->isPriceConvertionEnabled()) {
$shippingTax = toDecimal(getAdminVat()['value']);
}
$deliveryItemPrice = $shippingPrice->add($paymentPrice);
$deliveryItemPrice = $this->convertPrice($deliveryItemPrice, $currencyInfo)->removeVat($shippingTax);
$this->insertDeliveryItem(
[
'id_order' => $orderObj->id,
'id_product' => null,
'id_variation' => null,
'pieces' => 1,
'pieces_reserved' => 1,
'piece_price' => $deliveryItemPrice,
'total_price' => $deliveryItemPrice,
'descr' => 'Doprava a platba',
'tax' => $shippingTax,
'note' => json_encode(['item_type' => OrderItemInfo::TYPE_DELIVERY]),
]
);
$this->contextManager->activateOrder($orderObj, function () use ($orderObj) {
$orderObj->recalculate(round: false);
});
return $orderObj;
});
$this->modifyInsertedOrder($orderObj, $order);
}
}
public function out(array $config): void
{
if (isDevelopment()) {
return;
}
if (!($accessToken = $config['api_key'] ?? null)) {
$this->addActivityLog(
'V nastavení dropshipmentu chybí API klíč!',
$config
);
return;
}
$qb = $this->getBaseOutQueryBuilder();
foreach ($qb->execute() as $item) {
$order = new \Order();
$order->createFromDB($item['id']);
$balikobotData = json_decode($item['balikobot_data'] ?? '', true) ?? [];
$expandoData = $order->getData('expando');
[$carrier, $carrierName] = $this->getOutCarrier($order);
$params = [
'marketplaceOrderId' => $expandoData['orderId'],
'marketplace' => $this->getOutMarketplace($expandoData['marketplace'] ?? ''),
'shipDate' => $order->date_handle ? $order->date_handle->format('c') : (new \DateTime())->format('c'),
'status' => 'Shipped',
'carrier' => $carrier,
'carrierName' => $carrierName,
];
if ($order->package_id) {
$params['trackingNumber'] = $order->package_id;
$params['trackingUrl'] = $this->getTrackingUrl($order, $balikobotData);
}
try {
$this->sendFulfillment($accessToken, $params);
$expandoData['fulfillmentSent'] = true;
$order->setData('expando', $expandoData);
$order->logHistory('Objednávka v Expandu byla aktualizována na vyřízenou');
} catch (TransferException $e) {
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_SYNC, $e->getMessage(), $params);
}
}
}
protected function loadExistsOrdersCache(array $orders): void
{
$ids = array_map(function ($x) {
return (string) $x->orderId;
}, $orders);
$qb = sqlQueryBuilder()
->select('id_order AS id, id_external AS expandoId')
->from('order_dropshipment')
->sendToMaster()
->where(Operator::andX(
Operator::inStringArray($ids, 'id_external'),
Operator::equals([
'id_dropshipment' => $this->dropshipment['id'],
])
));
$this->existsOrdersCache = Mapping::mapKeys($qb->execute()->fetchAllAssociative(), function ($k, $v) {
return [$v['expandoId'], $v['id']];
});
}
protected function isDropshipOrderValidToImport(\SimpleXMLElement $xml): bool
{
return parent::isDropshipOrderValidToImport($xml) && $this->isValidToImport($xml);
}
protected function isOrderItemValidToImport(\SimpleXMLElement $item): bool
{
return true;
}
protected function isValidToImport(\SimpleXMLElement $order): bool
{
// pokud objednavka uz existuje, tak vracim false, protoze ji nechci importovat znovu
if ($this->existsOrdersCache[(string) $order->orderId] ?? false) {
return false;
}
return true;
}
protected function getExternalData(\SimpleXMLElement $xml): array
{
return [
(string) $xml->orderId,
[
'marketplace' => (string) $xml->marketplace,
'country' => $xml->customer->address->country->__toString(),
],
];
}
protected function getOrderDateCreated(\SimpleXMLElement $order): \DateTimeInterface
{
try {
$date = new \DateTime((string) $order->purchaseDate, new \DateTimeZone('UTC'));
$date->setTimezone(new \DateTimeZone('Europe/Prague'));
} catch (\Exception) {
$date = new \DateTime();
}
return $date;
}
protected function getCurrency(\SimpleXMLElement $order): ?string
{
$currencyCode = (string) $order->currencyCode;
if (empty($currencyCode)) {
$currencyCode = 'EUR';
}
return $currencyCode;
}
protected function getDeliveryType(\SimpleXMLElement $orderItem): ?\DeliveryType
{
return $this->getDeliveryTypeByConfiguration($orderItem);
}
protected function isOrderStatusValidToImport(\SimpleXMLElement $order): bool
{
$statusXML = strtolower((string) $order->orderStatus);
$importStatuses = $this->getConfiguration()['import_statuses'] ?? [];
// default behaviour if config is not set
if (empty($importStatuses)) {
return !in_array($statusXML, $this->getIgnoredStatuses());
}
return in_array($statusXML, $importStatuses);
}
protected function getIgnoredStatuses(): array
{
return [
'canceled',
'pending',
'shipped',
];
}
protected function getItemPiecePrice(\SimpleXMLElement $item): \Decimal
{
return toDecimal((string) $item->itemPrice);
}
protected function getItemTax(\SimpleXMLElement $item): float
{
$line = $item->lineItemPrice;
$priceWithTax = toDecimal((string) $line->withTax);
$taxValue = toDecimal((string) $line->tax);
return $this->calculateItemTax($priceWithTax, $taxValue)->printFloatValue(-2);
}
protected function calculateItemTax(\Decimal $priceWithTax, \Decimal $taxValue): \Decimal
{
if ($taxValue->isZero()) {
return toDecimal(0);
}
$priceWithoutTax = $priceWithTax->sub($taxValue);
return $priceWithTax->div($priceWithoutTax)->mul(\DecimalConstants::hundred())->sub(\DecimalConstants::hundred())->round();
}
protected function getProductByItem(\SimpleXMLElement $item): array
{
[$productId, $variationId] = $this->findProduct((string) $item->itemId);
if (empty($productId) && empty($variationId)) {
return $this->getProductByCode((string) $item->itemId, null);
}
return [$productId, $variationId];
}
protected function getUserData(\SimpleXMLElement $order): array
{
$customer = $order->customer;
$country = $customer->address->country->__toString();
// validate country
if (!isset($this->countryContext->getAll()[$country])) {
$country = '';
}
return [
'invoice_name' => (string) $customer->firstname,
'invoice_surname' => (string) $customer->surname,
'invoice_email' => (string) $customer->email,
'invoice_phone' => (string) $customer->phone,
'invoice_street' => $this->prepareAddressStreet($customer->address),
'invoice_city' => (string) $customer->address->city,
'invoice_zip' => (string) $customer->address->zip,
'invoice_country' => $country,
'invoice_custom_address' => (string) $customer->address->address3,
'invoice_state' => (string) $customer->address->stateOrRegion,
'invoice_dic' => (string) $customer->taxId,
'delivery_name' => (string) $customer->firstname,
'delivery_surname' => (string) $customer->surname,
'delivery_street' => $this->prepareAddressStreet($customer->address),
'delivery_city' => (string) $customer->address->city,
'delivery_zip' => (string) $customer->address->zip,
'delivery_country' => $country,
'delivery_custom_address' => (string) $customer->address->address3,
'delivery_state' => (string) $customer->address->stateOrRegion,
];
}
protected function findProduct(string $itemId): array
{
$codes = explode('_', $itemId);
$productCode = $codes[0] ?? null;
$variationCode = $codes[1] ?? null;
if ($variationCode) {
$variation = sqlQueryBuilder()
->select('id, id_product')
->from('products_variations')
->where(Operator::equals(['code' => $variationCode]))
->execute()->fetch();
if ($variation) {
return [(int) $variation['id_product'], (int) $variation['id']];
}
$variation = sqlQueryBuilder()
->select('id, id_product')
->from('products_variations')
->where(Operator::equals(['id' => $variationCode]))
->execute()->fetch();
if ($variation) {
return [(int) $variation['id_product'], (int) $variation['id']];
}
}
if ($productCode) {
$productId = sqlQueryBuilder()
->select('id')
->from('products')
->where(Operator::equals(['code' => $productCode]))
->execute()->fetchColumn();
if ($productId) {
return [(int) $productId, null];
}
$productId = sqlQueryBuilder()
->select('id')
->from('products')
->where(Operator::equals(['id' => $productCode]))
->execute()->fetchColumn();
if ($productId) {
return [(int) $productId, null];
}
}
return [null, null];
}
protected function insertDeliveryItem(array $item): void
{
$this->insertSQL('order_items', $item);
}
protected function getTrackingUrl(\Order $order, array $balikobotData): ?string
{
if (!($trackingUrl = ($balikobotData['response'][0]['track_url'] ?? false))) {
if ($deliveryType = $order->getDeliveryType()) {
if ($delivery = $deliveryType->getDelivery()) {
$trackingUrl = $delivery->getTrackAndTraceLink($order->package_id, $order);
}
}
}
if (!$trackingUrl) {
return null;
}
return $trackingUrl;
}
protected function getOutCarrier(\Order $order): array
{
$marketplace = $order->getData('expando')['marketplace'] ?? null;
if ($deliveryType = $order->getDeliveryType()) {
if ($delivery = $deliveryType->getDelivery()) {
if ($delivery instanceof \DHL) {
return ['DHL', null];
} elseif ($delivery instanceof \DPD) {
return ['DPD', null];
} elseif ($delivery instanceof \GLS) {
return ['GLS', null];
} elseif ($delivery instanceof \PPL) {
// Amazon nezna PPL, takze to musim poslat jako "Other"
if (strpos(mb_strtolower($marketplace), 'amazon') !== false) {
return ['Other', 'PPL'];
}
return ['PPL', null];
} elseif ($delivery instanceof \Fedex) {
return ['FedEx', null];
} elseif ($delivery instanceof \UPS) {
return ['UPS', 'Standard'];
}
}
}
return ['Other', $order->getDeliveryType()->delivery ?? ''];
}
private function prepareAddressStreet(\SimpleXMLElement $address): string
{
$street = [(string) $address->address1, (string) $address->address2];
$street = implode(', ', array_filter($street));
return $street;
}
private function getOutMarketplace(string $marketplace): string
{
return preg_replace('/\s+/', '_', mb_strtolower($marketplace));
}
private function sendFulfillment(string $accessToken, array $params): void
{
$ch = curl_init();
$this->logger->notice(sprintf('EXPANDO REQUEST: ', static::getType()),
[
'Data' => $params,
'Type' => static::getType(),
]);
curl_setopt($ch, CURLOPT_URL, self::URL_FULFILLMENT);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type:application/json', 'Authorization: Bearer '.$accessToken]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
curl_setopt($ch, CURLOPT_POST, true);
$result = curl_exec($ch);
$this->logger->notice(sprintf('EXPANDO RESPONSE: ', static::getType()),
[
'data' => (array) $result,
'Type' => static::getType(),
]);
curl_close($ch);
$result = json_decode($result, true);
if ($result['message'] ?? null) {
if (StringUtil::startsWith($result['message'], 'ValidationError')) {
throw new TransferException(
sprintf('Send fulfillment failed with message "%s"', $result['message'])
);
}
}
}
protected function getMarketplaceConfiguration(\SimpleXMLElement $order): array
{
// konfigurace dropshipmentu
$configuration = $this->getConfiguration();
// budu hledat konfiguraci podle marketplacu
$orderMarketplace = $order->marketplace->__toString();
$marketplaceConfiguration = null;
$default = null;
foreach ($configuration['marketplaces'] ?? [] as $marketplace) {
// podle nazvu zkusim najit konfiguraci pro dany marketplace
if (StringUtil::slugify($marketplace['name']) === StringUtil::slugify($orderMarketplace)) {
$marketplaceConfiguration = $marketplace;
break;
}
if (empty($marketplace['name'])) {
$default = $marketplace;
}
}
if (!$marketplaceConfiguration && $default) {
$marketplaceConfiguration = $default;
}
return $marketplaceConfiguration ?: [];
}
protected function getDeliveryTypeByConfiguration(\SimpleXMLElement $order): ?\DeliveryType
{
$config = $this->getMarketplaceConfiguration($order);
$deliveryId = null;
// najdu ID dopravy
$country = $order->customer->address->country->__toString();
foreach ($config['deliveries'] ?? [] as $item) {
if (empty($item['country']) || $item['country'] == $country) {
$deliveryId = (int) $item['id_delivery'];
break;
}
}
// nactu ID platby
$paymentId = empty($config['payments']['default']) ? null : (int) $config['payments']['default'];
return $this->findDeliveryType($deliveryId, $paymentId);
}
protected function updateDropshipOrder(\Order $order, \SimpleXMLElement $xml): void
{
$updateStatuses = $this->configuration['update'] ?? [];
$currentStatusXml = strtolower((string) $xml->orderStatus);
// pokud je povoleny update stavu
if (!in_array($currentStatusXml, $updateStatuses)) {
return;
}
// storno objednavky
if ($currentStatusXml === 'canceled') {
if (!$order->status_storno) {
$order->storno(false, '[Expando] Storno objednávky z Expanda', false);
}
return;
}
if ($currentStatusXml !== 'shipped') {
return;
}
// update stavu po expedici
$dic = (string) $xml->customer->taxId;
if ($dic != $order->invoice_dic) {
$this->updateSQL('orders', ['invoice_dic' => $dic], ['id' => $order->id]);
if ($this->vatContext->isCountryOssActive($order->invoice_country)) {
if (empty($dic)) {
$this->orderUtil->addFlag($order, 'OSS');
} else {
$this->orderUtil->removeFlag($order, 'OSS');
$this->orderUtil->removeVat($order, 0, false, true); // $keepFinalPrice = true
}
}
}
}
protected function getBaseOutQueryBuilder(): QueryBuilder
{
$qb = sqlQueryBuilder()
->select('o.id')
->from('orders', 'o')
->where(
Operator::not(
Operator::equalsNullable([JsonOperator::value('o.note_admin', 'expando.orderId') => null])
)
)
->andWhere(
Operator::equalsNullable([JsonOperator::value('o.note_admin', 'expando.fulfillmentSent') => null])
)
->andWhere('o.status_storno = 0')
->andWhere(Operator::inIntArray(getStatuses('handled'), 'o.status'))
->andWhere('DATEDIFF(NOW(), o.date_handle) <= 30') // ne starsi nez 30 dni
->groupBy('o.id');
if (findModule(\Modules::BALIKONOS, 'provider') === 'balikobot') {
$qb->addSelect('b.data as balikobot_data')
->leftJoin('o', 'balikonos', 'b', 'b.id_order = o.id');
}
return $qb;
}
public function prepareConfigurationData(array $data): array
{
foreach ($data['marketplaces'] ?? [] as $key => $marketplace) {
// zpracovani mapovani doprav
foreach ($marketplace['deliveries'] ?? [] as $dKey => $delivery) {
$delivery = array_filter($delivery);
if (!empty($delivery['delete'])) {
unset($data['marketplaces'][$key]['deliveries'][$dKey]);
continue;
}
if ($dKey <= 0) {
if (!empty($delivery['id_delivery'])) {
$data['marketplaces'][$key]['deliveries'][] = $delivery;
}
unset($data['marketplaces'][$key]['deliveries'][$dKey]);
}
}
}
$data['marketplaces'] = array_values($data['marketplaces'] ?? []);
return $data;
}
}

View File

@@ -0,0 +1,441 @@
<?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;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace KupShop\DropshipBundle\Transfer;
use KupShop\DropshipBundle\TransferInterface;
use Query\Operator;
class HeurekaTransfer extends AbstractTransfer implements TransferInterface
{
protected static string $type = 'heureka';
protected static string $name = 'Heureka';
protected function getExternalData(\SimpleXMLElement $xml): array
{
return [];
}
protected function getDeliveryTypeByConfiguration(\SimpleXMLElement $order): ?\DeliveryType
{
return null;
}
public function in(array $config): void
{
}
public function out(array $config): void
{
}
public function getConfigurationVariables(): array
{
$vars['deliveryTypes'] = sqlQueryBuilder()->select('id,name')
->from('delivery_type_delivery')
->andWhere(Operator::equals(['class' => 'OsobniOdber']))
->execute()->fetchAllKeyValue();
return $vars;
}
}

View File

@@ -0,0 +1,465 @@
<?php
namespace KupShop\DropshipBundle\Transfer;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\DropshipBundle\TransferInterface;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Query\Operator;
class MallTransfer extends AbstractTransfer implements TransferInterface
{
use \DatabaseCommunication;
protected static string $type = 'mall';
protected static string $name = 'MALL';
public const URL_API = 'https://mpapi.mallgroup.com/v1/orders/';
public function in(array $config): void
{
if (!($xml = $this->loadXML())) {
return;
}
$countryContext = Contexts::get(CountryContext::class);
$countries = $countryContext->getAll();
$defaultCurrency = Contexts::get(CurrencyContext::class)->getDefaultId();
foreach ($xml->ORDER as $order) {
if (!$this->isDropshipOrderValidToImport($order)) {
continue;
}
$deliveryType = $this->getDeliveryType($order);
$delivery_type = $order->DELIVERY_METHOD.' - '.$order->PAYMENT_TYPE;
$id_delivery = null;
if ($deliveryType) {
$delivery_type = $deliveryType->name;
$id_delivery = $deliveryType->id;
}
$deliveryPrice = $order->DELIVERY_PRICE + $order->COD_PRICE;
$customer = $order->ADDRESS;
$country = (string) $customer->COUNTRY;
if (!($countries[$country] ?? false)) {
$countries = array_keys($countries);
$country = reset($countries);
}
$customer_name = explode(' ', (string) $customer->NAME);
$name = array_shift($customer_name);
$surname = implode(' ', $customer_name);
$data = [
'date_created' => (new \DateTime())->format('Y-m-d H:i:s'),
'status' => 0,
'id_delivery' => $id_delivery,
'delivery_type' => $delivery_type,
'flags' => 'DSM',
'note_invoice' => (string) $order->ID,
'note_admin' => json_encode([
'mall' => [
'order_id' => (string) $order->ID,
'cash_on_delivery' => (string) $order->COD,
'delivery_method_id' => (string) $order->DELIVERY_METHOD_ID,
],
]),
'source' => OrderInfo::ORDER_SOURCE_DROPSHIP,
'invoice_name' => $name ?? '',
'invoice_surname' => $surname ?? '',
'invoice_email' => (string) $customer->EMAIL,
'invoice_phone' => (string) $customer->PHONE,
'invoice_street' => (string) $customer->STREET,
'invoice_city' => (string) $customer->CITY,
'invoice_zip' => (string) $customer->ZIP,
'invoice_country' => $country,
'delivery_name' => $name ?? '',
'delivery_surname' => $surname ?? '',
'delivery_street' => (string) $customer->STREET,
'delivery_city' => (string) $customer->CITY,
'delivery_zip' => (string) $customer->ZIP,
'delivery_country' => $country,
'currency' => $defaultCurrency,
];
$orderObj = sqlGetConnection()->transactional(function () use ($order, $data, $deliveryPrice) {
$orderObj = $this->createOrder($order, $data);
// zaloguju dalsi informace o objednavce
$orderObj->logHistory(implode('<br>', [
'Číslo Mall objednávky: '.(string) $order->ID,
'ID výdejního místa: '.(string) $order->DELIVERY_METHOD_ID,
'SHIP_DATE: '.(string) $order->SHIP_DATE,
]));
$orderID = $orderObj->id;
// prepare order items
$items = $this->prepareItems($order->ITEMS);
// add delivery item
$items[] = $this->getDeliveryItem($deliveryPrice);
if ($order->DISCOUNT > 0) {
// add discount item
$items[] = $this->getDiscountItem(-1 * $order->DISCOUNT);
}
// insert order items
foreach ($items as $item) {
if ($item['id_product']) {
$product = new \Product();
$product->createFromDB($item['id_product']);
$product->sell($item['id_variation'], toDecimal($item['pieces'])->asInteger());
}
$item['id_order'] = $orderID;
$this->insertSQL('order_items', $item);
$item['id'] = sqlInsertId();
$this->itemCreatedEvent(
product: $product ?? null,
idVariation: (int) $item['id_variation'],
piecePrice: toDecimal($item['piece_price']),
pieces: toDecimal($item['pieces'])->asInteger(),
data: [
'row' => $item,
'items_table' => 'order_items',
],
order: $orderObj
);
unset($product);
}
$orderObj->recalculate(round: false);
return $orderObj;
});
$this->modifyInsertedOrder($orderObj, $order);
}
}
protected function getDeliveryItem($deliveryPrice): array
{
$deliveryPrice = toDecimal($deliveryPrice);
$deliveryPrice = $deliveryPrice->removeVat(getVat());
return [
'id_product' => null,
'id_variation' => null,
'pieces' => 1,
'pieces_reserved' => 1,
'piece_price' => $deliveryPrice,
'total_price' => $deliveryPrice,
'descr' => 'Doprava a platba',
'tax' => getVat(),
'note' => '{"item_type":"delivery"}',
];
}
protected function getDiscountItem($discountPrice): array
{
$discountPrice = toDecimal($discountPrice);
$discountPrice = $discountPrice->removeVat(getVat());
return [
'id_product' => null,
'id_variation' => null,
'pieces' => 1,
'pieces_reserved' => 1,
'piece_price' => $discountPrice,
'total_price' => $discountPrice,
'descr' => 'Celková sleva',
'tax' => getVat(),
'note' => '{"item_type":"discount"}',
];
}
protected function getExternalData(\SimpleXMLElement $xml): array
{
return [
(string) $xml->ID,
[
'cash_on_delivery' => (string) $xml->COD,
'delivery_method_id' => (string) $xml->DELIVERY_METHOD_ID,
],
];
}
public function out(array $config): void
{
if (isDevelopment()) {
return;
}
if (!($clientId = $config['api_key'] ?? null)) {
$this->addActivityLog(
'V nastavení chybí API klíč',
$config
);
return;
}
$this->sendCancelledOrders($clientId);
$this->sendShippedOrders($clientId);
}
protected function sendCancelledOrders($clientId)
{
$qb = sqlQueryBuilder()->select('o.id')->from('orders', 'o')
->where(Operator::isNotNull(JsonOperator::value('o.note_admin', 'mall.order_id')))
->andWhere('o.status_storno = 1')
->andWhere(Operator::isNull(JsonOperator::value('o.note_admin', 'mall.cancelledSent')))
->groupBy('o.id');
foreach ($qb->execute() as $item) {
$order = new \Order();
$order->createFromDB($item['id']);
$mallData = $order->getData('mall');
$params = [
'confirmed' => true,
'status' => 'cancelled',
];
if ($this->sendOrderUpdate($clientId, $mallData['order_id'], $params)) {
$mallData['cancelledSent'] = true;
$order->setData('mall', $mallData);
$order->logHistory('Objednávka v MALL byla aktualizována na status "cancelled"');
}
}
}
protected function sendShippedOrders($clientId)
{
$qb = sqlQueryBuilder()->select('o.id')->from('orders', 'o')
->where(Operator::isNotNull(JsonOperator::value('o.note_admin', 'mall.order_id')))
->andWhere('o.status_storno = 0 AND o.package_id IS NOT NULL')
->andWhere(Operator::inIntArray($this->getShippedStatuses(), 'o.status'))
->andWhere(Operator::isNull(JsonOperator::value('o.note_admin', 'mall.shippedSent')))
->groupBy('o.id');
foreach ($qb->execute() as $item) {
$order = new \Order();
$order->createFromDB($item['id']);
$mallData = $order->getData('mall');
$packages = $this->orderInfo->getPackages($order);
$package = $packages[$order->package_id] ?? end($packages);
if (!$package) {
continue;
}
$params = [
'confirmed' => true,
'status' => 'shipped',
'tracking_number' => $package['package_id'],
'tracking_url' => $package['track_url'] ?? '',
];
if ($this->sendOrderUpdate($clientId, $mallData['order_id'], $params)) {
$mallData['shippedSent'] = true;
$order->setData('mall', $mallData);
$order->logHistory('Objednávka v MALL byla aktualizována na status "shipped"');
}
}
}
protected function getShippedStatuses(): ?array
{
return getStatuses('handled');
}
protected function sendOrderUpdate(string $clientId, $mall_order_id, array $params): bool
{
$ch = curl_init();
$url = self::URL_API.$mall_order_id.'?client_id='.$clientId;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
$result = curl_exec($ch);
curl_close($ch);
$result = json_decode($result, true);
if ('OK' != $result['result']['status'] ?? null) {
$message = $result['result']['message'] ?? '';
$data = [
'mall_order_id' => $mall_order_id,
'params' => $params,
'result' => $result['result'] ?? null,
];
addActivityLog(ActivityLog::SEVERITY_ERROR, ActivityLog::TYPE_SYNC,
sprintf('Dropshipment MALL: chyba při odeslání aktualizace stavů objednávky "%s"', $message), $data);
if (str_starts_with($message, 'Final status') && str_ends_with($message, 'cannot be changed')) {
return true;
}
return false;
}
return true;
}
protected function isDropshipOrderValidToImport(\SimpleXMLElement $xml): bool
{
return parent::isDropshipOrderValidToImport($xml) && $this->isValidToImport($xml);
}
protected function isValidToImport(\SimpleXMLElement $order): bool
{
$found = sqlQueryBuilder()
->select('id')
->from('orders')
->where(
Operator::equals(
[
JsonOperator::value('note_admin', 'mall.order_id') => (string) $order->ID,
]
)
)->execute()->fetchColumn();
// objednavka uz je vytvorena
if ($found) {
return false;
}
return true;
}
protected function createOrder(\SimpleXMLElement $order, array $data): \Order
{
return $this->createDropshipOrder($order, $data);
}
protected function getDeliveryType(\SimpleXMLElement $orderItem): ?\DeliveryType
{
return $this->getDeliveryTypeByConfiguration($orderItem);
}
private function prepareItems(\SimpleXMLElement $items): array
{
$result = [];
foreach ($items as $item) {
$itemCode = (string) $item->ID;
$parsed = explode('_', $itemCode);
$productID = $parsed[0] ?? $itemCode;
$variationID = $parsed[1] ?? null;
// check that productID exists
if (!$this->selectSQL('products', ['id' => $productID], ['id'])->fetch()) {
$productID = null;
}
// check that variationID exists
if ($variationID && !$this->selectSQL('products_variations', ['id' => $variationID], ['id'])->fetch()) {
$variationID = null;
}
// create name of item
$descr = 'Položka kód: '.$itemCode;
if ($productID) {
$title = $this->selectSQL('products', ['id' => $productID], ['title'])->fetchColumn();
if (!empty($title)) {
$descr = $title;
}
if ($variationID) {
$title = $this->selectSQL('products_variations', ['id' => $variationID], ['title'])->fetchColumn();
if (!empty($title)) {
$descr .= ' ('.$title.')';
}
}
}
$price = toDecimal((string) $item->PRICE);
$vat = (string) $item->VAT;
$pieces = toDecimal((string) $item->QUANTITY);
$price = $price->removeVat($vat);
$itemTotalPrice = $price->mul($pieces);
$result[] = $this->modifyItem([
'id_product' => $productID,
'id_variation' => $variationID,
'pieces' => $pieces,
'pieces_reserved' => (string) $item->QUANTITY,
'piece_price' => $price,
'total_price' => $itemTotalPrice,
'tax' => (string) $item->VAT,
'descr' => $descr,
'note' => json_encode(['item_type' => OrderItemInfo::TYPE_PRODUCT]),
], $item);
}
return $result;
}
protected function getDeliveryTypeByConfiguration(\SimpleXMLElement $order): ?\DeliveryType
{
$deliveryMethod = (string) $order->DELIVERY_METHOD;
$country = (string) $order->ADDRESS->COUNTRY;
$cod = (string) $order->COD;
$config = $this->getConfiguration();
$deliveryId = null;
foreach ($config['deliveries'] ?? [] as $item) {
if ($deliveryMethod == $item['id_external'] && (empty($item['country']) || $item['country'] == $country)) {
$deliveryId = (int) $item['id_delivery'];
break;
}
}
if ($cod > 0) {
$paymentId = empty($config['payments']['cod']) ? null : (int) $config['payments']['cod'];
} else {
$paymentId = empty($config['payments']['paid']) ? null : (int) $config['payments']['paid'];
}
return $this->findDeliveryType($deliveryId, $paymentId);
}
public function prepareConfigurationData(array $data): array
{
foreach ($data['deliveries'] ?? [] as $key => $item) {
$item = array_filter($item);
if (!empty($item['delete'])) {
unset($data['deliveries'][$key]);
continue;
}
if ($key <= 0) {
if (!empty($item['id_external']) && !empty($item['id_delivery'])) {
$data['deliveries'][] = $item;
}
unset($data['deliveries'][$key]);
}
}
return $data;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace KupShop\DropshipBundle;
interface TransferInterface
{
public static function getType(): string;
public static function getName(): string;
public function isRunnable(): bool;
public function setup(array $dropshipment): void;
public function process(): void;
public function in(array $config): void;
public function out(array $config): void;
public function prepareConfigurationData(array $data): array;
public function getConfigurationVariables(): array;
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Util;
use Query\Operator;
class DropshipmentUtil
{
private ?array $dropshipmentsCache = null;
public function __construct(private TransferLocator $transferLocator)
{
}
public function getDropshipmentTypes(): array
{
return $this->transferLocator->getTypes();
}
public function getDropshipment(int $id, bool $force = false): ?array
{
return $this->getDropshipments($force ? Operator::andX('1 = 1') : null)[$id] ?? null;
}
public function getDropshipments(?callable $spec = null, ?int &$totalCount = null): array
{
if ($spec === null && $this->dropshipmentsCache !== null) {
return $this->dropshipmentsCache;
}
$useTotalCount = count(func_get_args()) > 1;
$qb = sqlQueryBuilder()
->select('*')
->from('dropshipment');
if ($spec !== null) {
$qb->andWhere($spec);
}
if ($useTotalCount) {
$qb->addCalcRows();
}
$result = [];
foreach ($qb->execute() as $item) {
$item['configuration'] = json_decode($item['configuration'] ?: '', true) ?: [];
$item['data'] = json_decode($item['data'] ?: '', true) ?: [];
$result[$item['id']] = $item;
}
if ($useTotalCount) {
$totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
}
return $this->dropshipmentsCache = $result;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace KupShop\DropshipBundle\Util;
use KupShop\DropshipBundle\Exception\TransferNotFoundException;
use KupShop\DropshipBundle\TransferInterface;
class TransferLocator
{
private $transfers;
public function __construct(iterable $transfers)
{
$this->transfers = $transfers;
}
public function getTypes(): array
{
return array_keys($this->getTransfers());
}
/**
* @return TransferInterface[]
*/
public function getTransfers(): array
{
$result = [];
foreach ($this->transfers as $transfer) {
$result[$transfer::getType()] = $transfer;
}
return $result;
}
public function getTransfer(string $type): TransferInterface
{
if ($transfer = $this->getTransfers()[$type] ?? null) {
return $transfer;
}
throw new TransferNotFoundException($type);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace KupShop\DropshipBundle\Util;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\DropshipBundle\Exception\TransferException;
use KupShop\DropshipBundle\TransferInterface;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
use Query\Operator;
class TransferWorker
{
protected TransferLocator $transferLocator;
protected DropshipmentUtil $dropshipmentUtil;
private SentryLogger $sentryLogger;
public function __construct(DropshipmentUtil $dropshipmentUtil, TransferLocator $transferLocator, SentryLogger $sentryLogger)
{
$this->dropshipmentUtil = $dropshipmentUtil;
$this->transferLocator = $transferLocator;
$this->sentryLogger = $sentryLogger;
}
public function run(?int $id = null): void
{
$dropshipments = $id ? [$this->dropshipmentUtil->getDropshipment($id)] : $this->dropshipmentUtil->getDropshipments();
foreach ($dropshipments as $dropshipment) {
$transfer = $this->transferLocator->getTransfer($dropshipment['type']);
$transfer->setup($dropshipment);
// preskocim dropshipmenty, ktere se nemaji spoustet
if (!$transfer->isRunnable()) {
continue;
}
try {
$newLastSyncTime = date('Y-m-d H:i:s');
$transfer->process();
$this->updateLastSyncTime($newLastSyncTime, $transfer);
} catch (\Throwable $e) {
$this->logException($e, $transfer);
}
}
}
public function logException(\Throwable $e, TransferInterface $transfer): void
{
if (isLocalDevelopment()) {
throw $e;
}
$message = sprintf('[Dropshipment] Chyba během zpracování: %s (ID: %s)', $transfer->dropshipment['name'], $transfer->dropshipment['id']);
$data = ['error' => $e->getMessage()];
if (!($e instanceof TransferException)) {
$this->sentryLogger->captureException($e);
} else {
$message = $e->getMessage();
$data = array_merge($data, $e->getData());
}
addActivityLog(
ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_IMPORT,
$message,
$data
);
}
protected function updateLastSyncTime(string $lastSyncTime, TransferInterface $transfer): void
{
sqlQueryBuilder()
->update('dropshipment')
->directValues(['last_sync' => $lastSyncTime])
->where(Operator::equals(['id' => $transfer->dropshipment['id']]))
->execute();
}
}