From cd385783c5ef6b5bfd5e16e43c9d37190cd4e342 Mon Sep 17 00:00:00 2001 From: M2E Pro Date: Thu, 26 Dec 2024 15:07:58 +0000 Subject: [PATCH] 1.74.0 (FINAL RELEASE) --- .../Walmart/Listing/Product/Add/Index.php | 20 +++- Helper/Component/Amazon.php | 2 + Model/Amazon/Order.php | 35 +++++-- Model/Amazon/Order/Creditmemo/Handler.php | 8 -- .../Base/BaseInventoryTracker.php | 28 +++++- Model/ChangeTracker/Base/BasePriceTracker.php | 41 +++++++- Model/ChangeTracker/Base/ChangeHolder.php | 39 ++------ .../Base/ChangeHolderFactory.php | 20 ++++ Model/ChangeTracker/Base/InventoryStock.php | 27 +++--- .../Base/InventoryTrackerFactory.php | 33 +++---- .../Base/PriceTrackerFactory.php | 30 +++--- .../Base/TrackerFactoryInterface.php | 13 +-- Model/ChangeTracker/Base/TrackerInterface.php | 13 +-- .../ChangeTracker/ChangeTrackerProcessor.php | 86 +++++++++++++++++ .../Common/Helpers/TrackerLogger.php | 16 ++++ Model/ChangeTracker/PartManager.php | 84 ++++++++++++++++ Model/ChangeTracker/TrackerConfiguration.php | 19 ++++ .../Task/Listing/Product/ChangeTracker.php | 73 ++------------ Model/Cron/Task/Repository.php | 1 + Model/Ebay/AdvancedFilter/AllItemsOptions.php | 3 +- .../Checker/Active.php | 2 +- Model/Setup/Installer.php | 23 +++++ Model/Setup/Upgrader.php | 1 + .../Action/Type/ListAction/Validator.php | 14 +++ .../Product/Action/Type/Relist/Validator.php | 4 - .../Listing/Product/Action/Type/Validator.php | 16 ---- .../Checker/Active.php | 20 ---- .../Checker/Inactive.php | 9 -- .../PreconditionsChecker/AbstractModel.php | 1 + Setup/Update/Config.php | 3 +- .../y24_m12/AddAmazonMarketplaceIreland.php | 96 +++++++++++++++++++ Setup/Upgrade/v1_73_0__v1_74_0/Config.php | 15 +++ composer.json | 2 +- etc/module.xml | 2 +- 34 files changed, 553 insertions(+), 246 deletions(-) create mode 100644 Model/ChangeTracker/Base/ChangeHolderFactory.php create mode 100644 Model/ChangeTracker/ChangeTrackerProcessor.php create mode 100644 Model/ChangeTracker/PartManager.php create mode 100644 Model/ChangeTracker/TrackerConfiguration.php create mode 100644 Setup/Update/y24_m12/AddAmazonMarketplaceIreland.php create mode 100644 Setup/Upgrade/v1_73_0__v1_74_0/Config.php diff --git a/Controller/Adminhtml/Walmart/Listing/Product/Add/Index.php b/Controller/Adminhtml/Walmart/Listing/Product/Add/Index.php index 28c72344e..ee48d38dc 100644 --- a/Controller/Adminhtml/Walmart/Listing/Product/Add/Index.php +++ b/Controller/Adminhtml/Walmart/Listing/Product/Add/Index.php @@ -257,8 +257,9 @@ public function stepOneSourceCategories() private function processStep3(\Ess\M2ePro\Model\Marketplace $marketplace): void { if ( - !$marketplace->getChildObject() - ->isSupportedProductType() + $this->isMovedFromOther($this->getListing()) + || !$marketplace->getChildObject() + ->isSupportedProductType() ) { $this->review($marketplace); @@ -313,12 +314,16 @@ private function review(\Ess\M2ePro\Model\Marketplace $marketplace): void return; } - if ($marketplace->getChildObject()->isSupportedProductType()) { + $isMovedFromOther = $this->isMovedFromOther($this->getListing()); + if ( + !$isMovedFromOther + && $marketplace->getChildObject()->isSupportedProductType() + ) { $this->removeProductsWithoutProductTypes($additionalData['adding_listing_products_ids']); } //-- Remove successfully moved Unmanaged items - if (isset($additionalData['source']) && $additionalData['source'] == SourceModeBlock::MODE_OTHER) { + if ($isMovedFromOther) { $this->deleteListingOthers(); } //-- @@ -349,6 +354,13 @@ private function review(\Ess\M2ePro\Model\Marketplace $marketplace): void $this->addContent($blockReview); } + private function isMovedFromOther(\Ess\M2ePro\Model\Listing $listing): bool + { + $source = $listing->getSettings('additional_data')['source'] ?? null; + + return $source === SourceModeBlock::MODE_OTHER; + } + private function removeProductsWithoutProductTypes(array $addingListingProductsIds): void { /** @var \Ess\M2ePro\Model\ResourceModel\Listing\Product\Collection $collection */ diff --git a/Helper/Component/Amazon.php b/Helper/Component/Amazon.php index 297a33d07..b0b85e0c0 100644 --- a/Helper/Component/Amazon.php +++ b/Helper/Component/Amazon.php @@ -32,8 +32,10 @@ class Amazon public const MARKETPLACE_BE = 48; public const MARKETPLACE_ZA = 49; public const MARKETPLACE_SA = 50; + public const MARKETPLACE_IE = 51; public const NATIVE_ID_MARKETPLACE_SA = 22; + public const NATIVE_ID_MARKETPLACE_IE = 23; public const EEA_COUNTRY_CODES = [ 'AT', 'BE', 'BG', 'HR', 'CY', diff --git a/Model/Amazon/Order.php b/Model/Amazon/Order.php index 015277ae1..4032056ec 100644 --- a/Model/Amazon/Order.php +++ b/Model/Amazon/Order.php @@ -937,13 +937,15 @@ public function refund(array $items = [], Creditmemo $creditMemo = null) 'items' => $items ]; - $orderId = $this->getParentObject()->getId(); + $isRefundAction = $this->isShipped() + || $this->isPartiallyShipped() + || $this->getParentObject()->isOrderStatusUpdatingToShipped(); - $action = \Ess\M2ePro\Model\Order\Change::ACTION_CANCEL; - if ( - $this->isShipped() || $this->isPartiallyShipped() || - $this->getParentObject()->isOrderStatusUpdatingToShipped() - ) { + $action = $isRefundAction + ? \Ess\M2ePro\Model\Order\Change::ACTION_REFUND + : \Ess\M2ePro\Model\Order\Change::ACTION_CANCEL; + + if ($action == \Ess\M2ePro\Model\Order\Change::ACTION_REFUND) { if (empty($items)) { $this->getParentObject()->addErrorLog( 'Amazon Order was not refunded. Reason: %msg%', @@ -958,16 +960,31 @@ public function refund(array $items = [], Creditmemo $creditMemo = null) return false; } - $action = \Ess\M2ePro\Model\Order\Change::ACTION_REFUND; + if ( + empty($adjustmentFee) + && empty($adjustmentRefund) + && empty($shippingRefund) + && empty(array_sum(array_column($items, 'cancelled_qty'))) + ) { + $this->getParentObject()->addErrorLog( + 'Amazon order cannot be refunded: The Credit Memo does not specify any refund amount. ' . + 'Please ensure the Credit Memo includes a refund cost to process the refund.', + ); + + return false; + } } - if ($action == \Ess\M2ePro\Model\Order\Change::ACTION_CANCEL && $this->isCancellationRequested()) { + if ( + $action == \Ess\M2ePro\Model\Order\Change::ACTION_CANCEL + && $this->isCancellationRequested() + ) { $params['cancel_reason'] = \Ess\M2ePro\Model\Amazon\Order\Creditmemo\Handler::AMAZON_REFUND_REASON_BUYER_CANCELED; } $this->activeRecordFactory->getObject('Order\Change')->create( - $orderId, + $this->getParentObject()->getId(), $action, $this->getParentObject()->getLog()->getInitiator(), \Ess\M2ePro\Helper\Component\Amazon::NICK, diff --git a/Model/Amazon/Order/Creditmemo/Handler.php b/Model/Amazon/Order/Creditmemo/Handler.php index 3f85c844e..8fa32bb3b 100644 --- a/Model/Amazon/Order/Creditmemo/Handler.php +++ b/Model/Amazon/Order/Creditmemo/Handler.php @@ -45,14 +45,6 @@ protected function getItemsToRefund( $price = $creditmemoItem->getPrice() ?? 0.0; $tax = $creditmemoItem->getTaxAmount() ?? 0.0; - if ($price > $item->getChildObject()->getPrice()) { - $price = $item->getChildObject()->getPrice(); - } - - if ($tax > $item->getChildObject()->getTaxAmount()) { - $tax = $item->getChildObject()->getTaxAmount(); - } - $cancelledItemQty = $cancelledQty; if ($cancelledItemQty > $item->getChildObject()->getQtyPurchased()) { $cancelledItemQty = $item->getChildObject()->getQtyPurchased(); diff --git a/Model/ChangeTracker/Base/BaseInventoryTracker.php b/Model/ChangeTracker/Base/BaseInventoryTracker.php index bd4c1bc56..668db7a93 100644 --- a/Model/ChangeTracker/Base/BaseInventoryTracker.php +++ b/Model/ChangeTracker/Base/BaseInventoryTracker.php @@ -21,6 +21,8 @@ abstract class BaseInventoryTracker implements TrackerInterface private $attributesQueryBuilder; /** @var \Ess\M2ePro\Model\ChangeTracker\Common\Helpers\EnterpriseChecker */ private $enterpriseChecker; + private int $listingProductIdFrom; + private int $listingProductIdTo; public function __construct( string $channel, @@ -28,7 +30,9 @@ public function __construct( InventoryStock $inventoryStock, ProductAttributesQueryBuilder $attributesQueryBuilder, TrackerLogger $logger, - \Ess\M2ePro\Model\ChangeTracker\Common\Helpers\EnterpriseChecker $enterpriseChecker + \Ess\M2ePro\Model\ChangeTracker\Common\Helpers\EnterpriseChecker $enterpriseChecker, + int $listingProductIdFrom, + int $listingProductIdTo ) { $this->channel = $channel; $this->inventoryStock = $inventoryStock; @@ -36,6 +40,8 @@ public function __construct( $this->logger = $logger; $this->attributesQueryBuilder = $attributesQueryBuilder; $this->enterpriseChecker = $enterpriseChecker; + $this->listingProductIdFrom = $listingProductIdFrom; + $this->listingProductIdTo = $listingProductIdTo; } /** @@ -54,6 +60,16 @@ public function getChannel(): string return $this->channel; } + public function getListingProductIdFrom(): int + { + return $this->listingProductIdFrom; + } + + public function getListingProductIdTo(): int + { + return $this->listingProductIdTo; + } + /** * @return \Magento\Framework\DB\Select * @throws \Exception @@ -145,6 +161,7 @@ public function getDataQuery(): \Magento\Framework\DB\Select 'query' => (string)$mainQuery->getQuery(), 'type' => $this->getType(), 'channel' => $this->getChannel(), + 'tracker' => $this ]); return $mainQuery->getQuery(); @@ -216,6 +233,13 @@ protected function productSubQuery(): SelectQueryBuilder 'lpvo', 'm2epro_listing_product_variation_option', 'lpvo.listing_product_variation_id = lpv.id' + ) + ->andWhere( + sprintf( + 'lp.id >= %s AND lp.id <= %s', + $this->getListingProductIdFrom(), + $this->getListingProductIdTo() + ) ); /* We do not include grouped and bundle products in the selection */ @@ -367,7 +391,7 @@ protected function getSelectQuery(): SelectQueryBuilder ->from('product', $this->productSubQuery()) ->innerJoin( 'stock', - $this->inventoryStock->getInventoryStockTableName(), + $this->inventoryStock->getInventoryStockTableName($this), 'product.product_id = stock.product_id' ) ->leftJoin( diff --git a/Model/ChangeTracker/Base/BasePriceTracker.php b/Model/ChangeTracker/Base/BasePriceTracker.php index cf39ccacf..56b9c7e2b 100644 --- a/Model/ChangeTracker/Base/BasePriceTracker.php +++ b/Model/ChangeTracker/Base/BasePriceTracker.php @@ -20,19 +20,25 @@ abstract class BasePriceTracker implements TrackerInterface protected $attributesQueryBuilder; /** @var \Ess\M2ePro\Model\ChangeTracker\Common\PriceCondition\AbstractPriceCondition */ private $priceConditionBuilder; + private int $listingProductIdFrom; + private int $listingProductIdTo; public function __construct( string $channel, QueryBuilderFactory $queryBuilderFactory, ProductAttributesQueryBuilder $attributesQueryBuilder, PriceConditionFactory $conditionFactory, - TrackerLogger $logger + TrackerLogger $logger, + int $listingProductIdFrom, + int $listingProductIdTo ) { $this->channel = $channel; $this->queryBuilder = $queryBuilderFactory->make(); $this->attributesQueryBuilder = $attributesQueryBuilder; $this->priceConditionBuilder = $conditionFactory->create($channel); $this->logger = $logger; + $this->listingProductIdFrom = $listingProductIdFrom; + $this->listingProductIdTo = $listingProductIdTo; } /** @@ -51,6 +57,16 @@ public function getChannel(): string return $this->channel; } + public function getListingProductIdFrom(): int + { + return $this->listingProductIdFrom; + } + + public function getListingProductIdTo(): int + { + return $this->listingProductIdTo; + } + /** * Condition for select, in which we calculate the online price of the product. * @return string @@ -174,6 +190,12 @@ protected function productSubQuery(): SelectQueryBuilder 'marketplace', $this->setChannelToTableName('m2epro_%s_marketplace'), 'marketplace.marketplace_id = l.marketplace_id' + )->andWhere( + sprintf( + 'lp.id >= %s AND lp.id <= %s', + $this->getListingProductIdFrom(), + $this->getListingProductIdTo() + ) ); /* We do not include grouped and bundle products */ @@ -222,7 +244,7 @@ protected function getSelectQuery(): SelectQueryBuilder /* Required selects */ $query->addSelect('listing_product_id', 'product.listing_product_id'); - $query->addSelect('calculated_price', $this->priceConditionBuilder->getCondition()); + $query->addSelect('calculated_price', $this->priceConditionBuilder->getCondition($this)); $query->addSelect('product_id', 'product.product_id') ->addSelect('status', 'product.status') @@ -346,11 +368,24 @@ protected function getCurrencyRateSubQuery(): \Zend_Db_Expr ); $select->andWhere('listing.component_mode = ?', $this->getChannel()); + $select->leftJoin( + 'listing_product', + 'm2epro_listing_product', + 'listing.id = listing_product.listing_id' + )->andWhere( + sprintf( + 'listing_product.id >= %s AND listing_product.id <= %s', + $this->getListingProductIdFrom(), + $this->getListingProductIdTo() + ) + ); + $ratesByStores = $select->fetchAll(); - $this->logger->info('Get Currency Rates', [ + $this->logger->debug('Get Currency Rates', [ 'sql' => $select->getQuery()->__tostring(), 'result' => $ratesByStores, + 'tracker' => $this, ]); if ($ratesByStores === []) { diff --git a/Model/ChangeTracker/Base/ChangeHolder.php b/Model/ChangeTracker/Base/ChangeHolder.php index 293616393..bac371de8 100644 --- a/Model/ChangeTracker/Base/ChangeHolder.php +++ b/Model/ChangeTracker/Base/ChangeHolder.php @@ -42,7 +42,7 @@ public function __construct( */ public function holdChanges(TrackerInterface $tracker): void { - $this->logger->info(sprintf("%s Start collect changes", $this->logTags($tracker))); + $this->logger->info('Start collect changes', ['tracker' => $tracker]); // Prepare SQL query $this->profiler->start(); @@ -53,11 +53,8 @@ public function holdChanges(TrackerInterface $tracker): void } $this->profiler->stop(); $this->logger->info( - sprintf( - '%s Prepare SQL query time - %s sec.', - $this->logTags($tracker), - $this->profiler->getTime() - ) + sprintf('Prepare SQL query time - %s sec.', $this->profiler->getTime()), + ['tracker' => $tracker] ); // Execute SQL query @@ -70,11 +67,8 @@ public function holdChanges(TrackerInterface $tracker): void } $this->profiler->stop(); $this->logger->info( - sprintf( - '%s Execute SQL query time - %s sec.', - $this->logTags($tracker), - $this->profiler->getTime() - ) + sprintf('Execute SQL query time - %s sec.', $this->profiler->getTime()), + ['tracker' => $tracker] ); // Insert instruction @@ -90,19 +84,13 @@ public function holdChanges(TrackerInterface $tracker): void } $this->profiler->stop(); $this->logger->info( - sprintf( - '%s Insert instructions time - %s sec.', - $this->logTags($tracker), - $this->profiler->getTime() - ) + sprintf('Insert instructions time - %s sec.', $this->profiler->getTime()), + ['tracker' => $tracker] ); $this->logger->info( - sprintf( - '%s Added instructions: %s', - $this->logTags($tracker), - $instructionCounter - ) + sprintf('Added instructions: %s', $instructionCounter), + ['tracker' => $tracker] ); } @@ -166,13 +154,4 @@ private function processException(\Throwable $exception): void throw $exception; } - - private function logTags(\Ess\M2ePro\Model\ChangeTracker\Base\TrackerInterface $tracker): string - { - return sprintf( - '%s >> %s >>', - strtoupper($tracker->getChannel()), - strtoupper($tracker->getType()) - ); - } } diff --git a/Model/ChangeTracker/Base/ChangeHolderFactory.php b/Model/ChangeTracker/Base/ChangeHolderFactory.php new file mode 100644 index 000000000..e8f6a94bd --- /dev/null +++ b/Model/ChangeTracker/Base/ChangeHolderFactory.php @@ -0,0 +1,20 @@ +objectManager = $objectManager; + } + + public function create(): ChangeHolder + { + return $this->objectManager->create(ChangeHolder::class); + } +} diff --git a/Model/ChangeTracker/Base/InventoryStock.php b/Model/ChangeTracker/Base/InventoryStock.php index e1b7edd63..800d303c9 100644 --- a/Model/ChangeTracker/Base/InventoryStock.php +++ b/Model/ChangeTracker/Base/InventoryStock.php @@ -3,6 +3,7 @@ namespace Ess\M2ePro\Model\ChangeTracker\Base; use Ess\M2ePro\Model\ChangeTracker\Common\QueryBuilder\QueryBuilderFactory; +use Ess\M2ePro\Model\ChangeTracker\Common\QueryBuilder\SelectQueryBuilder; use Ess\M2ePro\Model\ChangeTracker\Common\TemporaryTable; use Magento\Framework\App\ResourceConnection; @@ -38,22 +39,16 @@ public function __construct( $this->logger = $logger; } - /** - * @return string - */ - public function getInventoryStockTableName(): string + public function getInventoryStockTableName(TrackerInterface $tracker): string { if ($this->magentoHelper->isMSISupportingVersion()) { - return $this->getMsiInventoryTableName(); + return $this->getMsiInventoryTableName($tracker); } return 'cataloginventory_stock_item'; } - /** - * @return \Ess\M2ePro\Model\ChangeTracker\Common\QueryBuilder\SelectQueryBuilder - */ - public function getInventoryQuery(): \Ess\M2ePro\Model\ChangeTracker\Common\QueryBuilder\SelectQueryBuilder + public function getInventoryQuery(TrackerInterface $tracker): SelectQueryBuilder { $stockSubQuery = $this->queryBuilder ->makeSubQuery() @@ -111,10 +106,7 @@ public function getInventoryQuery(): \Ess\M2ePro\Model\ChangeTracker\Common\Quer ); } - /** - * @return string - */ - private function getMsiInventoryTableName(): string + private function getMsiInventoryTableName(TrackerInterface $tracker): string { if ( $this->msiTableName !== '' @@ -123,9 +115,10 @@ private function getMsiInventoryTableName(): string return $this->msiTableName; } - $inventoryQuery = $this->getInventoryQuery(); + $inventoryQuery = $this->getInventoryQuery($tracker); $this->logger->debug('Stock temporary table', [ - 'query' => 'CREATE TEMPORARY TABLE IF NOT EXISTS `tmp_stock` ' . (string)$inventoryQuery->getQuery(), + 'query' => 'CREATE TEMPORARY TABLE IF NOT EXISTS `tmp_stock` ' . $inventoryQuery->getQuery(), + 'tracker' => $tracker ]); $start = microtime(true); @@ -134,7 +127,9 @@ private function getMsiInventoryTableName(): string $this->getDbAdapter() ); $createdTime = (float)number_format(microtime(true) - $start, 4); - $this->logger->debug("Stock temporary table created in $createdTime seconds"); + $this->logger->debug("Stock temporary table created in $createdTime seconds", [ + 'tracker' => $tracker + ]); return $this->msiTableName; } diff --git a/Model/ChangeTracker/Base/InventoryTrackerFactory.php b/Model/ChangeTracker/Base/InventoryTrackerFactory.php index 6ac028f11..d8aa27a4b 100644 --- a/Model/ChangeTracker/Base/InventoryTrackerFactory.php +++ b/Model/ChangeTracker/Base/InventoryTrackerFactory.php @@ -1,32 +1,23 @@ objectManager = $objectManager; } - /** - * @param string $channel - * - * @return \Ess\M2ePro\Model\ChangeTracker\Base\TrackerInterface - */ - public function create(string $channel): TrackerInterface + public function createByConfiguration(TrackerConfiguration $trackerConfiguration): TrackerInterface { - switch ($channel) { + switch ($trackerConfiguration->component) { case TrackerInterface::CHANNEL_EBAY: $class = \Ess\M2ePro\Model\ChangeTracker\Ebay\InventoryTracker::class; break; @@ -37,11 +28,13 @@ public function create(string $channel): TrackerInterface $class = \Ess\M2ePro\Model\ChangeTracker\Walmart\InventoryTracker::class; break; default: - throw new \RuntimeException('Unknown chanel ' . $channel); + throw new \RuntimeException('Unknown chanel ' . $trackerConfiguration->component); } return $this->objectManager->create($class, [ - 'channel' => $channel, + 'channel' => $trackerConfiguration->component, + 'listingProductIdFrom' => $trackerConfiguration->listingProductIdFrom, + 'listingProductIdTo' => $trackerConfiguration->listingProductIdTo, ]); } } diff --git a/Model/ChangeTracker/Base/PriceTrackerFactory.php b/Model/ChangeTracker/Base/PriceTrackerFactory.php index b877e0f5c..b2c140039 100644 --- a/Model/ChangeTracker/Base/PriceTrackerFactory.php +++ b/Model/ChangeTracker/Base/PriceTrackerFactory.php @@ -1,29 +1,23 @@ objectManager = $objectManager; } - /** - * @param string $channel - * - * @return \Ess\M2ePro\Model\ChangeTracker\Base\TrackerInterface - */ - public function create(string $channel): TrackerInterface + public function createByConfiguration(TrackerConfiguration $trackerConfiguration): TrackerInterface { - switch ($channel) { + switch ($trackerConfiguration->component) { case TrackerInterface::CHANNEL_EBAY: $class = \Ess\M2ePro\Model\ChangeTracker\Ebay\PriceTracker::class; break; @@ -34,11 +28,13 @@ public function create(string $channel): TrackerInterface $class = \Ess\M2ePro\Model\ChangeTracker\Walmart\PriceTracker::class; break; default: - throw new \RuntimeException('Unknown chanel ' . $channel); + throw new \RuntimeException('Unknown chanel ' . $trackerConfiguration->component); } return $this->objectManager->create($class, [ - 'channel' => $channel, + 'channel' => $trackerConfiguration->component, + 'listingProductIdFrom' => $trackerConfiguration->listingProductIdFrom, + 'listingProductIdTo' => $trackerConfiguration->listingProductIdTo, ]); } } diff --git a/Model/ChangeTracker/Base/TrackerFactoryInterface.php b/Model/ChangeTracker/Base/TrackerFactoryInterface.php index 36b1554cc..81bce09ff 100644 --- a/Model/ChangeTracker/Base/TrackerFactoryInterface.php +++ b/Model/ChangeTracker/Base/TrackerFactoryInterface.php @@ -2,16 +2,9 @@ namespace Ess\M2ePro\Model\ChangeTracker\Base; -/** - * Query factory interface - */ +use Ess\M2ePro\Model\ChangeTracker\TrackerConfiguration; + interface TrackerFactoryInterface { - /** - * @param string $channel - * - * @return \Ess\M2ePro\Model\ChangeTracker\Base\TrackerInterface - * @throws \RuntimeException - */ - public function create(string $channel): TrackerInterface; + public function createByConfiguration(TrackerConfiguration $trackerConfiguration): TrackerInterface; } diff --git a/Model/ChangeTracker/Base/TrackerInterface.php b/Model/ChangeTracker/Base/TrackerInterface.php index d676c0a34..2f07f162e 100644 --- a/Model/ChangeTracker/Base/TrackerInterface.php +++ b/Model/ChangeTracker/Base/TrackerInterface.php @@ -11,18 +11,13 @@ interface TrackerInterface public const TYPE_PRICE = "price"; public const TYPE_INVENTORY = "inventory"; - /** - * @return string - */ public function getType(): string; - /** - * @return string - */ public function getChannel(): string; - /** - * @return \Magento\Framework\DB\Select - */ + public function getListingProductIdFrom(): int; + + public function getListingProductIdTo(): int; + public function getDataQuery(): \Magento\Framework\DB\Select; } diff --git a/Model/ChangeTracker/ChangeTrackerProcessor.php b/Model/ChangeTracker/ChangeTrackerProcessor.php new file mode 100644 index 000000000..17e7eaab2 --- /dev/null +++ b/Model/ChangeTracker/ChangeTrackerProcessor.php @@ -0,0 +1,86 @@ +inventoryTrackerFactory = $inventoryTrackerFactory; + $this->priceTrackerFactory = $priceTrackerFactory; + $this->changeHolderFactory = $changeHolderFactory; + $this->partManager = $partManager; + $this->eventDispatcher = $eventDispatcher; + } + + public function process(): void + { + $totalPartsCount = $this->partManager->getPartsCount(); + $processedPartNumber = 0; + foreach ($this->partManager->getListingProductParts() as $part) { + $processedPartNumber++; + $this->processPart($part); + $this->dispatchKeepAliveEvent($totalPartsCount, $processedPartNumber); + } + } + + /** + * @param TrackerConfiguration[] $part + * @return void + * @throws \Throwable + */ + private function processPart(array $part): void + { + foreach ($part as $trackerConfiguration) { + $this->trackInventoryChanges($trackerConfiguration); + $this->trackPriceChanges($trackerConfiguration); + } + } + + private function trackInventoryChanges(TrackerConfiguration $trackerConfiguration): void + { + $changeHolder = $this->changeHolderFactory->create(); + $changeHolder->holdChanges( + $this->inventoryTrackerFactory->createByConfiguration( + $trackerConfiguration + ) + ); + } + + private function trackPriceChanges(TrackerConfiguration $trackerConfiguration): void + { + $changeHolder = $this->changeHolderFactory->create(); + $changeHolder->holdChanges( + $this->priceTrackerFactory->createByConfiguration( + $trackerConfiguration + ) + ); + } + + private function dispatchKeepAliveEvent(int $totalPartsCount, int $currentPartNumber): void + { + $percentage = ceil($currentPartNumber * 100 / $totalPartsCount); + + $this->eventDispatcher->dispatch( + \Ess\M2ePro\Model\Cron\Strategy\AbstractModel::PROGRESS_SET_DETAILS_EVENT_NAME, + [ + 'progress_nick' => \Ess\M2ePro\Model\Cron\Task\Listing\Product\ChangeTracker::NICK, + 'percentage' => $percentage, + 'total' => $totalPartsCount, + ] + ); + } +} diff --git a/Model/ChangeTracker/Common/Helpers/TrackerLogger.php b/Model/ChangeTracker/Common/Helpers/TrackerLogger.php index d2fc2b147..9e5243219 100644 --- a/Model/ChangeTracker/Common/Helpers/TrackerLogger.php +++ b/Model/ChangeTracker/Common/Helpers/TrackerLogger.php @@ -107,6 +107,22 @@ public function log($level, $message, array $context = []): void return; } + if (array_key_exists('tracker', $context)) { + /** @var \Ess\M2ePro\Model\ChangeTracker\Base\TrackerInterface $tracker */ + $tracker = $context['tracker']; + unset($context['tracker']); + + $prefix = sprintf( + '%s >> %s >> [%s - %s] >> ', + strtoupper($tracker->getChannel()), + strtoupper($tracker->getType()), + $tracker->getListingProductIdFrom(), + $tracker->getListingProductIdTo() + ); + + $message = $prefix . $message; + } + $this->tmpLogs[] = [ 'date' => Date::createCurrentGmt()->format('Y-m-d H:i:s'), 'level' => Logger::getLevelName($level), diff --git a/Model/ChangeTracker/PartManager.php b/Model/ChangeTracker/PartManager.php new file mode 100644 index 000000000..8714e2882 --- /dev/null +++ b/Model/ChangeTracker/PartManager.php @@ -0,0 +1,84 @@ +resourceConnection = $resourceConnection; + $this->listingProductResource = $listingProductResource; + } + + public function getPartsCount(): int + { + $select = $this->resourceConnection->getConnection()->select(); + $partsCountExpression = new \Zend_Db_Expr( + sprintf( + 'CEIL( COUNT(%s) / %s )', + \Ess\M2ePro\Model\ResourceModel\Listing\Product::COLUMN_ID, + self::PART_BATCH_SIZE + ) + ); + $select->from($this->listingProductResource->getMainTable(), [$partsCountExpression]); + + return (int)$select->query()->fetchColumn(); + } + + public function getListingProductParts(): \Generator + { + $select = $this->resourceConnection->getConnection()->select(); + $select->from($this->listingProductResource->getMainTable(), [ + 'component' => \Ess\M2ePro\Model\ResourceModel\Listing\Product::COMPONENT_MODE_FIELD, + 'id' => \Ess\M2ePro\Model\ResourceModel\Listing\Product::COLUMN_ID, + ]); + $select->order(\Ess\M2ePro\Model\ResourceModel\Listing\Product::COLUMN_ID); + $statement = $select->query(); + + $batchCounter = 0; + $batch = []; + + while ($row = $statement->fetch()) { + $batchCounter++; + $batch[$row['component']][] = (int)$row['id']; + + if ($batchCounter % self::PART_BATCH_SIZE === 0) { + yield $this->createPart($batch); + } + } + + if (empty($batch)) { + return; + } + + yield $this->createPart($batch); + } + + /** + * @param array $batch + * + * @return TrackerConfiguration[] + */ + private function createPart(array $batch): array + { + $part = []; + foreach ($batch as $component => $listingProductIds) { + $part[] = new TrackerConfiguration( + $component, + min($listingProductIds), + max($listingProductIds), + ); + } + + return $part; + } +} diff --git a/Model/ChangeTracker/TrackerConfiguration.php b/Model/ChangeTracker/TrackerConfiguration.php new file mode 100644 index 000000000..c76b9ee57 --- /dev/null +++ b/Model/ChangeTracker/TrackerConfiguration.php @@ -0,0 +1,19 @@ +component = $component; + $this->listingProductIdFrom = $listingProductIdFrom; + $this->listingProductIdTo = $listingProductIdTo; + } +} diff --git a/Model/Cron/Task/Listing/Product/ChangeTracker.php b/Model/Cron/Task/Listing/Product/ChangeTracker.php index 6e45b419b..1069a98e8 100644 --- a/Model/Cron/Task/Listing/Product/ChangeTracker.php +++ b/Model/Cron/Task/Listing/Product/ChangeTracker.php @@ -1,29 +1,18 @@ changeHolderFactory = $changeHolderFactory; $this->profiler = $profiler; $this->logger = $logger; - $this->accountCollection = $accountCollection; $this->changeTrackerHelper = $changeTrackerHelper; + $this->chunkManager = $chunkManager; + $this->changeTrackerProcessor = $changeTrackerProcessor; } /** @@ -79,53 +69,10 @@ public function isPossibleToRun() } /** - * @throws \Ess\M2ePro\Model\Exception\Logic - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Zend_Db_Statement_Exception * @throws \Throwable */ protected function performActions(): void { - $this->profiler->start(); - $this->initBuilders([ - $this->inventoryTrackerFactory, - $this->priceTrackerFactory, - ]); - $this->profiler->stop(); - - $this->logger->info('Prepare builders. ' . $this->profiler->logString()); - - foreach ($this->builders as $builder) { - $holder = $this->changeHolderFactory->create(); - $holder->holdChanges($builder); - } - } - - /** - * @param \Ess\M2ePro\Model\ChangeTracker\Base\TrackerFactoryInterface[] $factories - * - * @return void - */ - private function initBuilders(array $factories): void - { - foreach ($this->getActiveChannels() as $channel) { - foreach ($factories as $factory) { - $this->builders[] = $factory->create($channel); - } - } - } - - /** - * @return string[] - */ - private function getActiveChannels(): array - { - $collection = $this->accountCollection; - $select = $collection->getSelect(); - $select->reset(\Magento\Framework\DB\Select::COLUMNS); - $select->columns(['channel' => 'component_mode']); - $select->distinct(); - - return $collection->getColumnValues('channel'); + $this->changeTrackerProcessor->process(); } } diff --git a/Model/Cron/Task/Repository.php b/Model/Cron/Task/Repository.php index 2296c5c1e..505cd5e53 100644 --- a/Model/Cron/Task/Repository.php +++ b/Model/Cron/Task/Repository.php @@ -88,6 +88,7 @@ class Repository extends \Ess\M2ePro\Model\AbstractModel \Ess\M2ePro\Model\Cron\Task\Listing\Product\ChangeTracker::NICK => [ 'component' => self::COMPONENT_GENERAL, 'group' => self::GROUP_SYSTEM, + 'can-work-parallel' => true, ], \Ess\M2ePro\Model\Cron\Task\Listing\Product\InspectDirectChanges::NICK => [ 'component' => self::COMPONENT_GENERAL, diff --git a/Model/Ebay/AdvancedFilter/AllItemsOptions.php b/Model/Ebay/AdvancedFilter/AllItemsOptions.php index 89ab49472..dbfff782b 100644 --- a/Model/Ebay/AdvancedFilter/AllItemsOptions.php +++ b/Model/Ebay/AdvancedFilter/AllItemsOptions.php @@ -433,7 +433,8 @@ public function getMagentoProductTypeOptions(): DropDownFilter\OptionCollection $optionCollection = $this->optionCollectionFactory->create(); foreach ($optionsData as $optionData) { - $optionLabel = $magentoProductTypes[($optionData['type_id'])]; + $optionLabel = $magentoProductTypes[$optionData['type_id']] + ?? (string)__('Unknown Product Type "%type"', ['type' => $optionData['type_id']]); $optionValue = $optionData['type_id']; $option = $this->optionFactory->create($optionLabel, $optionValue); diff --git a/Model/Ebay/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php b/Model/Ebay/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php index fe0af510c..65a8e1e19 100644 --- a/Model/Ebay/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php +++ b/Model/Ebay/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php @@ -524,7 +524,7 @@ public function isMeetReviseTitleRequirements(): bool */ public function isMeetReviseSubtitleRequirements(): bool { - return $this->ebayReviseChecker->isNeedReviseForTitle( + return $this->ebayReviseChecker->isNeedReviseForSubtitle( $this->input->getListingProduct()->getChildObject() ); } diff --git a/Model/Setup/Installer.php b/Model/Setup/Installer.php index 3e100ff7d..e58a0acb9 100644 --- a/Model/Setup/Installer.php +++ b/Model/Setup/Installer.php @@ -11715,6 +11715,20 @@ private function installAmazonData() 'update_date' => '2023-06-25 00:00:00', 'create_date' => '2023-06-25 00:00:00', ], + [ + MarketplaceResource::COLUMN_ID => \Ess\M2ePro\Helper\Component\Amazon::MARKETPLACE_IE, + MarketplaceResource::COLUMN_NATIVE_ID + => \Ess\M2ePro\Helper\Component\Amazon::NATIVE_ID_MARKETPLACE_IE, + MarketplaceResource::COLUMN_TITLE => 'Ireland', + MarketplaceResource::COLUMN_CODE => 'IE', + MarketplaceResource::COLUMN_URL => 'amazon.ie', + MarketplaceResource::COLUMN_STATUS => 0, + MarketplaceResource::COLUMN_SORDER => 24, + MarketplaceResource::COLUMN_GROUP_TITLE => 'Europe', + MarketplaceResource::COLUMN_COMPONENT_MODE => 'amazon', + 'update_date' => '2024-12-20 00:00:00', + 'create_date' => '2023-12-20 00:00:00', + ], ] ); @@ -11889,6 +11903,15 @@ private function installAmazonData() AmazonMarketplaceResource::COLUMN_IS_VAT_CALCULATION_SERVICE_AVAILABLE => 1, AmazonMarketplaceResource::COLUMN_IS_PRODUCT_TAX_CODE_POLICY_AVAILABLE => 0, ], + [ + AmazonMarketplaceResource::COLUMN_MARKETPLACE_ID + => \Ess\M2ePro\Helper\Component\Amazon::MARKETPLACE_IE, + AmazonMarketplaceResource::COLUMN_DEFAULT_CURRENCY => 'EUR', + AmazonMarketplaceResource::COLUMN_IS_MERCHANT_FULFILLMENT_AVAILABLE => 1, + AmazonMarketplaceResource::COLUMN_IS_BUSINESS_AVAILABLE => 1, + AmazonMarketplaceResource::COLUMN_IS_VAT_CALCULATION_SERVICE_AVAILABLE => 1, + AmazonMarketplaceResource::COLUMN_IS_PRODUCT_TAX_CODE_POLICY_AVAILABLE => 0, + ], ] ); } diff --git a/Model/Setup/Upgrader.php b/Model/Setup/Upgrader.php index 188176b2d..1cc5950ae 100644 --- a/Model/Setup/Upgrader.php +++ b/Model/Setup/Upgrader.php @@ -223,6 +223,7 @@ class Upgrader '1.72.0' => ['1.72.1'], '1.72.1' => ['1.72.2'], '1.72.2' => ['1.73.0'], + '1.73.0' => ['1.74.0'], ]; //######################################## diff --git a/Model/Walmart/Listing/Product/Action/Type/ListAction/Validator.php b/Model/Walmart/Listing/Product/Action/Type/ListAction/Validator.php index 053dd494b..8fdcbc085 100644 --- a/Model/Walmart/Listing/Product/Action/Type/ListAction/Validator.php +++ b/Model/Walmart/Listing/Product/Action/Type/ListAction/Validator.php @@ -86,6 +86,20 @@ public function validate(): bool return true; } + private function validateWalmartProductType(): bool + { + if ( + $this->getWalmartMarketplace()->isSupportedProductType() + && !$this->getWalmartListingProduct()->isExistsProductType() + ) { + $this->addMessage('Product Type are not set.'); + + return false; + } + + return true; + } + //######################################## private function getSku() diff --git a/Model/Walmart/Listing/Product/Action/Type/Relist/Validator.php b/Model/Walmart/Listing/Product/Action/Type/Relist/Validator.php index 749a0c604..6edf24eb1 100644 --- a/Model/Walmart/Listing/Product/Action/Type/Relist/Validator.php +++ b/Model/Walmart/Listing/Product/Action/Type/Relist/Validator.php @@ -28,10 +28,6 @@ public function validate() return false; } - if (!$this->validateWalmartProductType()) { - return false; - } - if (!$this->validateMissedOnChannelBlocked()) { return false; } diff --git a/Model/Walmart/Listing/Product/Action/Type/Validator.php b/Model/Walmart/Listing/Product/Action/Type/Validator.php index 63c80547a..63a123b92 100644 --- a/Model/Walmart/Listing/Product/Action/Type/Validator.php +++ b/Model/Walmart/Listing/Product/Action/Type/Validator.php @@ -278,22 +278,6 @@ protected function validateSku() // --------------------------------------- - protected function validateWalmartProductType(): bool - { - if ( - $this->getWalmartMarketplace()->isSupportedProductType() - && !$this->getWalmartListingProduct()->isExistsProductType() - ) { - $this->addMessage('Product Type are not set.'); - - return false; - } - - return true; - } - - // --------------------------------------- - protected function validateOnlinePriceInvalidBlocked() { if ($this->getListingProduct()->isBlocked() && $this->getWalmartListingProduct()->isOnlinePriceInvalid()) { diff --git a/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php b/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php index c9e698aab..f9a4528a8 100644 --- a/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php +++ b/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Active.php @@ -61,17 +61,6 @@ public function isAllowed() return false; } - /** @var \Ess\M2ePro\Model\Walmart\Listing\Product $walmartListingProduct */ - $walmartListingProduct = $listingProduct->getChildObject(); - if ( - $walmartListingProduct - ->getWalmartMarketplace() - ->isSupportedProductType() - && !$walmartListingProduct->isExistsProductType() - ) { - return false; - } - if ($scheduledAction = $this->input->getScheduledAction()) { if ($scheduledAction->isActionTypeDelete() && $scheduledAction->isForce()) { return false; @@ -268,15 +257,6 @@ public function isMeetStopRequirements() return false; } - if ( - $walmartListingProduct - ->getWalmartMarketplace() - ->isSupportedProductType() - && !$walmartListingProduct->isExistsProductType() - ) { - return false; - } - if ($walmartSynchronizationTemplate->isStopStatusDisabled()) { if (!$listingProduct->getMagentoProduct()->isStatusEnabled()) { return true; diff --git a/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Inactive.php b/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Inactive.php index 2e248b6b9..9ae258881 100644 --- a/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Inactive.php +++ b/Model/Walmart/Listing/Product/Instruction/SynchronizationTemplate/Checker/Inactive.php @@ -183,15 +183,6 @@ public function isMeetRelistRequirements() return false; } - if ( - $walmartListingProduct - ->getWalmartMarketplace() - ->isSupportedProductType() - && !$walmartListingProduct->isExistsProductType() - ) { - return false; - } - $variationResource = $this->activeRecordFactory->getObject('Listing_Product_Variation')->getResource(); if ($walmartSynchronizationTemplate->isRelistStatusEnabled()) { diff --git a/Setup/MigrationFromMagento1/PreconditionsChecker/AbstractModel.php b/Setup/MigrationFromMagento1/PreconditionsChecker/AbstractModel.php index 0ef088379..cba39043e 100644 --- a/Setup/MigrationFromMagento1/PreconditionsChecker/AbstractModel.php +++ b/Setup/MigrationFromMagento1/PreconditionsChecker/AbstractModel.php @@ -81,6 +81,7 @@ abstract class AbstractModel '6.70.*', '6.71.*', '6.73.*', + '6.74.*', ]; /** @var \Ess\M2ePro\Model\ActiveRecord\Factory */ diff --git a/Setup/Update/Config.php b/Setup/Update/Config.php index 895c2d922..215be73b5 100644 --- a/Setup/Update/Config.php +++ b/Setup/Update/Config.php @@ -398,6 +398,7 @@ public function getFeaturesList(): array ], 'y24_m12' => [ 'AddLanguageToEbayComplianceDocuments', + 'AddAmazonMarketplaceIreland', ], ]; } @@ -437,7 +438,6 @@ public static function getFeaturesForRepeatAfterMigrationFromMagento1(): array \Ess\M2ePro\Setup\Update\y23_m09\AddOnlineBestOfferForEbayProduct::class, \Ess\M2ePro\Setup\Update\y23_m09\RefactorAmazonOrderColumns::class, \Ess\M2ePro\Setup\Update\y23_m09\RemoveLastAccessAndRunFromConfigTable::class, - \Ess\M2ePro\Setup\Update\y23_m09\AddAmazonProductTypeAttributeMappingTable::class, \Ess\M2ePro\Setup\Update\y23_m09\AddPriceRoundingToEbayAmazonWalmartSellingTemplate::class, \Ess\M2ePro\Setup\Update\y23_m10\CreateEbayCategorySpecificValidationResultTable::class, \Ess\M2ePro\Setup\Update\y23_m10\ImproveAmazonOrderPrefixes::class, @@ -469,7 +469,6 @@ public static function getFeaturesForRepeatAfterMigrationFromMagento1(): array \Ess\M2ePro\Setup\Update\y24_m06\AddAmazonShippingPalletDelivery::class, \Ess\M2ePro\Setup\Update\y24_m06\AddPriceLastUpdateDateColumnToEbayListingProductTable::class, \Ess\M2ePro\Setup\Update\y24_m06\RemoveAuEpidsVisibleFromConfigTable::class, - \Ess\M2ePro\Setup\Update\y24_m06\AddAmazonMarketplaceSaudiArabia::class, \Ess\M2ePro\Setup\Update\y24_m06\AddEbayBundleOptionMappingTable::class, \Ess\M2ePro\Setup\Update\y24_m07\AddProductIdentifiersSettingsForAmazonListing::class, diff --git a/Setup/Update/y24_m12/AddAmazonMarketplaceIreland.php b/Setup/Update/y24_m12/AddAmazonMarketplaceIreland.php new file mode 100644 index 000000000..f8e7d97db --- /dev/null +++ b/Setup/Update/y24_m12/AddAmazonMarketplaceIreland.php @@ -0,0 +1,96 @@ +createMarketplace(); + $this->createAmazonMarketplace(); + } + + private function createMarketplace(): void + { + $marketplaceTableName = $this->getFullTableName(Tables::TABLE_MARKETPLACE); + + $marketplace = $this->installer + ->getConnection()->select() + ->from($marketplaceTableName) + ->where( + MarketplaceResource::COLUMN_ID . ' = ?', + \Ess\M2ePro\Helper\Component\Amazon::MARKETPLACE_IE + ) + ->query() + ->fetch(); + + if ($marketplace !== false) { + return; + } + + $this->installer->getConnection()->insert( + $marketplaceTableName, + [ + MarketplaceResource::COLUMN_ID => \Ess\M2ePro\Helper\Component\Amazon::MARKETPLACE_IE, + MarketplaceResource::COLUMN_NATIVE_ID => \Ess\M2ePro\Helper\Component\Amazon::NATIVE_ID_MARKETPLACE_IE, + MarketplaceResource::COLUMN_TITLE => 'Ireland', + MarketplaceResource::COLUMN_CODE => 'IE', + MarketplaceResource::COLUMN_URL => 'amazon.ie', + MarketplaceResource::COLUMN_STATUS => 0, + MarketplaceResource::COLUMN_SORDER => 24, + MarketplaceResource::COLUMN_GROUP_TITLE => 'Europe', + MarketplaceResource::COLUMN_COMPONENT_MODE => 'amazon', + 'update_date' => '2024-12-20 00:00:00', + 'create_date' => '2023-12-20 00:00:00', + ] + ); + } + + private function createAmazonMarketplace(): void + { + $amazonMarketplaceTableName = $this->getFullTableName(Tables::TABLE_AMAZON_MARKETPLACE); + + $marketplace = $this->installer + ->getConnection() + ->select() + ->from($amazonMarketplaceTableName) + ->where( + AmazonMarketplaceResource::COLUMN_MARKETPLACE_ID . ' = ?', + \Ess\M2ePro\Helper\Component\Amazon::MARKETPLACE_IE + ) + ->query() + ->fetch(); + + if ($marketplace !== false) { + return; + } + + $bind = [ + AmazonMarketplaceResource::COLUMN_MARKETPLACE_ID => \Ess\M2ePro\Helper\Component\Amazon::MARKETPLACE_IE, + AmazonMarketplaceResource::COLUMN_DEFAULT_CURRENCY => 'EUR', + AmazonMarketplaceResource::COLUMN_IS_MERCHANT_FULFILLMENT_AVAILABLE => 1, + AmazonMarketplaceResource::COLUMN_IS_BUSINESS_AVAILABLE => 1, + AmazonMarketplaceResource::COLUMN_IS_VAT_CALCULATION_SERVICE_AVAILABLE => 1, + AmazonMarketplaceResource::COLUMN_IS_PRODUCT_TAX_CODE_POLICY_AVAILABLE => 0, + ]; + + if ( + $this->getTableModifier(Tables::TABLE_AMAZON_MARKETPLACE) + ->isColumnExists('is_new_asin_available') + ) { + // May be missing after migration from m1 + $bind['is_new_asin_available'] = 1; + } + + $this->installer->getConnection()->insert( + $amazonMarketplaceTableName, + $bind + ); + } +} diff --git a/Setup/Upgrade/v1_73_0__v1_74_0/Config.php b/Setup/Upgrade/v1_73_0__v1_74_0/Config.php new file mode 100644 index 000000000..d488bc326 --- /dev/null +++ b/Setup/Upgrade/v1_73_0__v1_74_0/Config.php @@ -0,0 +1,15 @@ + - +