diff --git a/.github/workflows/magento-ci.yml b/.github/workflows/magento-ci.yml index b329f29..29bfe13 100644 --- a/.github/workflows/magento-ci.yml +++ b/.github/workflows/magento-ci.yml @@ -7,7 +7,7 @@ env: DB_USER: root DB_PASSWORD: root ES_VERSION: 7.10.0 - PHP_VERSION: 8.1 + PHP_VERSION: 8.3 on: [pull_request] @@ -23,7 +23,7 @@ jobs: uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php with: php-version: ${{ env.PHP_VERSION }} - tools: composer:2.2.17 + tools: composer extensions: mbstring, gd, bcmath, soap, dom, xml, json, tokenizer, mysql, zip, xdebug # Use composer cache diff --git a/Block/Adminhtml/InitialImport/CurrentProgress.php b/Block/Adminhtml/InitialImport/CurrentProgress.php index 4ed35f5..66b592b 100644 --- a/Block/Adminhtml/InitialImport/CurrentProgress.php +++ b/Block/Adminhtml/InitialImport/CurrentProgress.php @@ -14,7 +14,7 @@ use Bloomreach\EngagementConnector\Model\Export\Queue\AddInitialExportDataToExportQueue; use Bloomreach\EngagementConnector\Model\Export\Queue\Source\StatusSource as ExportQueueStatusSource; use Bloomreach\EngagementConnector\Model\ExportQueueModel; -use Bloomreach\EngagementConnector\Model\InitialExportStatus\Source\StatusSource as InitialExportStatusSource; +use Bloomreach\EngagementConnector\Model\InitialExportStatus\Source\StatusSource; use Bloomreach\EngagementConnector\Model\ResourceModel\ExportQueue\Collection as ExportQueueCollection; use Bloomreach\EngagementConnector\Model\ResourceModel\ExportQueue\CollectionFactory as ExportQueueCollectionFactory; use Bloomreach\EngagementConnector\Service\InitialExportStatus\ItemGetter as InitialExportStatusGetter; @@ -180,7 +180,10 @@ public function getExportQueueCollection(): ExportQueueCollection */ public function isStarted(): bool { - return $this->getInitialExportStatus()->getStatus() === InitialExportStatusSource::PROCESSING; + return in_array( + $this->getInitialExportStatus()->getStatus(), + StatusSource::PROGRESS_LOG_VISIBLE_STATUSES + ); } /** @@ -307,6 +310,42 @@ public function getDecoratedStatus(ExportQueueInterface $exportQueue): string ); } + /** + * Checks whether import is finished + * + * @return bool + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function isFinished(): bool + { + return in_array($this->getInitialExportStatus()->getStatus(), StatusSource::FINISHED_STATUSES); + } + + /** + * Get total error items + * + * @return int + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function getTotalErrorItems(): int + { + return $this->getInitialExportStatus()->getTotalErrorItems(); + } + + /** + * Get total exported items + * + * @return int + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function getTotalExportedItems(): int + { + return $this->getInitialExportStatus()->getTotalExported(); + } + /** * Set template * diff --git a/Model/Config/Backend/ValidateSearchableFields.php b/Model/Config/Backend/ValidateSearchableFields.php new file mode 100644 index 0000000..93b8307 --- /dev/null +++ b/Model/Config/Backend/ValidateSearchableFields.php @@ -0,0 +1,54 @@ +getFieldsetDataValue('enabled') !== '1') { + return parent::beforeSave(); + } + + $value = $this->getValue(); + + if (!$value) { + throw new LocalizedException( + __( + 'Searchable fields cannot be empty.', + FieldsMapper::MAX_SEARCHABLE + ) + ); + } + + if (count($value) <= FieldsMapper::MAX_SEARCHABLE) { + return parent::beforeSave(); + + } + + throw new LocalizedException( + __( + 'Maximum number of searchable fields exceeded. Max number: "%1"', + FieldsMapper::MAX_SEARCHABLE + ) + ); + } +} diff --git a/Model/Config/Source/CatalogProductFields.php b/Model/Config/Source/CatalogProductFields.php new file mode 100644 index 0000000..bafa2c8 --- /dev/null +++ b/Model/Config/Source/CatalogProductFields.php @@ -0,0 +1,67 @@ +configResolver = $configResolver; + } + + /** + * Get options + * + * @return array + * @throws NotFoundException + */ + public function toOptionArray() + { + $options = []; + + foreach ($this->configResolver->getByEntityType($this->getEntityType()) as $item) { + if ($item->getBloomreachCode() === FieldsMapper::PRIMARY_ID) { + continue; + } + + $options[] = [ + 'label' => $item->getBloomreachCode(), + 'value' => $item->getBloomreachCode() + ]; + } + + return $options; + } + + /** + * Get entity type + * + * @return string + */ + protected function getEntityType(): string + { + return DefaultType::ENTITY_TYPE; + } +} diff --git a/Model/Config/Source/CatalogProductVariantsFields.php b/Model/Config/Source/CatalogProductVariantsFields.php new file mode 100644 index 0000000..9ffa331 --- /dev/null +++ b/Model/Config/Source/CatalogProductVariantsFields.php @@ -0,0 +1,30 @@ +getData($fieldCode), 2); + } +} diff --git a/Model/DataMapping/FieldValueRenderer/Order/FloatValue.php b/Model/DataMapping/FieldValueRenderer/Order/FloatValue.php index 366c5a6..312e3bc 100644 --- a/Model/DataMapping/FieldValueRenderer/Order/FloatValue.php +++ b/Model/DataMapping/FieldValueRenderer/Order/FloatValue.php @@ -7,25 +7,12 @@ namespace Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Order; -use Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\RenderInterface; -use Magento\Framework\Api\AbstractSimpleObject; -use Magento\Framework\Model\AbstractModel; +use Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\DefaultFloatValueRenderer; /** - * The class is responsible for rendering float value field + * @deprecated + * @see DefaultFloatValueRenderer */ -class FloatValue implements RenderInterface +class FloatValue extends DefaultFloatValueRenderer { - /** - * Render the value of float field - * - * @param AbstractSimpleObject|AbstractModel $entity - * @param string $fieldCode - * - * @return string - */ - public function render($entity, string $fieldCode) - { - return number_format(round((float) $entity->getData($fieldCode), 2), 4, '.', ''); - } } diff --git a/Model/DataMapping/FieldValueRenderer/Order/VariantList.php b/Model/DataMapping/FieldValueRenderer/Order/VariantList.php index 9901c93..a223136 100644 --- a/Model/DataMapping/FieldValueRenderer/Order/VariantList.php +++ b/Model/DataMapping/FieldValueRenderer/Order/VariantList.php @@ -94,7 +94,7 @@ private function getItemData(OrderItemInterface $orderItem): array return [ 'variant_id' => $productId, 'sku' => $orderItem->getSku(), - 'quantity' => $orderItem->getQtyOrdered() + 'quantity' => (int) $orderItem->getQtyOrdered() ]; } } diff --git a/Model/DataMapping/FieldValueRenderer/OrderItem/OriginalPrice.php b/Model/DataMapping/FieldValueRenderer/OrderItem/OriginalPrice.php index e3a7054..62e9f2f 100644 --- a/Model/DataMapping/FieldValueRenderer/OrderItem/OriginalPrice.php +++ b/Model/DataMapping/FieldValueRenderer/OrderItem/OriginalPrice.php @@ -38,17 +38,13 @@ public function __construct(GetChildItems $getChildItems) * @param AbstractSimpleObject|AbstractModel $entity * @param string $fieldCode * - * @return string + * @return float */ public function render($entity, string $fieldCode) { - $price = $this->roundPrice((float) $entity->getData($fieldCode)); - - if ($entity->getProductType() === BundleType::TYPE_CODE) { - $price = $this->getBundleProductPrice($entity, $fieldCode); - } - - return number_format($price, 2, ',', ''); + return $entity->getProductType() === BundleType::TYPE_CODE + ? $this->getBundleProductPrice($entity, $fieldCode) + : $this->roundPrice((float) $entity->getData($fieldCode)); } /** diff --git a/Model/DataMapping/FieldValueRenderer/Product/Discount.php b/Model/DataMapping/FieldValueRenderer/Product/Discount.php index 7120e01..2cab950 100644 --- a/Model/DataMapping/FieldValueRenderer/Product/Discount.php +++ b/Model/DataMapping/FieldValueRenderer/Product/Discount.php @@ -37,10 +37,10 @@ public function __construct(GetDiscountService $getDiscountService) * @param AbstractSimpleObject|AbstractModel $entity * @param string $fieldCode * - * @return string + * @return float */ public function render($entity, string $fieldCode) { - return number_format($this->getDiscountService->execute($entity), 2, '.', ''); + return $this->getDiscountService->execute($entity); } } diff --git a/Model/DataMapping/FieldValueRenderer/Product/FinalPrice.php b/Model/DataMapping/FieldValueRenderer/Product/FinalPrice.php index fb3d451..b8edce0 100644 --- a/Model/DataMapping/FieldValueRenderer/Product/FinalPrice.php +++ b/Model/DataMapping/FieldValueRenderer/Product/FinalPrice.php @@ -22,10 +22,10 @@ class FinalPrice implements RenderInterface * @param AbstractSimpleObject|AbstractModel $entity * @param string $fieldCode * - * @return string + * @return float */ public function render($entity, string $fieldCode) { - return number_format(round((float) $entity->getFinalPrice(), 2), 4, '.', ''); + return round((float) $entity->getFinalPrice(), 2); } } diff --git a/Model/DataMapping/FieldValueRenderer/Product/FloatValue.php b/Model/DataMapping/FieldValueRenderer/Product/FloatValue.php new file mode 100644 index 0000000..fd23101 --- /dev/null +++ b/Model/DataMapping/FieldValueRenderer/Product/FloatValue.php @@ -0,0 +1,30 @@ +getQuoteTotals->execute($entity); - return number_format( + return (float) round( (float) ($quoteTotals ? $quoteTotals->getBaseGrandTotal() : $entity->getBaseGrandTotal()), - 2, - ',', - '' + 2 ); } } diff --git a/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceLocalCurrency.php b/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceLocalCurrency.php index c84150b..cb464ea 100644 --- a/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceLocalCurrency.php +++ b/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceLocalCurrency.php @@ -47,6 +47,6 @@ public function render($entity, string $fieldCode) $grandTotal = $quoteTotals->getGrandTotal() + $quoteTotals->getTaxAmount(); } - return number_format((float) ($grandTotal ?: $entity->getGrandTotal()), 2, '.', ''); + return round((float) ($grandTotal ?: $entity->getGrandTotal()), 2); } } diff --git a/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceWithoutTax.php b/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceWithoutTax.php index f00b654..6ea70ae 100644 --- a/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceWithoutTax.php +++ b/Model/DataMapping/FieldValueRenderer/Quote/TotalPriceWithoutTax.php @@ -47,6 +47,6 @@ public function render($entity, string $fieldCode) $totalWithoutTax = $quoteTotals->getBaseGrandTotal() - $quoteTotals->getBaseTaxAmount(); } - return number_format($totalWithoutTax ?: (float) $entity->getBaseGrandTotal(), 2, '.', ''); + return $totalWithoutTax ? (float) round((float) $entity->getBaseGrandTotal(), 2) : 0.0; } } diff --git a/Model/Export/Condition/IsEntityTypeFeedEnabled.php b/Model/Export/Condition/IsEntityTypeFeedEnabled.php index efb4aa0..3a137aa 100644 --- a/Model/Export/Condition/IsEntityTypeFeedEnabled.php +++ b/Model/Export/Condition/IsEntityTypeFeedEnabled.php @@ -32,16 +32,24 @@ class IsEntityTypeFeedEnabled */ private $cache = []; + /** + * @var bool + */ + private $useCache; + /** * @param ScopeConfigInterface $scopeConfig * @param ConfigPathGetter $configPathGetter + * @param bool $useCache */ public function __construct( ScopeConfigInterface $scopeConfig, - ConfigPathGetter $configPathGetter + ConfigPathGetter $configPathGetter, + bool $useCache = true ) { $this->scopeConfig = $scopeConfig; $this->configPathGetter = $configPathGetter; + $this->useCache = $useCache; } /** @@ -59,7 +67,7 @@ public function execute( string $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null ): bool { - if (!array_key_exists($entityType, $this->cache)) { + if (!array_key_exists($entityType, $this->cache) || !$this->useCache) { $this->cache[$entityType] = $this->isAllowed($entityType, $scopeType, $scopeCode); } diff --git a/Model/Export/Condition/IsRealTimeUpdateAllowed.php b/Model/Export/Condition/IsRealTimeUpdateAllowed.php index 480be75..09ab115 100644 --- a/Model/Export/Condition/IsRealTimeUpdateAllowed.php +++ b/Model/Export/Condition/IsRealTimeUpdateAllowed.php @@ -29,16 +29,24 @@ class IsRealTimeUpdateAllowed */ private $cache = []; + /** + * @var bool + */ + private $useCache; + /** * @param ScopeConfigInterface $scopeConfig * @param array $configPool + * @param bool $useCache */ public function __construct( ScopeConfigInterface $scopeConfig, - array $configPool = [] + array $configPool = [], + bool $useCache = true ) { $this->scopeConfig = $scopeConfig; $this->configPool = $configPool; + $this->useCache = $useCache; } /** @@ -55,7 +63,7 @@ public function execute( string $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null ): bool { - if (!array_key_exists($entityType, $this->cache)) { + if (!array_key_exists($entityType, $this->cache) || !$this->useCache) { $this->cache[$entityType] = $this->isAllowed($entityType, $scopeType, $scopeCode); } diff --git a/Model/Export/Queue/AddApiTypeFilter.php b/Model/Export/Queue/AddApiTypeFilter.php index e5e3ff7..f7d246d 100644 --- a/Model/Export/Queue/AddApiTypeFilter.php +++ b/Model/Export/Queue/AddApiTypeFilter.php @@ -18,16 +18,6 @@ */ class AddApiTypeFilter { - private const IN_PROGRESS_STATUSES = [ - StatusSource::SCHEDULED, - StatusSource::PROCESSING - ]; - - private const FINISHED_STATUSES = [ - StatusSource::ERROR, - StatusSource::SUCCESS - ]; - /** * @var EntityType */ @@ -72,7 +62,7 @@ public function execute(ExportQueueCollection $exportQueueCollection): void ExportQueueModel::ENTITY_TYPE, $item->getEntityType() ); - } elseif (in_array($item->getStatus(), self::FINISHED_STATUSES)) { + } elseif (in_array($item->getStatus(), StatusSource::FINISHED_STATUSES)) { $result[] = sprintf( '(%s = \'%s\' AND %s != \'%s\')', ExportQueueModel::ENTITY_TYPE, @@ -80,7 +70,7 @@ public function execute(ExportQueueCollection $exportQueueCollection): void ExportQueueModel::API_TYPE, AddInitialExportDataToExportQueue::API_TYPE ); - } elseif (in_array($item->getStatus(), self::IN_PROGRESS_STATUSES)) { + } elseif (in_array($item->getStatus(), StatusSource::IN_PROGRESS_STATUSES)) { $result[] = sprintf( '(%s = \'%s\' AND %s = \'%s\')', ExportQueueModel::ENTITY_TYPE, diff --git a/Model/Export/Queue/Batch/Command/Data/Builder/CustomerUpdate.php b/Model/Export/Queue/Batch/Command/Data/Builder/CustomerUpdate.php index 9bc0253..bcdc023 100644 --- a/Model/Export/Queue/Batch/Command/Data/Builder/CustomerUpdate.php +++ b/Model/Export/Queue/Batch/Command/Data/Builder/CustomerUpdate.php @@ -8,7 +8,6 @@ namespace Bloomreach\EngagementConnector\Model\Export\Queue\Batch\Command\Data\Builder; use Bloomreach\EngagementConnector\Api\Data\ExportQueueInterface; -use Bloomreach\EngagementConnector\Model\DataMapping\Event\RegisteredGenerator; use InvalidArgumentException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\SerializerInterface; @@ -23,21 +22,12 @@ class CustomerUpdate implements BuilderInterface */ private $jsonSerializer; - /** - * @var RegisteredGenerator - */ - private $registeredGenerator; - /** * @param SerializerInterface $jsonSerializer - * @param RegisteredGenerator $registeredGenerator */ - public function __construct( - SerializerInterface $jsonSerializer, - RegisteredGenerator $registeredGenerator - ) { + public function __construct(SerializerInterface $jsonSerializer) + { $this->jsonSerializer = $jsonSerializer; - $this->registeredGenerator = $registeredGenerator; } /** @@ -52,10 +42,6 @@ public function build(ExportQueueInterface $exportQueue): array { $properties = $this->unserializeData($exportQueue->getBody()); - if (is_array($properties)) { - $this->registeredGenerator->deleteRegisteredData($properties); - } - return [ 'customer_ids' => $this->jsonSerializer->unserialize($exportQueue->getRegistered()), 'properties' => $properties, diff --git a/Model/Export/Transporter/Batch/RequestBuilder.php b/Model/Export/Transporter/Batch/RequestBuilder.php index f14a778..e004d0a 100644 --- a/Model/Export/Transporter/Batch/RequestBuilder.php +++ b/Model/Export/Transporter/Batch/RequestBuilder.php @@ -10,6 +10,7 @@ use Bloomreach\EngagementConnector\Block\Adminhtml\System\Config\ModuleVersion; use Bloomreach\EngagementConnector\Model\Export\Queue\Batch\CommandsListBuilder; use Magento\Framework\Module\ResourceInterface; +use Magento\Framework\App\ProductMetadataInterface; /** * The class is responsible for building batch request data @@ -26,6 +27,11 @@ class RequestBuilder */ private $commandsListBuilder; + /** + * @var ProductMetadataInterface + */ + private $productMetadata; + /** * @var string */ @@ -34,13 +40,16 @@ class RequestBuilder /** * @param ResourceInterface $moduleResource * @param CommandsListBuilder $commandsListBuilder + * @param ProductMetadataInterface $productMetadata */ public function __construct( ResourceInterface $moduleResource, - CommandsListBuilder $commandsListBuilder + CommandsListBuilder $commandsListBuilder, + ProductMetadataInterface $productMetadata ) { $this->moduleResource = $moduleResource; $this->commandsListBuilder = $commandsListBuilder; + $this->productMetadata = $productMetadata; } /** @@ -55,6 +64,7 @@ public function build(array $exportQueueList): array return [ 'sdk' => 'magento-plugin', 'sdk_version' => $this->getModuleVersion(), + 'magento_version' => sprintf('v%s', $this->productMetadata->getVersion()), 'commands' => $this->commandsListBuilder->build($exportQueueList) ]; } diff --git a/Model/InitialExport/Action/Configure/Catalog/Create.php b/Model/InitialExport/Action/Configure/Catalog/Create.php index 80f486f..2bc39cb 100644 --- a/Model/InitialExport/Action/Configure/Catalog/Create.php +++ b/Model/InitialExport/Action/Configure/Catalog/Create.php @@ -128,7 +128,8 @@ private function getFields(string $entityType): array } return $this->fieldsMapper->map( - $this->dataMapperResolver->map($collection->getFirstItem(), $entityType)->toArray() + $this->dataMapperResolver->map($collection->getFirstItem(), $entityType)->toArray(), + $entityType ); } } diff --git a/Model/InitialExport/Action/Configure/Catalog/FieldsMapper.php b/Model/InitialExport/Action/Configure/Catalog/FieldsMapper.php index 85a3acf..01cb2d0 100644 --- a/Model/InitialExport/Action/Configure/Catalog/FieldsMapper.php +++ b/Model/InitialExport/Action/Configure/Catalog/FieldsMapper.php @@ -8,6 +8,8 @@ namespace Bloomreach\EngagementConnector\Model\InitialExport\Action\Configure\Catalog; use Bloomreach\EngagementConnector\Service\ValueTypeGetter; +use Bloomreach\EngagementConnector\System\SearchableFieldsResolver; +use Magento\Framework\Exception\LocalizedException; /** * The class is responsible for mapping fields @@ -23,37 +25,59 @@ class FieldsMapper */ private $valueTypeGetter; + /** + * @var SearchableFieldsResolver + */ + private $searchableFieldsResolver; + /** * @param ValueTypeGetter $valueTypeGetter + * @param SearchableFieldsResolver $searchableFieldsResolver */ - public function __construct(ValueTypeGetter $valueTypeGetter) - { + public function __construct( + ValueTypeGetter $valueTypeGetter, + SearchableFieldsResolver $searchableFieldsResolver + ) { $this->valueTypeGetter = $valueTypeGetter; + $this->searchableFieldsResolver = $searchableFieldsResolver; } /** * Maps data * * @param array $data + * @param string|null $entityType * * @return array + * @throws LocalizedException */ - public function map(array $data): array + public function map(array $data, ?string $entityType = null): array { $result = []; - $iterator = 1; + $searchableFields = $entityType ? $this->searchableFieldsResolver->get($entityType) : []; + $searchableFieldsNumber = count($searchableFields); + $searchableNumber = 1; foreach ($data as $column => $value) { if ($column === self::PRIMARY_ID) { continue; } + $isSearchable = $searchableFieldsNumber > 0 + ? in_array($column, $searchableFields) && self::MAX_SEARCHABLE >= $searchableNumber + : self::MAX_SEARCHABLE >= $searchableNumber; + $result[] = [ 'name' => $column, 'type' => $this->valueTypeGetter->execute($value), - 'searchable' => self::MAX_SEARCHABLE >= $iterator, + 'searchable' => $isSearchable ]; - $iterator++; + + if (!$isSearchable) { + continue; + } + + $searchableNumber++; } return $result; diff --git a/Model/InitialExport/Action/Configure/Import/Request/Entity/Customer.php b/Model/InitialExport/Action/Configure/Import/Request/Entity/Customer.php index 6c17238..e9a6a13 100644 --- a/Model/InitialExport/Action/Configure/Import/Request/Entity/Customer.php +++ b/Model/InitialExport/Action/Configure/Import/Request/Entity/Customer.php @@ -9,6 +9,7 @@ use Bloomreach\EngagementConnector\Model\InitialExport\Action\Configure\Import\Request\BuilderInterface; use Bloomreach\EngagementConnector\Model\InitialExport\Action\Configure\Import\Request\IdMappingGetter; +use stdClass; /** * The class is responsible for building request body for Customer entity type @@ -39,7 +40,7 @@ public function __construct(IdMappingGetter $idMappingGetter) */ public function build(string $entityType, array $body = []): array { - $body['destination'] = ['customer_destination' => []]; + $body['destination'] = ['customer_destination' => new stdClass()]; $body['mapping']['column_mapping']['id_mappings'] = $this->idMappingGetter->execute(); return $body; diff --git a/Model/InitialExport/Action/Configure/Import/Source/PropertyMapper.php b/Model/InitialExport/Action/Configure/Import/Source/PropertyMapper.php index 3241fdd..13dd935 100644 --- a/Model/InitialExport/Action/Configure/Import/Source/PropertyMapper.php +++ b/Model/InitialExport/Action/Configure/Import/Source/PropertyMapper.php @@ -9,6 +9,8 @@ use Bloomreach\EngagementConnector\Model\InitialExport\Action\Configure\Catalog\FieldsMapper; use Bloomreach\EngagementConnector\Service\ValueTypeGetter; +use Bloomreach\EngagementConnector\System\SearchableFieldsResolver; +use Magento\Framework\Exception\LocalizedException; /** * The class is responsible for mapping properties @@ -22,40 +24,73 @@ class PropertyMapper */ private $valueTypeGetter; + /** + * @var SearchableFieldsResolver + */ + private $searchableFieldsResolver; + /** * @param ValueTypeGetter $valueTypeGetter + * @param SearchableFieldsResolver $searchableFieldsResolver */ - public function __construct(ValueTypeGetter $valueTypeGetter) - { + public function __construct( + ValueTypeGetter $valueTypeGetter, + SearchableFieldsResolver $searchableFieldsResolver + ) { $this->valueTypeGetter = $valueTypeGetter; + $this->searchableFieldsResolver = $searchableFieldsResolver; } /** * Maps data * * @param array $data + * @param string|null $entityType * * @return array */ - public function map(array $data): array + public function map(array $data, ?string $entityType = null): array { $result = []; $iterator = 1; + $searchableFields = $this->getSearchableFields($entityType); + $searchableFieldsNumber = count($searchableFields); foreach ($data as $column => $value) { + $isSearchable = !($column === self::PRIMARY_ID) + && ($searchableFieldsNumber > 0 + ? in_array($column, $searchableFields) && FieldsMapper::MAX_SEARCHABLE >= $iterator + : FieldsMapper::MAX_SEARCHABLE >= $iterator + ); $result[] = [ 'from_column' => $column, 'to_column' => $column, 'target_type' => $this->valueTypeGetter->execute($value), - 'searchable' => $column !== self::PRIMARY_ID && FieldsMapper::MAX_SEARCHABLE >= $iterator, + 'searchable' => $isSearchable, 'indexed' => null ]; - if ($column !== self::PRIMARY_ID) { + if ($column !== self::PRIMARY_ID && $isSearchable) { $iterator++; } } return $result; } + + /** + * Returns searchable fields + * + * @param string|null $entityType + * + * @return array + */ + private function getSearchableFields(?string $entityType = null): array + { + try { + return $entityType ? $this->searchableFieldsResolver->get($entityType) : []; + } catch (LocalizedException $e) { + return []; + } + } } diff --git a/Model/InitialExport/Action/Configure/Import/Source/SourceGenerator.php b/Model/InitialExport/Action/Configure/Import/Source/SourceGenerator.php index 646201d..6086ba6 100644 --- a/Model/InitialExport/Action/Configure/Import/Source/SourceGenerator.php +++ b/Model/InitialExport/Action/Configure/Import/Source/SourceGenerator.php @@ -103,7 +103,7 @@ public function generateEmpty(string $entityType): SourceInterface $entityType )->toArray(); $source->setFileUrl($this->sampleFileGenerator->execute(array_keys($data), $entityType)); - $source->setPropertyMappings($this->propertyMapper->map($data)); + $source->setPropertyMappings($this->propertyMapper->map($data, $entityType)); return $source; } @@ -132,7 +132,7 @@ public function generate(string $entityType): SourceInterface } $source->setFileUrl($this->sampleFileGenerator->execute($data, $entityType)); - $source->setPropertyMappings($this->propertyMapper->map(current($data))); + $source->setPropertyMappings($this->propertyMapper->map(current($data), $entityType)); return $source; } diff --git a/Model/InitialExportStatus/Source/StatusSource.php b/Model/InitialExportStatus/Source/StatusSource.php index 30c3a35..6b97908 100644 --- a/Model/InitialExportStatus/Source/StatusSource.php +++ b/Model/InitialExportStatus/Source/StatusSource.php @@ -16,18 +16,25 @@ class StatusSource implements OptionSourceInterface { public const DISABLED = 1; - public const NOT_READY = 2; - public const READY = 3; - public const SCHEDULED = 4; - public const PROCESSING = 5; - public const ERROR = 6; - public const SUCCESS = 7; + public const IN_PROGRESS_STATUSES = [ + StatusSource::SCHEDULED, + StatusSource::PROCESSING + ]; + public const FINISHED_STATUSES = [ + StatusSource::ERROR, + StatusSource::SUCCESS + ]; + public const PROGRESS_LOG_VISIBLE_STATUSES = [ + StatusSource::PROCESSING, + StatusSource::ERROR, + StatusSource::SUCCESS + ]; /** * Get Statuses Source diff --git a/Model/Product/CategoryDataResolver.php b/Model/Product/CategoryDataResolver.php index 48740bd..2aea280 100644 --- a/Model/Product/CategoryDataResolver.php +++ b/Model/Product/CategoryDataResolver.php @@ -147,7 +147,7 @@ private function generateCategoryData(int $categoryId): void $this->categoryCache[$categoryId][self::CATEGORY_PATH] = $category->getName(); $this->categoryCache[$categoryId][self::CATEGORY_LEVEL_1] = $category->getName(); $this->categoryCache[$categoryId][self::CATEGORY_URL_1] = $this->removeBaseRoot($category->getUrl()); - $this->categoryCache[$categoryId][self::CATEGORY_IDS] = [$category->getId()]; + $this->categoryCache[$categoryId][self::CATEGORY_IDS] = [$category->getUrlKey()]; return; } @@ -216,15 +216,13 @@ private function generateCategoryDataWithLevel3(CategoryInterface $category, int $parentCategory->getUrl() ); $path[] = $parentCategory->getName(); - $ids[] = $parentCategory->getId(); } $this->categoryCache[$categoryId][self::CATEGORY_LEVEL_2] = $category->getName(); $this->categoryCache[$categoryId][self::CATEGORY_URL_2] = $this->removeBaseRoot($category->getUrl()); $path[] = $category->getName(); - $ids[] = $category->getId(); $this->categoryCache[$categoryId][self::CATEGORY_PATH] = $this->generatePath($path); - $this->categoryCache[$categoryId][self::CATEGORY_IDS] = $ids; + $this->categoryCache[$categoryId][self::CATEGORY_IDS] = [$category->getUrlKey()]; } /** @@ -236,7 +234,7 @@ private function generateCategoryDataWithLevel3(CategoryInterface $category, int */ private function generatePath(array $path): string { - return implode('>', $path); + return implode('|', $path); } /** @@ -251,11 +249,16 @@ private function generateCategoryDataWithLevelMore3(CategoryInterface $category, { $categoryId = $category->getId(); $parentCategories = $category->getParentCategories(); + $this->sortByLevel($parentCategories); $this->categoryCache[$categoryId][self::CATEGORY_LEVEL_3] = $category->getName(); $this->categoryCache[$categoryId][self::CATEGORY_URL_3] = $this->removeBaseRoot($category->getUrl()); $iterator = 1; foreach ($parentCategories as $parentCategory) { + if ($parentCategory->getLevel() < 2) { + continue; + } + if ($iterator > 2) { break; } @@ -266,16 +269,29 @@ private function generateCategoryDataWithLevelMore3(CategoryInterface $category, } $path[] = $parentCategory->getName(); - $ids[] = $parentCategory->getId(); $this->categoryCache[$categoryId][self::CATEGORY_LEVEL . $iterator] = $parentCategory->getName(); $this->categoryCache[$categoryId]['category_' . $iterator . '_url'] = $this->removeBaseRoot( $parentCategory->getUrl() ); $iterator++; } - $ids[] = $category->getId(); + $path[] = $category->getName(); $this->categoryCache[$categoryId][self::CATEGORY_PATH] = $this->generatePath($path); - $this->categoryCache[$categoryId][self::CATEGORY_IDS] = $ids; + $this->categoryCache[$categoryId][self::CATEGORY_IDS] = [$category->getUrlKey()]; + } + + /** + * Sort categories by level + * + * @param array $categories + * + * @return void + */ + private function sortByLevel(array &$categories): void + { + usort($categories, function ($a, $b) { + return $a->getLevel() <=> $b->getLevel(); + }); } } diff --git a/README.md b/README.md index 38d1f06..90e4b8f 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This is a module for integration with the [Bloomreach service](https://www.bloom ## Prerequisites -- Magento 2.3, 2.4 -- PHP 7.3, 7.4, 8.1, 8.2 +- Magento 2.4 +- PHP 7.3, 7.4, 8.1, 8.2, 8.3 ## Installation @@ -235,9 +235,7 @@ If the module was installed manually, then you need to do the following steps to - - Vendor\Name\Model\DataMapping\DataMapper\Custom - + Vendor\Name\Model\DataMapping\DataMapper\Custom @@ -250,9 +248,7 @@ If the module was installed manually, then you need to do the following steps to - - Vendor\Name\Model\DataMapping\FieldValueRenderer\CustomRenderer\EntityIdRenderer - + Vendor\Name\Model\DataMapping\FieldValueRenderer\CustomRenderer\EntityIdRenderer @@ -268,9 +264,7 @@ If the module was installed manually, then you need to do the following steps to - - Vendor\Name\Model\DataMapping\DataMapper\Product\Configurable - + Vendor\Name\Model\DataMapping\DataMapper\Product\Configurable @@ -285,9 +279,7 @@ If the module was installed manually, then you need to do the following steps to - - Vendor\Name\Model\DataMapping\FieldValueRenderer\Product\CustomRenderer - + Vendor\Name\Model\DataMapping\FieldValueRenderer\Product\CustomRenderer @@ -303,9 +295,7 @@ If the module was installed manually, then you need to do the following steps to - - Vendor\Name\Model\DataMapping\FieldValueRenderer\Product\Simple\EntityIdRenderer - + Vendor\Name\Model\DataMapping\FieldValueRenderer\Product\Simple\EntityIdRenderer @@ -321,9 +311,7 @@ If the module was installed manually, then you need to do the following steps to - - Vendor\Name\Model\DataMapping\FieldValueRenderer\Product\\EntityIdRenderer - + Vendor\Name\Model\DataMapping\FieldValueRenderer\Product\EntityIdRenderer @@ -416,9 +404,7 @@ If the module was installed manually, then you need to do the following steps to - - Magento\Catalog\Model\ResourceModel\Product\Collection - + Magento\Catalog\Model\ResourceModel\Product\Collection @@ -459,9 +445,7 @@ If the module was installed manually, then you need to do the following steps to - - Bloomreach\EngagementConnector\Model\Export\Transporter\InitialExport\DefaultTransporter - + Bloomreach\EngagementConnector\Model\Export\Transporter\InitialExport\DefaultTransporter @@ -478,9 +462,7 @@ If the module was installed manually, then you need to do the following steps to - - Bloomreach\EngagementConnector\Model\Export\Transporter\Event\CustomEntityTransporter - + Bloomreach\EngagementConnector\Model\Export\Transporter\Event\CustomEntityTransporter @@ -533,7 +515,7 @@ If the module was installed manually, then you need to do the following steps to 2. Create child block for `bloomreach.engagement.connector.tracking` in the layout. 3. Use `Bloomreach\EngagementConnector\Block\Tracking\Event` class for event block. 4. Pass your event class to event block via arguments with name `events`. -5. Use `Bloomreach_EngagementConnector::tracking/event/default.phtml` template for send event +5. Use `Bloomreach_EngagementConnector::tracking/event/default.phtml` template for send event. ```xml - - Bloomreach\EngagementConnector\Model\Tracking\Event\ProductPage\ViewItem - + Bloomreach\EngagementConnector\Model\Tracking\Event\ProductPage\ViewItem diff --git a/RELEASELOG.md b/RELEASELOG.md index 44425ca..1b2ef3c 100644 --- a/RELEASELOG.md +++ b/RELEASELOG.md @@ -1,3 +1,23 @@ +# v1.1.1 + +- Fixes + - Resolved issues with configuring the `Customers Feed` import + - Corrected the order of category levels + - Fixed incorrect value types for fields such as price, qty, and others + - Resolved an issue where `customer_id` and `email_id` were not tracked when a new customer was created +- Additions + - Added compatibility with `PHP 8.3` + - Added compatibility with Magento `2.4.7-p3` + - Introduced the ability to configure searchable fields for `Products Feed` and `Variants Feed` + - Added the `sku` field to the `Purchase Items Feed` +- Improvements + - Enhanced the `Export Queue Clean Up` cron job + - Enhanced the `Progress Log` in the Initial Import Grid + - Improved the module uninstallation process by ensuring module log files are deleted +- Changes + - Discontinued compatibility with Magento versions lower than `2.4.0` + - Country names are now tracked in English instead of being translated + # v1.1.0 - Fixed guest cart merging after logging in as customer diff --git a/Service/Address/GetCountryName.php b/Service/Address/GetCountryName.php index 0ea4802..b354308 100644 --- a/Service/Address/GetCountryName.php +++ b/Service/Address/GetCountryName.php @@ -43,7 +43,7 @@ public function execute(string $countryCode): string { if (!array_key_exists($countryCode, $this->countryCache)) { $country = $this->countryFactory->create()->loadByCode($countryCode); - $this->countryCache[$countryCode] = $country ? (string) $country->getName() : ''; + $this->countryCache[$countryCode] = $country ? (string) $country->getName('en_US') : ''; } return $this->countryCache[$countryCode]; diff --git a/Service/Cron/CleanExportQueueService.php b/Service/Cron/CleanExportQueueService.php index 4545b79..5cccdec 100644 --- a/Service/Cron/CleanExportQueueService.php +++ b/Service/Cron/CleanExportQueueService.php @@ -8,6 +8,7 @@ namespace Bloomreach\EngagementConnector\Service\Cron; use Bloomreach\EngagementConnector\Api\DeleteExportQueueInterface; +use Bloomreach\EngagementConnector\Logger\Debugger; use Bloomreach\EngagementConnector\Model\ExportQueueModel; use Bloomreach\EngagementConnector\Model\ResourceModel\ExportQueue\Collection; use Bloomreach\EngagementConnector\Model\ResourceModel\ExportQueue\CollectionFactory; @@ -21,6 +22,9 @@ */ class CleanExportQueueService { + private const TOTAL_ERRORS = 'total_errors'; + private const TOTAL_STATUS_COMPLETE = 'total_status_complete'; + private const TOTAL_STATUS_ERROR = 'total_status_error'; private const DAYS_CLEAN_EXPORT_QUEUE = 'bloomreach_engagement/bloomreach_engagement_cron/clean_export_queue'; @@ -49,25 +53,38 @@ class CleanExportQueueService */ private $logger; + /** + * @var Debugger + */ + private $debugLogger; + + /** + * @var array + */ + private $cleanUpResult = []; + /** * @param ScopeConfigInterface $scopeConfig * @param DateTime $date * @param CollectionFactory $collectionFactory * @param DeleteExportQueueInterface $deleteExportQueue * @param LoggerInterface $logger + * @param Debugger $debuggerLogger */ public function __construct( ScopeConfigInterface $scopeConfig, DateTime $date, CollectionFactory $collectionFactory, DeleteExportQueueInterface $deleteExportQueue, - LoggerInterface $logger + LoggerInterface $logger, + Debugger $debuggerLogger ) { $this->scopeConfig = $scopeConfig; $this->date = $date; $this->collectionFactory = $collectionFactory; $this->deleteExportQueue = $deleteExportQueue; $this->logger = $logger; + $this->debugLogger = $debuggerLogger; } /** @@ -77,29 +94,49 @@ public function execute(): void { $day = $this->scopeConfig->getValue(self::DAYS_CLEAN_EXPORT_QUEUE); - if ($day > 0) { - $timeEnd = strtotime($this->date->date()) - $day * 24 * 60 * 60; - $lastEntityId = 0; + if (!$day) { + $this->logger->info(__('Export queue cleanup is not configured')); - /** @var Collection $collection */ - $collection = $this->collectionFactory->create(); - $this->setFilters($collection, $timeEnd, $lastEntityId); + return; + } + + $this->debugLogger->log(__('Export queue cleanup started')); + $timeEnd = strtotime($this->date->date()) - $day * 24 * 60 * 60; + $lastEntityId = 0; - $collectionSize = $collection->getSize(); + /** @var Collection $collection */ + $collection = $this->collectionFactory->create(); + $this->setFilters($collection, $timeEnd, $lastEntityId); + $collectionSize = $collection->getSize(); - if (!$collectionSize) { - return; - } + if (!$collectionSize) { - $collection->setPageSize(100); - $lastPageNumber = $collection->getLastPageNumber(); + $this->debugLogger->log(__('Export queue cleanup complete. No records were deleted')); + return; + } - for ($currentPage = 1; $currentPage <= $lastPageNumber; $currentPage++) { - $collection->getSelect()->reset('where'); - $this->setFilters($collection, $timeEnd, $lastEntityId); - $lastEntityId = $this->deleteQueue($collection); - } + $collection->setPageSize(100); + $lastPageNumber = $collection->getLastPageNumber(); + + for ($currentPage = 1; $currentPage <= $lastPageNumber; $currentPage++) { + $collection->getSelect()->reset('where'); + $this->setFilters($collection, $timeEnd, $lastEntityId); + $lastEntityId = $this->deleteQueue($collection); } + + $this->debugLogger->log( + __( + 'Export queue cleanup complete.' + . ' Total records deleted with status "complete": "%total_complete_status".' + . ' Total records deleted with status "error": "%total_error_status".' + . ' Total errors encountered: "%total_errors"', + [ + 'total_complete_status' => $this->cleanUpResult[self::TOTAL_STATUS_COMPLETE] ?? 0, + 'total_error_status' => $this->cleanUpResult[self::TOTAL_STATUS_ERROR] ?? 0, + 'total_errors' => $this->cleanUpResult[self::TOTAL_ERRORS] ?? 0 + ] + ) + ); } /** @@ -116,10 +153,14 @@ private function deleteQueue(Collection $collection): int /** @var ExportQueueModel $item */ foreach ($collection as $item) { $lastEntityId = $item->getEntityId(); + $status = $item->getStatus(); + try { $this->deleteExportQueue->execute($item->getEntityId()); + $this->addDeletedToCleanupResult($status); } catch (Exception $e) { $this->logger->critical($e); + $this->addErrorToCleanupResult(); } } @@ -141,7 +182,40 @@ private function setFilters(Collection $collection, int $timeEnd, int $lastEntit { $collection ->addFieldToFilter(ExportQueueModel::CREATED_AT, ['lteq' => date('Y-m-d H:i:s', $timeEnd)]) - ->addFieldToFilter(ExportQueueModel::STATUS, ['eq' => ExportQueueModel::STATUS_COMPLETE]) + ->addFieldToFilter( + ExportQueueModel::STATUS, + ['in' => [ExportQueueModel::STATUS_COMPLETE, ExportQueueModel::STATUS_ERROR]] + ) ->addFieldToFilter(ExportQueueModel::ENTITY_ID, ['gt' => $lastEntityId]); } + + /** + * Add 1 error to cleanup results + * + * @return void + */ + private function addErrorToCleanupResult(): void + { + $totalErrors = $this->cleanUpResult[self::TOTAL_ERRORS] ?? 0; + $this->cleanUpResult[self::TOTAL_ERRORS] = $totalErrors + 1; + } + + /** + * Add 1 deleted to cleanup results + * + * @param int $status + * + * @return void + */ + private function addDeletedToCleanupResult(int $status): void + { + $key = self::TOTAL_STATUS_ERROR; + + if ($status === ExportQueueModel::STATUS_COMPLETE) { + $key = self::TOTAL_STATUS_COMPLETE; + } + + $value = $this->cleanUpResult[$key] ?? 0; + $this->cleanUpResult[$key] = $value + 1; + } } diff --git a/Service/InitialExportStatus/ExportQueue/UpdateProgress.php b/Service/InitialExportStatus/ExportQueue/UpdateProgress.php index 360a6de..b7fe9e6 100644 --- a/Service/InitialExportStatus/ExportQueue/UpdateProgress.php +++ b/Service/InitialExportStatus/ExportQueue/UpdateProgress.php @@ -100,7 +100,6 @@ private function update(ExportQueueInterface $exportQueue): void } $tempTotal = $initialExportStatus->getTotalExported() + $initialExportStatus->getTotalErrorItems(); - if ($initialExportStatus->getTotalItems() <= $tempTotal) { if ($initialExportStatus->getTotalErrorItems() === 0) { $initialExportStatus->setStatus(StatusSource::SUCCESS); diff --git a/Setup/Service/RemoveFiles.php b/Setup/Service/RemoveFiles.php index 2685650..e57e90b 100644 --- a/Setup/Service/RemoveFiles.php +++ b/Setup/Service/RemoveFiles.php @@ -9,6 +9,7 @@ use Bloomreach\EngagementConnector\Model\Export\File\DirectoryProvider; use Exception; +use Magento\Framework\App\Filesystem\DirectoryList as AppDirectoryList; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DirectoryList; use Magento\Framework\Filesystem\Driver\File as DriverFile; @@ -57,9 +58,38 @@ public function __construct( */ public function execute(): void { - $directory = DIRECTORY_SEPARATOR . DirectoryProvider::DEFAULT_BASE_DIRECTORY . DIRECTORY_SEPARATOR; - $mediaPath = $this->directoryList->getPath('media') . $directory; - $this->deleteDirectoryRecursively($mediaPath); + $this->deleteImportFiles(); + $this->deleteLogFiles(); + } + + /** + * Delete log files + * + * @return void + * @throws FileSystemException + */ + private function deleteLogFiles(): void + { + $this->deleteDirectoryRecursively( + $this->directoryList->getPath(AppDirectoryList::LOG) + . DIRECTORY_SEPARATOR + . DirectoryProvider::DEFAULT_BASE_DIRECTORY + ); + } + + /** + * Delete import files + * + * @return void + * @throws FileSystemException + */ + private function deleteImportFiles(): void + { + $this->deleteDirectoryRecursively( + $this->directoryList->getPath(AppDirectoryList::MEDIA) + . DIRECTORY_SEPARATOR + . DirectoryProvider::DEFAULT_BASE_DIRECTORY + ); } /** diff --git a/System/CatalogIdResolver.php b/System/CatalogIdResolver.php index 6898cc3..7776f0b 100644 --- a/System/CatalogIdResolver.php +++ b/System/CatalogIdResolver.php @@ -11,7 +11,7 @@ use Magento\Framework\Exception\LocalizedException; /** - * The class is responsible for returning import id for entity type + * The class is responsible for returning catalog id for entity type */ class CatalogIdResolver { diff --git a/System/ConfigProvider.php b/System/ConfigProvider.php index c84f759..e4ef541 100644 --- a/System/ConfigProvider.php +++ b/System/ConfigProvider.php @@ -243,6 +243,11 @@ class ConfigProvider */ public const REALTIME_UPDATE_TYPE = 'realtime_update'; + /** + * Searchable fields type + */ + public const SEARCHABLE_FIELDS = 'searchable_fields'; + /** * @var ScopeConfigInterface */ diff --git a/System/SearchableFieldsResolver.php b/System/SearchableFieldsResolver.php new file mode 100644 index 0000000..1bfad5f --- /dev/null +++ b/System/SearchableFieldsResolver.php @@ -0,0 +1,63 @@ +scopeConfig = $scopeConfig; + $this->configPathGetter = $configPathGetter; + } + + /** + * Get searchable fields by entity type + * + * @param string $entityType + * @param string $scopeType + * @param null|int|string $scopeCode + * + * @return array + * @throws LocalizedException + */ + public function get( + string $entityType, + $scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + $scopeCode = null + ): array { + $value = (string) $this->scopeConfig->getValue( + $this->configPathGetter->get($entityType, ConfigProvider::SEARCHABLE_FIELDS), + $scopeType, + $scopeCode + ); + + return $value ? explode(',', $value) : []; + } +} diff --git a/Test/Integration/Event/Customer/CustomerDeleteTest.php b/Test/Integration/Event/Customer/CustomerDeleteTest.php new file mode 100644 index 0000000..d5dbaf1 --- /dev/null +++ b/Test/Integration/Event/Customer/CustomerDeleteTest.php @@ -0,0 +1,130 @@ +deleteCustomer(); + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(0, $exportQueueCollection->getSize()); + } + + /** + * Real time customer delete event test + * + * @magentoAppIsolation enabled + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoAppArea adminhtml + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/customer.php + * + * @return void + * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function testRealTimeCustomerDelete(): void + { + $this->deleteCustomer(); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $exportQueueCollection->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, 'customer'); + $exportQueueCollection->addFieldToFilter(ExportQueueModel::API_TYPE, 'delete'); + + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getLastItem(); + $this->assertNotEmpty($exportQueueItem->getId()); + $body = $this->jsonSerializer->unserialize($exportQueueItem->getBody()); + $customerData = $body['customers'][0]['customer_ids'] ?? []; + $this->assertEquals(true, (bool) $exportQueueItem->getId()); + $this->assertEquals(ExportQueueModel::STATUS_NEW, $exportQueueItem->getStatus()); + $this->assertEquals(0, $exportQueueItem->getRetries()); + $this->assertEquals('customer@example.com', $customerData['email_id'] ?? ''); + } + + /** + * Delete customer + * + * @return void + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws InputMismatchException + */ + private function deleteCustomer(): void + { + $customer = $this->customerRepository->get('customer@example.com'); + $this->customerRepository->delete($customer); + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerRepository = $objectManager->create(CustomerRepositoryInterface::class); + $this->jsonSerializer = $objectManager->create(SerializerInterface::class); + $this->exportQueueCollectionFactory = $objectManager->create(CollectionFactory::class); + } +} diff --git a/Test/Integration/Event/Customer/CustomerUpdateTest.php b/Test/Integration/Event/Customer/CustomerUpdateTest.php index 3b2c6b0..c59402c 100644 --- a/Test/Integration/Event/Customer/CustomerUpdateTest.php +++ b/Test/Integration/Event/Customer/CustomerUpdateTest.php @@ -10,8 +10,11 @@ use Bloomreach\EngagementConnector\Model\ExportQueueModel; use Bloomreach\EngagementConnector\Model\ResourceModel\ExportQueue\Collection; use Bloomreach\EngagementConnector\Model\ResourceModel\ExportQueue\CollectionFactory; -use Exception; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\State\InputMismatchException; use Magento\Framework\Serialize\SerializerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -37,22 +40,48 @@ class CustomerUpdateTest extends TestCase private $exportQueueCollectionFactory; /** - * Customer update event test + * Customer update event test (real time update is disabled) * + * @magentoAppIsolation enabled * @magentoConfigFixture bloomreach_engagement/general/enable 1 * @magentoConfigFixture bloomreach_engagement/customer_feed/enabled 1 - * @magentoConfigFixture bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture bloomreach_engagement/customer_feed/real_time_updates 0 * @magentoAppArea adminhtml * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/customer.php * * @return void + * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException */ public function testCustomerUpdate(): void { - $customer = $this->customerRepository->get('customer@example.com'); - $customer->setFirstname('Veronica'); - $customer->setLastname('Costello'); - $this->customerRepository->save($customer); + $this->updateCustomerData(); + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(0, $exportQueueCollection->getSize()); + } + + /** + * Real time customer update event test + * + * @magentoAppIsolation enabled + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoAppArea adminhtml + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/customer.php + * + * @return void + * @throws InputException + * @throws InputMismatchException + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function testRealTimeCustomerUpdate(): void + { + $this->updateCustomerData(); /** @var Collection $exportQueueCollection */ $exportQueueCollection = $this->exportQueueCollectionFactory->create(); @@ -65,42 +94,41 @@ public function testCustomerUpdate(): void $this->assertEquals(true, (bool) $exportQueueItem->getId()); $body = $this->jsonSerializer->unserialize($exportQueueItem->getBody()); - $this->assertEquals(ExportQueueModel::STATUS_NEW, (int) $exportQueueItem->getStatus()); - $this->assertEquals(0, (int) $exportQueueItem->getRetries()); - $this->assertEquals('customer@example.com', $body['email']); - $this->assertEquals('Veronica', $body['first_name']); - $this->assertEquals('Costello', $body['last_name']); + $this->assertEquals(ExportQueueModel::STATUS_NEW, $exportQueueItem->getStatus()); + $this->assertEquals(0, $exportQueueItem->getRetries()); + $this->assertEquals('customer@example.com', $body['email'] ?? ''); + $this->assertEquals('Veronica', $body['first_name'] ?? ''); + $this->assertEquals('Costello', $body['last_name'] ?? ''); } /** - * Test set up + * Update customer data to trigger related logic * * @return void + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws InputMismatchException */ - protected function setUp(): void + private function updateCustomerData(): void { - parent::setUp(); - $objectManager = Bootstrap::getObjectManager(); - $this->customerRepository = $objectManager->create(CustomerRepositoryInterface::class); - $this->jsonSerializer = $objectManager->create(SerializerInterface::class); - $this->exportQueueCollectionFactory = $objectManager->create(CollectionFactory::class); + $customer = $this->customerRepository->get('customer@example.com'); + $customer->setFirstname('Veronica'); + $customer->setLastname('Costello'); + $this->customerRepository->save($customer); } /** - * Clean export queue + * Test set up * * @return void - * @throws Exception */ - protected function tearDown(): void + protected function setUp(): void { - parent::tearDown(); - /** @var Collection $exportQueueCollection */ - $exportQueueCollection = $this->exportQueueCollectionFactory->create(); - $exportQueueResourceModel = $exportQueueCollection->getResource(); - /** @var ExportQueueModel $exportQueueItem */ - foreach ($exportQueueCollection as $exportQueueItem) { - $exportQueueResourceModel->delete($exportQueueItem); - } + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerRepository = $objectManager->create(CustomerRepositoryInterface::class); + $this->jsonSerializer = $objectManager->create(SerializerInterface::class); + $this->exportQueueCollectionFactory = $objectManager->create(CollectionFactory::class); } } diff --git a/Test/Integration/Event/Order/PlaceOrderTest.php b/Test/Integration/Event/Order/PlaceOrderTest.php new file mode 100644 index 0000000..ccd32b2 --- /dev/null +++ b/Test/Integration/Event/Order/PlaceOrderTest.php @@ -0,0 +1,457 @@ +placeGuestOrder(); + $this->assertPlaceOrder(1, 1, 1); + } + + /** + * Covers case when registered customer places the order and all settings are enabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 1 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/customer.php + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testCustomerPlaceOrder(): void + { + $this->deleteCustomerFromExportQueue(); //Delete customer events from export queue + $this->placeCustomerOrder(); + $this->assertPlaceOrder(0, 1, 1); + } + + /** + * Covers case when guest customer places the order and feature is disabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 0 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 1 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testExtensionDisabled(): void + { + $this->placeGuestOrder(); + $this->assertPlaceOrder(0, 0, 0); + } + + /** + * Covers case when guest customer places the order and purchase feed is disabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 0 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 1 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testPurchaseFeedDisabled(): void + { + $this->placeGuestOrder(); + $this->assertPlaceOrder(1, 0, 1); + } + + /** + * Covers case when guest customer places the order and purchase item feed is disabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 0 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 1 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testPurchaseItemFeedDisabled(): void + { + $this->placeGuestOrder(); + $this->assertPlaceOrder(1, 1, 0); + } + + /** + * Covers case when guest customer places the order and purchase feed realtime update is disabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 0 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 1 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testPurchaseFeedRealTimeUpdateDisabled(): void + { + $this->placeGuestOrder(); + $this->assertPlaceOrder(1, 0, 1); + } + + /** + * Covers case when guest customer places the order and purchase item feed realtime update is disabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 0 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testPurchaseItemFeedRealTimeUpdateDisabled(): void + { + $this->placeGuestOrder(); + $this->assertPlaceOrder(1, 1, 0); + } + + /** + * Covers case when guest customer places the order and customer feed is disabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 0 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 1 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testCustomerFeedDisabled(): void + { + $this->placeGuestOrder(); + $this->assertPlaceOrder(0, 1, 1); + } + + /** + * Covers case when guest customer places the order and customer feed realtime update is disabled + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/real_time_updates 0 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/real_time_updates 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/real_time_updates 1 + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + */ + public function testCustomerRealTimeUpdateDisabled(): void + { + $this->placeGuestOrder(); + $this->assertPlaceOrder(0, 1, 1); + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->exportQueueResource = $this->objectManager->get(ExportQueueResource::class); + $this->cartManagement = $this->objectManager->get(CartManagementInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->quoteRepository = $this->objectManager->get(QuoteRepository::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + } + + /** + * Delete data after test + * + * @return void + */ + protected function tearDown(): void + { + parent::tearDown(); + + try { + /** @var ExportQueueInterface $exportQueue */ + foreach ($this->getExportQueueCollection()->getItems() as $exportQueue) { + $this->exportQueueResource->delete($exportQueue); + } + } catch (Exception $e) { + //Nothing to do + } + } + + /** + * Place customer order + * + * @return void + * @throws LocalizedException + */ + private function placeCustomerOrder(): void + { + $this->placeOrder( + (int) $this->cartManagement->createEmptyCartForCustomer( + $this->customerRepository->get('customer@example.com')->getId() + ) + ); + } + + /** + * Place guest customer order + * + * @return void + * @throws LocalizedException + */ + private function placeGuestOrder(): void + { + $this->placeOrder( + (int) $this->cartManagement->createEmptyCart() + ); + } + + /** + * Place an order + * + * @param int $quoteId + * + * @return void + * @throws LocalizedException + */ + private function placeOrder(int $quoteId): void + { + $product = $this->productRepository->get('simple'); + $product->setData('salable', true); + $quote = $this->quoteRepository->getActive($quoteId); + + if ($quote->getCustomerIsGuest()) { + $this->addGuestCustomerData($quote); + } + + $quote->addProduct($product); + $this->quoteRepository->save($quote); + $this->prepareQuote($quote); + $this->cartManagement->placeOrder($quote->getId()); + } + + /** + * Assert place order + * + * @param int $customerExpected + * @param int $purchaseExpected + * @param int $purchaseItemsExpected + * + * @return void + */ + private function assertPlaceOrder(int $customerExpected, int $purchaseExpected, int $purchaseItemsExpected): void + { + $this->assertEquals( + $customerExpected, + $this->getExportQueueCollection() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, Customer::ENTITY_TYPE) + ->addFieldToFilter(ExportQueueModel::API_TYPE, self::EXPORT_QUEUE_API_TYPE) + ->getSize() + ); + $this->assertEquals( + $purchaseExpected, + $this->getExportQueueCollection() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, Order::ENTITY_TYPE) + ->addFieldToFilter(ExportQueueModel::API_TYPE, self::EXPORT_QUEUE_API_TYPE) + ->getSize() + ); + $this->assertEquals( + $purchaseItemsExpected, + $this->getExportQueueCollection() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, OrderItem::ENTITY_TYPE) + ->addFieldToFilter(ExportQueueModel::API_TYPE, self::EXPORT_QUEUE_API_TYPE) + ->getSize() + ); + } + + /** + * Get export queue collection + * + * @return ExportQueueCollection + */ + private function getExportQueueCollection(): ExportQueueCollection + { + return $this->objectManager->create(ExportQueueCollection::class); + } + + /** + * Select shipping and select payment + * + * @param CartInterface $quote + * + * @return void + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function prepareQuote(CartInterface $quote): void + { + $quote->getBillingAddress()->addData($this->getCustomerAddress()); + $quote->getShippingAddress()->addData($this->getCustomerAddress()); + $quote->getShippingAddress() + ->setCollectShippingRates(true) + ->collectShippingRates() + ->setShippingMethod('flatrate_flatrate'); + $quote->setPaymentMethod('checkmo'); + $quote->setInventoryProcessed(false); + $quote->collectTotals(); + $this->quoteRepository->save($quote); + $quote->getPayment()->importData(['method' => 'checkmo']); + $this->quoteRepository->save($quote); + } + + /** + * Returns customer address for quote + * + * @return array + */ + private function getCustomerAddress(): array + { + return [ + 'firstname' => 'John', + 'lastname' => 'Doe', + 'street' => 'xxxxxx', + 'city' => 'xxxxxxx', + 'country_id' => 'US', + 'region' => 'Texas', + 'region_id' => '57', + 'postcode' => '85001', + 'telephone' => '52556542', + 'fax' => '3242322556', + 'save_in_address_book' => 0 + ]; + } + + /** + * Add guest customer data + * + * @param CartInterface $quote + * + * @return void + */ + private function addGuestCustomerData(CartInterface $quote): void + { + $quote->setCustomerEmail('john.doe@example.com'); + $quote->setCustomerFirstname('John'); + $quote->setCustomerLastname('Doe'); + } + + /** + * Delete customer from Export queue + * + * @return void + * @throws Exception + */ + private function deleteCustomerFromExportQueue(): void + { + $collection = $this->getExportQueueCollection() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, Customer::ENTITY_TYPE); + + foreach ($collection as $exportQueue) { + $this->exportQueueResource->delete($exportQueue); + } + } +} diff --git a/Test/Integration/Event/Product/ProductDeleteTest.php b/Test/Integration/Event/Product/ProductDeleteTest.php new file mode 100644 index 0000000..2111227 --- /dev/null +++ b/Test/Integration/Event/Product/ProductDeleteTest.php @@ -0,0 +1,346 @@ +productRepository = $objectManager->create(ProductRepositoryInterface::class); + $this->jsonSerializer = $objectManager->create(SerializerInterface::class); + $this->exportQueueCollectionFactory = $objectManager->create(CollectionFactory::class); + } + + /** + * Test simple product delete (real time related configs are disabled) + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testSimpleProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('simple'); + $this->productRepository->delete($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(0, $exportQueueCollection->getSize()); + } + + /** + * Test simple product delete: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is disabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductRealTimeSimpleProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('simple'); + $this->productRepository->delete($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(1, $exportQueueCollection->getSize()); + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getLastItem(); + $this->assertDeleteExportQueueItem($exportQueueItem, $product, 'catalog_product'); + } + + /** + * Test simple product delete: + * - catalog_product_feed real time is disabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductVariationRealTimeSimpleProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('simple'); + $this->productRepository->delete($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(1, $exportQueueCollection->getSize()); + + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getLastItem(); + $this->assertDeleteExportQueueItem($exportQueueItem, $product, 'catalog_product_variants'); + } + + /** + * Test simple product delete: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testRealTimeSimpleProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('simple'); + $this->productRepository->delete($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(2, $exportQueueCollection->getSize()); + + /** @var ExportQueueModel[] $items */ + $items = $exportQueueCollection->getItems(); + $this->assertDeleteExportQueueItem(array_shift($items), $product, 'catalog_product_variants'); + $this->assertDeleteExportQueueItem(array_shift($items), $product, 'catalog_product'); + } + + /** + * Test configurable product delete (real time related configs are disabled) + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testConfigurableProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('configurable'); + $this->productRepository->delete($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(0, $exportQueueCollection->getSize()); + } + + /** + * Test configurable product delete: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is disabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductRealTimeConfigurableProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('configurable'); + $this->productRepository->delete($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(1, $exportQueueCollection->getSize()); + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getLastItem(); + $this->assertDeleteExportQueueItem($exportQueueItem, $product, 'catalog_product'); + } + + /** + * Test configurable product delete: + * - catalog_product_feed real time is disabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductVariationRealTimeConfigurableProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('configurable'); + $this->productRepository->delete($product); + + $firstChild = $this->productRepository->get('simple_1'); + $secondChild = $this->productRepository->get('simple_2'); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(2, $exportQueueCollection->getSize()); + + /** @var ExportQueueModel[] $items */ + $items = $exportQueueCollection->getItems(); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $firstChild); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $secondChild); + } + + /** + * Test configurable product update: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws NoSuchEntityException + * @throws StateException + */ + public function testRealTimeConfigurableProductDelete(): void + { + // Delete product + $product = $this->productRepository->get('configurable'); + $this->productRepository->delete($product); + + $firstChild = $this->productRepository->get('simple_1'); + $secondChild = $this->productRepository->get('simple_2'); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(3, $exportQueueCollection->getSize()); + + /** @var ExportQueueModel[] $items */ + $items = $exportQueueCollection->getItems(); + $this->assertDeleteExportQueueItem(array_shift($items), $product, 'catalog_product'); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $firstChild); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $secondChild); + } + + /** + * Assert ExportQueueModel item data with 'delete' API type + * + * @param ExportQueueModel $exportQueueItem + * @param ProductInterface $product + * @param string $entityType + * + * @return void + */ + private function assertDeleteExportQueueItem( + ExportQueueModel $exportQueueItem, + ProductInterface $product, + string $entityType + ): void { + $this->assertEquals(ExportQueueModel::STATUS_NEW, $exportQueueItem->getStatus()); + $this->assertEquals($entityType, $exportQueueItem->getEntityType()); + $this->assertEquals('delete', $exportQueueItem->getApiType()); + $this->assertEquals(0, $exportQueueItem->getRetries()); + $this->assertEquals($exportQueueItem->getBody(), (string)$product->getId()); + } + + /** + * Assert ExportQueueModel item data with 'partial_update' API type + * + * @param ExportQueueModel $exportQueueItem + * @param ProductInterface $product + * + * @return void + */ + private function assertPartialUpdateExportQueueItem( + ExportQueueModel $exportQueueItem, + ProductInterface $product + ): void { + $body = $this->jsonSerializer->unserialize($exportQueueItem->getBody()); + $expectedBody = [ + 'item_id' => $product->getId(), + 'product_active' => 'FALSE' + ]; + $this->assertEquals(ExportQueueModel::STATUS_NEW, $exportQueueItem->getStatus()); + $this->assertEquals('catalog_product_variants', $exportQueueItem->getEntityType()); + $this->assertEquals('partial_update', $exportQueueItem->getApiType()); + $this->assertEquals(0, $exportQueueItem->getRetries()); + $this->assertEquals($expectedBody, $body); + } +} diff --git a/Test/Integration/Event/Product/ProductUpdateTest.php b/Test/Integration/Event/Product/ProductUpdateTest.php new file mode 100644 index 0000000..31faba8 --- /dev/null +++ b/Test/Integration/Event/Product/ProductUpdateTest.php @@ -0,0 +1,408 @@ +productRepository->get('simple'); + $product->setName('test'); + $this->productRepository->save($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(0, $exportQueueCollection->getSize()); + } + + /** + * Test simple product update: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is disabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductRealTimeSimpleProductUpdate(): void + { + // Update product name + $product = $this->productRepository->get('simple'); + $product->setName('test'); + $this->productRepository->save($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(1, $exportQueueCollection->getSize()); + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getLastItem(); + $this->assertEventExportQueueItem($exportQueueItem, $product, 'catalog_product'); + } + + /** + * Test simple product update: + * - catalog_product_feed real time is disabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductVariationRealTimeSimpleProductUpdate(): void + { + // Update product name + $product = $this->productRepository->get('simple'); + $product->setName('test'); + $this->productRepository->save($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(1, $exportQueueCollection->getSize()); + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getLastItem(); + $this->assertEventExportQueueItem($exportQueueItem, $product, 'catalog_product_variants'); + } + + /** + * Test simple product update: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/product_simple.php + * + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + */ + public function testRealTimeSimpleProductUpdate(): void + { + // Update product name + $product = $this->productRepository->get('simple'); + $product->setName('test'); + $this->productRepository->save($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(2, $exportQueueCollection->getSize()); + + /** @var ExportQueueModel[] $items */ + $items = $exportQueueCollection->getItems(); + $this->assertEventExportQueueItem(array_shift($items), $product, 'catalog_product_variants'); + $this->assertEventExportQueueItem(array_shift($items), $product, 'catalog_product'); + } + + /** + * Test configurable product update (real time related configs are disabled) + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + */ + public function testConfigurableProductUpdate(): void + { + // Update product name + $product = $this->productRepository->get('configurable'); + $product->setName('test'); + $this->productRepository->save($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(0, $exportQueueCollection->getSize()); + } + + /** + * Test configurable product update: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is disabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductRealTimeConfigurableProductUpdate(): void + { + // Update product name + $product = $this->productRepository->get('configurable'); + $product->setName('test'); + $this->productRepository->save($product); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(1, $exportQueueCollection->getSize()); + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getLastItem(); + $this->assertEventExportQueueItem($exportQueueItem, $product, 'catalog_product'); + } + + /** + * Test configurable product update: + * - catalog_product_feed real time is disabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + */ + public function testCatalogProductVariationRealTimeConfigurableProductUpdate(): void + { + // Update product status + $product = $this->productRepository->get('configurable'); + $product->setStatus(1); + $this->productRepository->save($product); + + $firstChild = $this->productRepository->get('simple_1'); + $secondChild = $this->productRepository->get('simple_2'); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(2, $exportQueueCollection->getSize()); + + /** @var ExportQueueModel[] $items */ + $items = $exportQueueCollection->getItems(); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $firstChild, 'FALSE'); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $secondChild, 'TRUE'); + } + + /** + * Test configurable product update: + * - catalog_product_feed real time is enabled, + * - catalog_product_variants_feed real time is enabled. + * + * @magentoConfigFixture bloomreach_engagement/general/enable 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/enabled 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/catalog_id 1 + * @magentoConfigFixture bloomreach_engagement/catalog_product_variants_feed/real_time_updates 1 + * + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/disabled_configurable_product_with_three_children.php + * + * @return void + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException + */ + public function testRealTimeConfigurableProductUpdate(): void + { + // Update product status + $product = $this->productRepository->get('configurable'); + $product->setStatus(1); + $this->productRepository->save($product); + + $firstChild = $this->productRepository->get('simple_1'); + $secondChild = $this->productRepository->get('simple_2'); + + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $this->assertEquals(3, $exportQueueCollection->getSize()); + + /** @var ExportQueueModel[] $items */ + $items = $exportQueueCollection->getItems(); + $this->assertEventExportQueueItem(array_shift($items), $product, 'catalog_product'); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $firstChild, 'FALSE'); + $this->assertPartialUpdateExportQueueItem(array_shift($items), $secondChild, 'TRUE'); + } + + /** + * Assert ExportQueueModel item data with 'event' API type + * + * @param ExportQueueModel $exportQueueItem + * @param ProductInterface $product + * @param string $entityType + * + * @return void + */ + private function assertEventExportQueueItem( + ExportQueueModel $exportQueueItem, + ProductInterface $product, + string $entityType + ): void { + $body = $this->jsonSerializer->unserialize($exportQueueItem->getBody()); + $this->assertEquals(ExportQueueModel::STATUS_NEW, $exportQueueItem->getStatus()); + $this->assertEquals($entityType, $exportQueueItem->getEntityType()); + $this->assertEquals('event', $exportQueueItem->getApiType()); + $this->assertEquals(0, $exportQueueItem->getRetries()); + $this->assertEquals((string)$product->getId(), $body['item_id'] ?? ''); + $this->assertEquals($product->getSku(), $body['sku'] ?? ''); + } + + /** + * Assert ExportQueueModel item data with 'partial_update' API type + * + * @param ExportQueueModel $exportQueueItem + * @param ProductInterface $product + * @param string $productActive + * + * @return void + */ + private function assertPartialUpdateExportQueueItem( + ExportQueueModel $exportQueueItem, + ProductInterface $product, + string $productActive + ): void { + $body = $this->jsonSerializer->unserialize($exportQueueItem->getBody()); + $expectedBody = [ + 'item_id' => $product->getId(), + 'product_active' => $productActive + ]; + $this->assertEquals(ExportQueueModel::STATUS_NEW, $exportQueueItem->getStatus()); + $this->assertEquals('catalog_product_variants', $exportQueueItem->getEntityType()); + $this->assertEquals('partial_update', $exportQueueItem->getApiType()); + $this->assertEquals(0, $exportQueueItem->getRetries()); + $this->assertEquals($expectedBody, $body); + } + + /** + * Test set up + * + * @return void + * @throws Exception + */ + protected function setUp(): void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $objectManager->create(ProductRepositoryInterface::class); + $this->jsonSerializer = $objectManager->create(SerializerInterface::class); + $this->exportQueueCollectionFactory = $objectManager->create(CollectionFactory::class); + $this->cleanExportQueue(); + } + + /** + * Clean export queue after each test + * + * @return void + * @throws Exception + */ + protected function tearDown(): void + { + parent::tearDown(); + $this->cleanExportQueue(); + } + + /** + * Clean export queue + * + * @return void + * @throws Exception + */ + private function cleanExportQueue(): void + { + /** @var Collection $exportQueueCollection */ + $exportQueueCollection = $this->exportQueueCollectionFactory->create(); + $exportQueueResourceModel = $exportQueueCollection->getResource(); + /** @var ExportQueueModel $exportQueueItem */ + foreach ($exportQueueCollection as $exportQueueItem) { + $exportQueueResourceModel->delete($exportQueueItem); + } + } +} diff --git a/Test/Integration/InitialImport/Action/AbstractActionTestCase.php b/Test/Integration/InitialImport/Action/AbstractActionTestCase.php new file mode 100644 index 0000000..f19d693 --- /dev/null +++ b/Test/Integration/InitialImport/Action/AbstractActionTestCase.php @@ -0,0 +1,242 @@ +expectException(NoSuchEntityException::class); + $this->expectExceptionMessage('Entity Type with code ' . $entityType . ' does not exists'); + $this->executeAction($entityType); + } + + /** + * Test validation exception + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @dataProvider initialImportStatusDataProvider + * + * @param string $statusLabel + * @param int $status + * + * @return void + * @throws LocalizedException + */ + public function testValidationException(string $statusLabel, int $status): void + { + $initialExportStatus = $this->initialExportStatusGetter->execute(DefaultType::ENTITY_TYPE); + $initialExportStatus->setStatus($status); + $this->saveInitialExportStatus->execute($initialExportStatus); + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, $statusLabel); + } + + /** + * Data provider + * + * @return array[] + */ + public function initialImportStatusDataProvider(): array + { + return [ + [ + 'statusLabel' => 'Scheduled', + 'status' => StatusSource::SCHEDULED + ], + [ + 'statusLabel' => 'Processing', + 'status' => StatusSource::PROCESSING + ], + [ + 'statusLabel' => 'Error', + 'status' => StatusSource::ERROR + ], + [ + 'statusLabel' => 'Success', + 'status' => StatusSource::SUCCESS + ] + ]; + } + + /** + * Data provider + * + * @return array[] + */ + public function entityDataProvider(): array + { + return [ + [ + 'entityType' => DefaultType::ENTITY_TYPE, + 'label' => 'Product Feed' + ], + [ + 'entityType' => ProductVariantsType::ENTITY_TYPE, + 'label' => 'Product Variants Feed' + ], + [ + 'entityType' => Customer::ENTITY_TYPE, + 'label' => 'Customer Feed' + ], + [ + 'entityType' => Order::ENTITY_TYPE, + 'label' => 'Purchase Feed' + ], + [ + 'entityType' => OrderItem::ENTITY_TYPE, + 'label' => 'Purchase Items Feed' + ] + ]; + } + + /** + * Assert validation exception + * + * @param string $entityType + * @param string $statusLabel + * + * @return void + */ + protected function validationExceptionTest(string $entityType, string $statusLabel): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage($this->getValidationExceptionMessage($statusLabel)); + $this->executeAction($entityType); + } + + /** + * Execute action + * + * @param string $entityType + * + * @return void + */ + abstract protected function executeAction(string $entityType): void; + + /** + * Get validation exception message + * + * @param string $statusLabel + * + * @return string + */ + abstract protected function getValidationExceptionMessage(string $statusLabel): string; + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->configureObjectManager(); + $this->initialExportStatusGetter = $this->objectManager->get(InitialExportStatusGetter::class); + $this->saveInitialExportStatus = $this->objectManager->get(SaveInitialExportStatusInterface::class); + $this->deleteInitialExportStatus = $this->objectManager->get(DeleteInitialExportStatusInterface::class); + $this->scopeConfig = $this->objectManager->get(ScopeConfigInterface::class); + $this->entityType = $this->objectManager->get(EntityType::class); + } + + /** + * Delete data after test + * + * @return void + */ + protected function tearDown(): void + { + parent::tearDown(); + + try { + foreach ($this->entityType->getAllTypes() as $entityType) { + $this->deleteInitialExportStatus->execute($entityType); + } + } catch (Exception $e) { + //Nothing to do + } + } + + /** + * Configure object Manager + * + * @return void + */ + protected function configureObjectManager(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->configure( + [ + IsEntityTypeFeedEnabled::class => [ + 'arguments' => [ + 'useCache' => false + ] + ] + ] + ); + } +} diff --git a/Test/Integration/InitialImport/Action/ConfigureTest.php b/Test/Integration/InitialImport/Action/ConfigureTest.php new file mode 100644 index 0000000..4348d75 --- /dev/null +++ b/Test/Integration/InitialImport/Action/ConfigureTest.php @@ -0,0 +1,192 @@ +enableInitialExport->execute($entityType); + $this->scopeConfig->clean(); + $this->executeAction($entityType); + $this->scopeConfig->clean(); + $this->assertEquals( + StatusSource::READY, + $this->initialExportStatusGetter->execute($entityType)->getStatus() + ); + + if (!in_array($entityType, self::CATALOG_TYPES, true)) { + $this->expectException(LocalizedException::class); + } + + $this->assertNotEmpty($this->catalogIdResolver->getCatalogId($entityType)); + } + + /** + * Test no items exception + * + * @return void + * + * @magentoConfigFixture default/bloomreach_engagement/customer_feed/enabled 1 + * @throws LocalizedException + */ + public function testNoItemsException(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage(sprintf('You have no items in Customer Feed. Create one and try again')); + $this->executeAction(Customer::ENTITY_TYPE); + } + + /** + * Test validation exception when import status is "Disabled" + * + * @return void + */ + public function testValidationExceptionWithDisabledStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Disabled'); + } + + /** + * Test validation exception when import status is "Ready" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * + * @return void + */ + public function testValidationExceptionWithReadyStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Ready'); + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->enableInitialExport = $this->objectManager->get(EnableInitialExport::class); + $this->configureInitialExport = $this->objectManager->get(Configure::class); + $this->catalogIdResolver = $this->objectManager->get(CatalogIdResolver::class); + } + + /** + * Configure object manager + * + * @return void + */ + protected function configureObjectManager(): void + { + parent::configureObjectManager(); + $createImportMock = $this->getMockBuilder(CreateImport::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ConfigProvider::class), + $this->objectManager->get(RequestSender::class), + $this->objectManager->get(ResponseValidator::class) + ] + ) + ->getMock(); + $createImportMock->expects($this->any())->method('execute')->willReturn(uniqid()); + $createCatalogMock = $this->getMockBuilder(CreateCatalog::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ConfigProvider::class), + $this->objectManager->get(RequestSender::class), + $this->objectManager->get(ResponseValidator::class) + ] + ) + ->getMock(); + $createCatalogMock->expects($this->any())->method('execute')->willReturn(uniqid()); + $this->objectManager->addSharedInstance($createImportMock, CreateImport::class); + $this->objectManager->addSharedInstance($createCatalogMock, CreateCatalog::class); + } + + /** + * Execute configure action + * + * @param string $entityType + * + * @return void + * @throws LocalizedException + */ + protected function executeAction(string $entityType): void + { + $this->configureInitialExport->execute($entityType); + } + + /** + * Get validation exception message + * + * @param string $statusLabel + * + * @return string + */ + protected function getValidationExceptionMessage(string $statusLabel): string + { + return 'Import is already configured. Current import status: ' . $statusLabel; + } +} diff --git a/Test/Integration/InitialImport/Action/EnableTest.php b/Test/Integration/InitialImport/Action/EnableTest.php new file mode 100644 index 0000000..408d5bd --- /dev/null +++ b/Test/Integration/InitialImport/Action/EnableTest.php @@ -0,0 +1,104 @@ +executeAction(DefaultType::ENTITY_TYPE); + $this->scopeConfig->clean(); + $this->assertEquals( + StatusSource::NOT_READY, + $this->initialExportStatusGetter->execute(DefaultType::ENTITY_TYPE)->getStatus() + ); + } + + /** + * Test validation exception when import status is "Not Ready" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * + * @return void + * @throws LocalizedException + */ + public function testValidationExceptionWithNotReadyStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Not Ready'); + } + + /** + * Test validation exception when import status is "Ready" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * + * @return void + */ + public function testValidationExceptionWithReadyStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Ready'); + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->enableInitialExport = $this->objectManager->get(EnableInitialExport::class); + } + + /** + * Execute enable action + * + * @param string $entityType + * + * @return void + * @throws LocalizedException + */ + protected function executeAction(string $entityType): void + { + $this->enableInitialExport->execute($entityType); + } + + /** + * Get validation exception message + * + * @param string $statusLabel + * + * @return string + */ + protected function getValidationExceptionMessage(string $statusLabel): string + { + return 'Import is already enabled. Current import status: ' . $statusLabel; + } +} diff --git a/Test/Integration/InitialImport/Action/FlushTest.php b/Test/Integration/InitialImport/Action/FlushTest.php new file mode 100644 index 0000000..ef8bdd8 --- /dev/null +++ b/Test/Integration/InitialImport/Action/FlushTest.php @@ -0,0 +1,260 @@ +objectManager->create(ExportQueueCollection::class); + //Add items to the export queue + $this->addInitialExportDataToExportQueue->execute($entityType, 'test-data', 100, []); + $this->addInitialExportDataToExportQueue->execute(Customer::ENTITY_TYPE, 'test-data', 100, []); + $this->assertEquals( + 2, + $exportQueueCollection->getSize() + ); + //Create import + $this->enableAction->execute($entityType); + $this->scopeConfig->clean(); + $this->configureAction->execute($entityType); + $this->scopeConfig->clean(); + //Set initial import status + $initialExportStatus = $this->initialExportStatusGetter->execute($entityType); + $initialExportStatus->setStatus(StatusSource::SUCCESS); + $this->saveInitialExportStatus->execute($initialExportStatus); + $this->scopeConfig->clean(); + //Execute flush action + $this->executeAction($entityType); + $this->scopeConfig->clean(); + $this->assertEquals( + StatusSource::NOT_READY, + $this->initialExportStatusGetter->execute(DefaultType::ENTITY_TYPE)->getStatus() + ); + $this->assertEquals( + 1, + $exportQueueCollection->clear()->getSize() + ); + $this->assertEquals( + 0, + $exportQueueCollection + ->clear() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, $entityType) + ->getSize() + ); + } + + /** + * Test validation exception when import status is "Disabled" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 0 + * + * @return void + */ + public function testValidationExceptionWithDisabledStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Disabled'); + } + + /** + * Test validation exception when import status is "Not Ready" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * + * @return void + */ + public function testValidationExceptionWithNotReadyStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Not Ready'); + } + + /** + * Skip test execution + * + * @dataProvider initialImportStatusDataProvider + * + * @param string $statusLabel + * @param int $status + * + * @return void + */ + public function testValidationException(string $statusLabel, int $status): void + { + $this->assertIsInt($status); + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->enableAction = $this->objectManager->get(Enable::class); + $this->configureAction = $this->objectManager->get(Configure::class); + $this->flushAction = $this->objectManager->get(Flush::class); + $this->exportQueueResource = $this->objectManager->create(ExportQueueResource::class); + $this->addInitialExportDataToExportQueue = $this->objectManager->get( + AddInitialExportDataToExportQueue::class + ); + } + + /** + * Configure object manager + * + * @return void + */ + protected function configureObjectManager(): void + { + parent::configureObjectManager(); + $createImportMock = $this->getMockBuilder(CreateImport::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ConfigProvider::class), + $this->objectManager->get(RequestSender::class), + $this->objectManager->get(ResponseValidator::class) + ] + ) + ->getMock(); + $createImportMock->expects($this->any())->method('execute')->willReturn(uniqid()); + $this->objectManager->addSharedInstance($createImportMock, CreateImport::class); + $createCatalogMock = $this->getMockBuilder(CreateCatalog::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ConfigProvider::class), + $this->objectManager->get(RequestSender::class), + $this->objectManager->get(ResponseValidator::class) + ] + ) + ->getMock(); + $createCatalogMock->expects($this->any())->method('execute')->willReturn(uniqid()); + $this->objectManager->addSharedInstance($createCatalogMock, CreateCatalog::class); + $deleteImportMock = $this->getMockBuilder(DeleteImport::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ConfigProvider::class), + $this->objectManager->get(RequestSender::class), + $this->objectManager->get(ResponseValidator::class) + ] + ) + ->getMock(); + $deleteImportMock->expects($this->any())->method('execute')->willReturn(true); + $this->objectManager->addSharedInstance($deleteImportMock, DeleteImport::class); + $deleteCatalogMock = $this->getMockBuilder(DeleteCatalog::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ConfigProvider::class), + $this->objectManager->get(RequestSender::class), + $this->objectManager->get(ResponseValidator::class) + ] + ) + ->getMock(); + $deleteCatalogMock->expects($this->any())->method('execute')->willReturn(true); + $this->objectManager->addSharedInstance($deleteCatalogMock, DeleteCatalog::class); + } + + /** + * Execute enable action + * + * @param string $entityType + * + * @return void + * @throws LocalizedException + */ + protected function executeAction(string $entityType): void + { + $this->flushAction->execute($entityType); + } + + /** + * Get validation exception message + * + * @param string $statusLabel + * + * @return string + */ + protected function getValidationExceptionMessage(string $statusLabel): string + { + return 'Unable to flush import. Current import status: ' . $statusLabel; + } + + /** + * Delete data after test + * + * @return void + */ + protected function tearDown(): void + { + parent::tearDown(); + + try { + foreach ($this->objectManager->create(ExportQueueCollection::class)->getItems() as $exportQueue) { + $this->exportQueueResource->delete($exportQueue); + } + } catch (Exception $e) { + //Nothing to do + } + } +} diff --git a/Test/Integration/InitialImport/Action/StartTest.php b/Test/Integration/InitialImport/Action/StartTest.php new file mode 100644 index 0000000..6c42b4f --- /dev/null +++ b/Test/Integration/InitialImport/Action/StartTest.php @@ -0,0 +1,97 @@ +executeAction(DefaultType::ENTITY_TYPE); + $this->assertEquals( + StatusSource::SCHEDULED, + $this->initialExportStatusGetter->execute(DefaultType::ENTITY_TYPE)->getStatus() + ); + } + + /** + * Test validation exception when import status is "Disabled" + * + * @return void + */ + public function testValidationExceptionWithDisabledStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Disabled'); + } + + /** + * Test validation exception when import status is "Not Ready" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * + * @return void + */ + public function testValidationExceptionWithNotReadyStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Not Ready'); + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->startAction = $this->objectManager->get(Start::class); + } + + /** + * Execute action + * + * @param string $entityType + * + * @return void + */ + protected function executeAction(string $entityType): void + { + $this->startAction->execute($entityType); + } + + /** + * Get validation exception message + * + * @param string $statusLabel + * + * @return string + */ + protected function getValidationExceptionMessage(string $statusLabel): string + { + return 'Unable to start import. Current import status: ' . $statusLabel; + } +} diff --git a/Test/Integration/InitialImport/Action/StopTest.php b/Test/Integration/InitialImport/Action/StopTest.php new file mode 100644 index 0000000..470c24e --- /dev/null +++ b/Test/Integration/InitialImport/Action/StopTest.php @@ -0,0 +1,215 @@ +objectManager->create(ExportQueueCollection::class); + //Add items to the export queue + $this->addInitialExportDataToExportQueue->execute($entityType, 'test-data', 100, []); + $this->addInitialExportDataToExportQueue->execute(Customer::ENTITY_TYPE, 'test-data', 100, []); + $this->assertEquals( + 2, + $exportQueueCollection->getSize() + ); + + //Set initial import status + $initialExportStatus = $this->initialExportStatusGetter->execute($entityType); + $initialExportStatus->setStatus($status); + $this->saveInitialExportStatus->execute($initialExportStatus); + //Execute action + $this->executeAction($entityType); + $this->assertEquals( + StatusSource::READY, + $this->initialExportStatusGetter->execute($entityType)->getStatus() + ); + $this->assertEquals( + 1, + $exportQueueCollection->clear()->getSize() + ); + $this->assertEquals( + 0, + $exportQueueCollection + ->clear() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, $entityType) + ->getSize() + ); + } + + /** + * Test validation exception when import status is "Disabled" + * + * @return void + */ + public function testValidationExceptionWithDisabledStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Disabled'); + } + + /** + * Test validation exception when import status is "Not Ready" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * + * @return void + */ + public function testValidationExceptionWithNotReadyStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Not Ready'); + } + + /** + * Test validation exception when import status is "Ready" + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * + * @return void + */ + public function testValidationExceptionWithReadyStatus(): void + { + $this->validationExceptionTest(DefaultType::ENTITY_TYPE, 'Ready'); + } + + /** + * Data provider + * + * @return array[] + */ + public function initialImportStatusDataProvider(): array + { + return [ + [ + 'statusLabel' => 'Error', + 'status' => StatusSource::ERROR + ], + [ + 'statusLabel' => 'Success', + 'status' => StatusSource::SUCCESS + ] + ]; + } + + /** + * In-progress statuses + * + * @return array[] + */ + public function inProgressStatusDataProvider(): array + { + return [ + [ + 'status' => StatusSource::SCHEDULED + ], + [ + 'status' => StatusSource::PROCESSING + ] + ]; + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->stopAction = $this->objectManager->get(Stop::class); + $this->exportQueueResource = $this->objectManager->create(ExportQueueResource::class); + $this->addInitialExportDataToExportQueue = $this->objectManager->get( + AddInitialExportDataToExportQueue::class + ); + } + + /** + * Execute action + * + * @param string $entityType + * + * @return void + * @throws LocalizedException + */ + protected function executeAction(string $entityType): void + { + $this->stopAction->execute($entityType); + } + + /** + * Get validation exception message + * + * @param string $statusLabel + * + * @return string + */ + protected function getValidationExceptionMessage(string $statusLabel): string + { + return 'Unable to stop import. Current import status: ' . $statusLabel; + } + + /** + * Delete data after test + * + * @return void + */ + protected function tearDown(): void + { + parent::tearDown(); + + try { + foreach ($this->objectManager->create(ExportQueueCollection::class)->getItems() as $exportQueue) { + $this->exportQueueResource->delete($exportQueue); + } + } catch (Exception $e) { + //Nothing to do + } + } +} diff --git a/Test/Integration/Model/Export/AbstractProcessorTestCase.php b/Test/Integration/Model/Export/AbstractProcessorTestCase.php new file mode 100644 index 0000000..9d1c466 --- /dev/null +++ b/Test/Integration/Model/Export/AbstractProcessorTestCase.php @@ -0,0 +1,161 @@ +initialExportStatusGetter->execute($entityType); + $initialExportStatus->setStatus($status); + $initialExportStatus->setIsLocked($isLocked); + $initialExportStatus->setTotalItems($totalItems); + $this->saveInitialExportStatus->execute($initialExportStatus); + } + + /** + * Get export queue collection + * + * @return ExportQueueCollection + */ + protected function getExportQueueCollection(): ExportQueueCollection + { + return $this->objectManager->create(ExportQueueCollection::class); + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + $this->configureObjectManager(); + $this->initialExportStatusGetter = $this->objectManager->get(InitialExportStatusGetter::class); + $this->saveInitialExportStatus = $this->objectManager->get(SaveInitialExportStatusInterface::class); + $this->exportQueueResource = $this->objectManager->get(ExportQueueResource::class); + $this->deleteInitialExportStatus = $this->objectManager->get(DeleteInitialExportStatusInterface::class); + $this->entityType = $this->objectManager->get(EntityType::class); + } + + /** + * Configure Object Manager + * + * @return void + */ + protected function configureObjectManager(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Delete data after test + * + * @return void + */ + protected function tearDown(): void + { + parent::tearDown(); + $this->cleanUpInitialImportStatus(); + $this->cleanUpExportQueue(); + } + + /** + * Delete data after test + * + * @return void + */ + private function cleanUpInitialImportStatus(): void + { + parent::tearDown(); + + try { + foreach ($this->entityType->getAllTypes() as $entityType) { + $this->deleteInitialExportStatus->execute($entityType); + } + } catch (\Exception $e) { + //Nothing to do + } + } + + /** + * Clean up export queue + * + * @return void + */ + private function cleanUpExportQueue(): void + { + try { + foreach ($this->getExportQueueCollection()->getItems() as $exportQueue) { + $this->exportQueueResource->delete($exportQueue); + } + } catch (\Exception $e) { + //Nothing to do + } + } +} diff --git a/Test/Integration/Model/Export/ExportProcessorTest.php b/Test/Integration/Model/Export/ExportProcessorTest.php new file mode 100644 index 0000000..1c32baa --- /dev/null +++ b/Test/Integration/Model/Export/ExportProcessorTest.php @@ -0,0 +1,482 @@ +createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::PROCESSING, false, 1); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + AddInitialExportDataToExportQueue::API_TYPE, + ExportQueueModel::STATUS_NEW, + 0, + time() - 100000, + 1 + ); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + self::EVENT_API_TYPE, + ExportQueueModel::STATUS_NEW, + 0 + ); + $this->exportProcessor->process(); + $exportQueueItem = $this->getExportQueueItem( + DefaultType::ENTITY_TYPE, + AddInitialExportDataToExportQueue::API_TYPE + ); + $this->assertExportQueueItem($exportQueueItem, ExportQueueModel::STATUS_COMPLETE, 1); + $exportQueueItem = $this->getExportQueueItem(DefaultType::ENTITY_TYPE, self::EVENT_API_TYPE); + $this->assertExportQueueItem($exportQueueItem, ExportQueueModel::STATUS_NEW, 0); + $initialExportStatus = $this->initialExportStatusGetter->execute(DefaultType::ENTITY_TYPE); + $this->assertEquals(StatusSource::SUCCESS, $initialExportStatus->getStatus()); + $this->assertEquals(1, $initialExportStatus->getTotalExported()); + } + + /** + * Covers success event export + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSuccessEventExport(): void + { + $this->createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::SUCCESS, false, 1); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + AddInitialExportDataToExportQueue::API_TYPE, + ExportQueueModel::STATUS_COMPLETE, + 1 + ); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + self::EVENT_API_TYPE, + ExportQueueModel::STATUS_NEW, + 0 + ); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + self::EVENT_API_TYPE, + ExportQueueModel::STATUS_ERROR, + 1, + time() - 10000 + ); + $this->exportProcessor->process(); + $exportQueueItems = $this->getExportQueueItems(DefaultType::ENTITY_TYPE, self::EVENT_API_TYPE); + + $retries = 1; + + foreach ($exportQueueItems->getItems() as $exportQueueItem) { + $this->assertExportQueueItem($exportQueueItem, ExportQueueModel::STATUS_COMPLETE, $retries++); + } + } + + /** + * Covers case when Initial Import is locked + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfInitialImportIsLocked(): void + { + $this->createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::PROCESSING, true); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + self::EVENT_API_TYPE, + ExportQueueModel::STATUS_NEW, + 0 + ); + $this->exportProcessor->process(); + $exportQueueItem = $this->getExportQueueItem(DefaultType::ENTITY_TYPE, self::EVENT_API_TYPE); + $this->assertExportQueueItem($exportQueueItem, ExportQueueModel::STATUS_NEW, 0); + } + + /** + * Covers case when retry count exceeded + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfRetryCountExceeded(): void + { + $this->createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::SUCCESS); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + self::EVENT_API_TYPE, + ExportQueueModel::STATUS_ERROR, + 5 + ); + $this->exportProcessor->process(); + $exportQueueItem = $this->getExportQueueItem(DefaultType::ENTITY_TYPE, self::EVENT_API_TYPE); + $this->assertExportQueueItem($exportQueueItem, ExportQueueModel::STATUS_ERROR, 5); + } + + /** + * Covers case when sending attempt time is not reached + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfTimeOfSendingAttemptTimeIsNotReached(): void + { + $this->createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::SUCCESS); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + self::EVENT_API_TYPE, + ExportQueueModel::STATUS_ERROR, + 2, + time() + 10000 + ); + $this->exportProcessor->process(); + $exportQueueItem = $this->getExportQueueItem(DefaultType::ENTITY_TYPE, self::EVENT_API_TYPE); + $this->assertExportQueueItem($exportQueueItem, ExportQueueModel::STATUS_ERROR, 2); + } + + /** + * Covers case when item status is not allowed + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * @dataProvider notAllowedStatusesDataProvider + * + * @param int $status + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfStatusIsNotAllowed(int $status): void + { + $this->createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::SUCCESS); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + self::EVENT_API_TYPE, + $status, + 2, + time() - 10000 + ); + $this->exportProcessor->process(); + $exportQueueItem = $this->getExportQueueItem(DefaultType::ENTITY_TYPE, self::EVENT_API_TYPE); + $this->assertExportQueueItem($exportQueueItem, $status, 2); + } + + /** + * Covers case when feed is disabled + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 0 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfCsvExportAndFeedIsDisabled(): void + { + $this->assertExportItemSendingIsNotAllowed(AddInitialExportDataToExportQueue::API_TYPE); + } + + /** + * Covers case when import ID is empty + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfCsvExportAndImportIdIsEmpty(): void + { + $this->assertExportItemSendingIsNotAllowed(AddInitialExportDataToExportQueue::API_TYPE); + } + + /** + * Covers case when feed is disabled + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 0 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfEventExportAndFeedIsDisabled(): void + { + $this->assertExportItemSendingIsNotAllowed(self::EVENT_API_TYPE); + } + + /** + * Covers case when Import ID is empty + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfEventExportAndImportIdIsEmpty(): void + { + $this->assertExportItemSendingIsNotAllowed(self::EVENT_API_TYPE); + } + + /** + * Covers case when Catalog ID is empty + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 1 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfEventExportAndCatalogIdIsEmpty(): void + { + $this->assertExportItemSendingIsNotAllowed(self::EVENT_API_TYPE); + } + + /** + * Covers case when Real time update is disabled + * + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/catalog_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/catalog_product_feed/real_time_updates 0 + * + * @return void + * @throws LocalizedException + */ + public function testSkipIfEventExportAndRealTimeUpdateIsDisabled(): void + { + $this->assertExportItemSendingIsNotAllowed(self::EVENT_API_TYPE); + } + + /** + * Not allowed statuses data provider + * + * @return array[] + */ + public function notAllowedStatusesDataProvider(): array + { + return [ + [ + 'status' => ExportQueueModel::STATUS_COMPLETE + ], + [ + 'status' => ExportQueueModel::STATUS_IN_PROGRESS + ] + ]; + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->exportProcessor = $this->objectManager->get(ExportProcessor::class); + $this->saveExportQueue = $this->objectManager->get(SaveExportQueueInterface::class); + } + + /** + * Configure object manager + * + * @return void + */ + protected function configureObjectManager(): void + { + parent::configureObjectManager(); + $this->objectManager->configure( + [ + IsRealTimeUpdateAllowed::class => [ + 'arguments' => [ + 'useCache' => false + ] + ] + ] + ); + $transporterMock = $this->getMockBuilder(TransporterResolver::class) + ->setConstructorArgs( + [ + [] + ] + ) + ->getMock(); + $transporterMock->expects($this->any())->method('send')->willReturn(true); + $this->objectManager->addSharedInstance($transporterMock, TransporterResolver::class); + } + + /** + * Assert Export Item sending is not allowed + * + * @param string $apiType + * + * @return void + * @throws LocalizedException + */ + private function assertExportItemSendingIsNotAllowed(string $apiType): void + { + $this->createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::SUCCESS); + $this->createExportQueueItem( + DefaultType::ENTITY_TYPE, + $apiType, + ExportQueueModel::STATUS_NEW, + 0 + ); + $this->exportProcessor->process(); + $exportQueueItem = $this->getExportQueueItem(DefaultType::ENTITY_TYPE, $apiType); + $this->assertExportQueueItem($exportQueueItem, ExportQueueModel::STATUS_NEW, 0); + } + + /** + * Assert export queue item + * + * @param ExportQueueInterface $exportQueueItem + * @param int $status + * @param int $retries + * + * @return void + */ + private function assertExportQueueItem(ExportQueueInterface $exportQueueItem, int $status, int $retries): void + { + $this->assertEquals( + $status, + $exportQueueItem->getStatus(), + ); + $this->assertEquals( + $retries, + $exportQueueItem->getRetries() + ); + } + + /** + * Get export queue items + * + * @param string $entityType + * @param string $apiType + * + * @return Collection + */ + private function getExportQueueItems(string $entityType, string $apiType): Collection + { + return $this->getExportQueueCollection() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, $entityType) + ->addFieldToFilter(ExportQueueModel::API_TYPE, $apiType); + } + + /** + * Get export queue items + * + * @param string $entityType + * @param string $apiType + * + * @return ExportQueueInterface + */ + private function getExportQueueItem(string $entityType, string $apiType): ExportQueueInterface + { + return $this->getExportQueueItems($entityType, $apiType)->getFirstItem(); + } + + /** + * Create export queue item + * + * @param string $entityType + * @param string $apiType + * @param int $status + * @param int $retries + * @param int $timeOfNextSending + * @param int $numberOfItems + * + * @return void + * @throws CouldNotSaveException + */ + private function createExportQueueItem( + string $entityType, + string $apiType, + int $status, + int $retries, + int $timeOfNextSending = 0, + int $numberOfItems = 0 + ): void { + $exportQueueItem = $this->objectManager->create(ExportQueueInterface::class); + $exportQueueItem->setEntityType($entityType); + $exportQueueItem->setApiType($apiType); + $exportQueueItem->setStatus($status); + $exportQueueItem->setRetries($retries); + $exportQueueItem->setTimeOfNextSendingAttempt($timeOfNextSending); + $exportQueueItem->setNumberOfItems($numberOfItems); + $exportQueueItem->setBody(uniqid()); + $this->saveExportQueue->execute($exportQueueItem); + } +} diff --git a/Test/Integration/Model/Export/QueueProcessorTest.php b/Test/Integration/Model/Export/QueueProcessorTest.php new file mode 100644 index 0000000..f5dad83 --- /dev/null +++ b/Test/Integration/Model/Export/QueueProcessorTest.php @@ -0,0 +1,206 @@ +createInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::PROCESSING, false, 10); + //Is locked + $this->createInitialExportStatus(ProductVariantsType::ENTITY_TYPE, StatusSource::SCHEDULED, true); + //Feed is disabled + $this->createInitialExportStatus(Customer::ENTITY_TYPE, StatusSource::SCHEDULED); + //Import ID is missing + $this->createInitialExportStatus(Order::ENTITY_TYPE, StatusSource::SCHEDULED); + //Everything is fine + $this->createInitialExportStatus(OrderItem::ENTITY_TYPE, StatusSource::SCHEDULED); + //Execute Action + $this->queueProcessor->process(); + //Assert initial export status + $this->assertInitialExportStatus(DefaultType::ENTITY_TYPE, StatusSource::PROCESSING, 10); + $this->assertInitialExportStatus(ProductVariantsType::ENTITY_TYPE, StatusSource::SCHEDULED, 0); + $this->assertInitialExportStatus(Customer::ENTITY_TYPE, StatusSource::DISABLED, 0); + $this->assertInitialExportStatus(Order::ENTITY_TYPE, StatusSource::NOT_READY, 0); + $this->assertInitialExportStatus(OrderItem::ENTITY_TYPE, StatusSource::PROCESSING, 1); + //Assert export queue + $this->assertExportQueueDoesNotContainEntityType(DefaultType::ENTITY_TYPE); + $this->assertExportQueueDoesNotContainEntityType(ProductVariantsType::ENTITY_TYPE); + $this->assertExportQueueDoesNotContainEntityType(Customer::ENTITY_TYPE); + $this->assertExportQueueDoesNotContainEntityType(Order::ENTITY_TYPE); + $this->assertExportQueue(OrderItem::ENTITY_TYPE, 1); + } + + /** + * Covers processing only one entity during iteration + * + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_feed/import_id test_import_id + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/import_id test_import_id + * @magentoDataFixture Bloomreach_EngagementConnector::Test/Integration/_files/order.php + * + * @return void + * @throws LocalizedException + */ + public function testProcessingOnlyOneEntity(): void + { + $this->createInitialExportStatus(Order::ENTITY_TYPE, StatusSource::SCHEDULED); + $this->createInitialExportStatus(OrderItem::ENTITY_TYPE, StatusSource::SCHEDULED); + $this->queueProcessor->process(); + $this->assertExportQueue(Order::ENTITY_TYPE, 1); + $this->assertExportQueueDoesNotContainEntityType(OrderItem::ENTITY_TYPE); + } + + /** + * Covers with not allowed status + * + * @param int $status + * + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/enabled 1 + * @magentoConfigFixture default/bloomreach_engagement/purchase_item_feed/import_id test_import_id + * @dataProvider initialExportStatusesDataProvider + * + * @return void + * @throws LocalizedException + */ + public function testSkipNotAllowedStatuses(int $status): void + { + $this->createInitialExportStatus(OrderItem::ENTITY_TYPE, $status); + $this->queueProcessor->process(); + $this->assertInitialExportStatus(OrderItem::ENTITY_TYPE, $status, 0); + $this->assertExportQueueDoesNotContainEntityType(OrderItem::ENTITY_TYPE); + } + + /** + * Initial export statuses data provider + * + * @return array[] + */ + public function initialExportStatusesDataProvider(): array + { + return [ + [ + 'status' => StatusSource::PROCESSING + ], + [ + 'status' => StatusSource::ERROR + ], + [ + 'status' => StatusSource::SUCCESS + ] + ]; + } + + /** + * Test set up + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->queueProcessor = $this->objectManager->get(QueueProcessor::class); + } + + /** + * Assert export queue + * + * @param string $entityType + * @param int $numberOfItems + * + * @return void + */ + private function assertExportQueue(string $entityType, int $numberOfItems): void + { + $exportQueueCollection = $this->getExportQueueCollection() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, $entityType) + ->addFieldToFilter(ExportQueueModel::API_TYPE, AddInitialExportDataToExportQueue::API_TYPE); + $this->assertEquals(1, $exportQueueCollection->getSize()); + /** @var ExportQueueModel $exportQueueItem */ + $exportQueueItem = $exportQueueCollection->getFirstItem(); + $this->assertEquals(ExportQueueModel::STATUS_NEW, $exportQueueItem->getStatus()); + $this->assertEquals(0, $exportQueueItem->getRetries()); + $this->assertEquals('.csv', mb_substr($exportQueueItem->getBody(), -4)); + $this->assertEmpty($exportQueueItem->getRegistered()); + $this->assertEquals($numberOfItems, $exportQueueItem->getNumberOfItems()); + $this->assertNotEmpty($exportQueueItem->getAdditionalData()); + } + + /** + * Assert Export queue does not contain export entity + * + * @param string $entityType + * + * @return void + */ + private function assertExportQueueDoesNotContainEntityType(string $entityType): void + { + $this->assertEquals( + 0, + $this->getExportQueueCollection() + ->addFieldToFilter(ExportQueueModel::ENTITY_TYPE, $entityType) + ->addFieldToFilter(ExportQueueModel::API_TYPE, AddInitialExportDataToExportQueue::API_TYPE) + ->getSize() + ); + } + + /** + * Assert initial export status + * + * @param string $entityType + * @param int $status + * @param int $totalItems + * + * @return void + * @throws LocalizedException + */ + private function assertInitialExportStatus(string $entityType, int $status, int $totalItems): void + { + $initialExportStatus = $this->initialExportStatusGetter->execute($entityType); + $this->assertEquals($status, $initialExportStatus->getStatus()); + $this->assertEquals($totalItems, $initialExportStatus->getTotalItems()); + } +} diff --git a/Test/Integration/_files/configurable_attribute.php b/Test/Integration/_files/configurable_attribute.php new file mode 100644 index 0000000..14e6987 --- /dev/null +++ b/Test/Integration/_files/configurable_attribute.php @@ -0,0 +1,71 @@ +get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +try { + $attributeRepository->get('test_configurable'); + Resolver::getInstance()->requireDataFixture( + 'Bloomreach_EngagementConnector::Test/Integration/_files/configurable_attribute_rollback.php' + ); +} catch (NoSuchEntityException $e) { +} + +$eavConfig = $objectManager->get(Config::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); +/** @var ProductAttributeInterface $attributeModel */ +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2'], 'option_2' => ['Option 3']], + 'order' => ['option_0' => 1, 'option_1' => 2, 'option_2' => 3], + ], + ] +); + +$attribute = $attributeRepository->save($attributeModel); + +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); +$eavConfig->clear(); diff --git a/Test/Integration/_files/configurable_attribute_rollback.php b/Test/Integration/_files/configurable_attribute_rollback.php new file mode 100644 index 0000000..000a7c4 --- /dev/null +++ b/Test/Integration/_files/configurable_attribute_rollback.php @@ -0,0 +1,26 @@ +get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$eavConfig = $objectManager->get(Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +if ($attribute instanceof AbstractAttribute && $attribute->getId()) { + $attribute->delete(); +} +$eavConfig->clear(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/Test/Integration/_files/disabled_configurable_product_with_three_children.php b/Test/Integration/_files/disabled_configurable_product_with_three_children.php new file mode 100644 index 0000000..0eba667 --- /dev/null +++ b/Test/Integration/_files/disabled_configurable_product_with_three_children.php @@ -0,0 +1,100 @@ +requireDataFixture( + 'Bloomreach_EngagementConnector::Test/Integration/_files/configurable_attribute.php' +); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductExtensionInterfaceFactory $productExtensionFactory */ +$productExtensionFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); +$eavConfig = Bootstrap::getObjectManager()->get(Config::class); +$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); +$associatedProductIds = $attributeValues = []; +$simpleProductsData = [ + ['simple_1', 10, 'Black', Status::STATUS_DISABLED, Visibility::VISIBILITY_NOT_VISIBLE], + ['simple_2', 20, 'White', Status::STATUS_ENABLED, Visibility::VISIBILITY_NOT_VISIBLE], + ['simple_3', 30, 'White', Status::STATUS_DISABLED, Visibility::VISIBILITY_BOTH], +]; +foreach ($options as $option) { + if (!$option->getValue()) { + continue; + } + [$productSku, $productPrice, $productDescription, $status, $visibility] = array_shift($simpleProductsData); + $product = $productFactory->create(); + $product->isObjectNew(true); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setName('Configurable ' . $option->getLabel()) + ->setSku($productSku) + ->setPrice($productPrice) + ->setTestConfigurable($option->getValue()) + ->setDescription($productDescription) + ->setVisibility($visibility) + ->setStatus($status) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} +$product = $productFactory->create(); +$product->isObjectNew(true); +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId()]) + ->setName('Configurable product with two child') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_DISABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$configurableOptions = $optionsFactory->create( + [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], + ] +); +$extensionConfigurableAttributes = $product->getExtensionAttributes() ?? $productExtensionFactory->create(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); +$productRepository->save($product); diff --git a/Test/Integration/_files/disabled_configurable_product_with_three_children_rollback.php b/Test/Integration/_files/disabled_configurable_product_with_three_children_rollback.php new file mode 100644 index 0000000..3748ab7 --- /dev/null +++ b/Test/Integration/_files/disabled_configurable_product_with_three_children_rollback.php @@ -0,0 +1,31 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +foreach (['simple_1', 'simple_2', 'configurable'] as $sku) { + try { + $productRepository->deleteById($sku); + } catch (NoSuchEntityException $e) { + // Product already deleted + } +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); +Resolver::getInstance()->requireDataFixture( + 'Bloomreach_EngagementConnector::Test/Integration/_files/configurable_attribute_rollback.php' +); diff --git a/Test/Integration/_files/product_simple.php b/Test/Integration/_files/product_simple.php index 771d449..6a3c20b 100644 --- a/Test/Integration/_files/product_simple.php +++ b/Test/Integration/_files/product_simple.php @@ -1,22 +1,10 @@ get(CategoryLinkManagementInterface::class); - -$tierPrices = []; -/** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ -$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); - -/** @var ProductTierPriceExtensionFactory $tpExtensionAttributesFactory */ -$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); - -/** @var ProductExtensionInterfaceFactory $productExtensionAttributesFactory */ -$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); - -/** @var WebsiteInterface $adminWebsite */ -$adminWebsite = $objectManager->get(WebsiteRepositoryInterface::class)->get('admin'); - -$tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create() - ->setWebsiteId($adminWebsite->getId()); -$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create( - ['website_ids' => $adminWebsite->getId()] -); - -$tierPrices[] = $tierPriceFactory->create( - [ - 'data' => [ - 'customer_group_id' => Group::CUST_GROUP_ALL, - 'qty' => 2, - 'value' => 8 - ] - ] -)->setExtensionAttributes($tierPriceExtensionAttributes1); - -$tierPrices[] = $tierPriceFactory->create( - [ - 'data' => [ - 'customer_group_id' => Group::CUST_GROUP_ALL, - 'qty' => 5, - 'value' => 5 - ] - ] -)->setExtensionAttributes($tierPriceExtensionAttributes1); - -$tierPrices[] = $tierPriceFactory->create( - [ - 'data' => [ - 'customer_group_id' => Group::NOT_LOGGED_IN_ID, - 'qty' => 3, - 'value' => 5 - ] - ] -)->setExtensionAttributes($tierPriceExtensionAttributes1); - -$tierPrices[] = $tierPriceFactory->create( - [ - 'data' => [ - 'customer_group_id' => Group::NOT_LOGGED_IN_ID, - 'qty' => 3.2, - 'value' => 6, - ] - ] -)->setExtensionAttributes($tierPriceExtensionAttributes1); - -$tierPriceExtensionAttributes2 = $tpExtensionAttributesFactory->create() - ->setWebsiteId($adminWebsite->getId()) - ->setPercentageValue(50); - -$tierPrices[] = $tierPriceFactory->create( - [ - 'data' => [ - 'customer_group_id' => Group::NOT_LOGGED_IN_ID, - 'qty' => 10 - ] - ] -)->setExtensionAttributes($tierPriceExtensionAttributes2); - /** @var Product $product */ $product = $objectManager->create(Product::class); $product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product->setTypeId(Type::TYPE_SIMPLE) ->setId(1) ->setAttributeSetId(4) ->setWebsiteIds([1]) @@ -113,9 +26,7 @@ ->setWeight(1) ->setShortDescription("Short description") ->setTaxClassId(2) - ->setTierPrices($tierPrices) ->setDescription('Description with html tag') - ->setExtensionAttributes($productExtensionAttributesWebsiteIds) ->setMetaTitle('meta title') ->setMetaKeyword('meta keyword') ->setMetaDescription('meta description') @@ -128,99 +39,8 @@ 'is_qty_decimal' => 0, 'is_in_stock' => 1, ] - )->setCanSaveCustomOptions(true) - ->setHasOptions(true); - -$oldOptions = [ - [ - 'previous_group' => 'text', - 'title' => 'Test Field', - 'type' => 'field', - 'is_require' => 1, - 'sort_order' => 0, - 'price' => 1, - 'price_type' => 'fixed', - 'sku' => '1-text', - 'max_characters' => 100, - ], - [ - 'previous_group' => 'date', - 'title' => 'Test Date and Time', - 'type' => 'date_time', - 'is_require' => 1, - 'sort_order' => 0, - 'price' => 2, - 'price_type' => 'fixed', - 'sku' => '2-date', - ], - [ - 'previous_group' => 'select', - 'title' => 'Test Select', - 'type' => 'drop_down', - 'is_require' => 1, - 'sort_order' => 0, - 'values' => [ - [ - 'option_type_id' => null, - 'title' => 'Option 1', - 'price' => 3, - 'price_type' => 'fixed', - 'sku' => '3-1-select', - ], - [ - 'option_type_id' => null, - 'title' => 'Option 2', - 'price' => 3, - 'price_type' => 'fixed', - 'sku' => '3-2-select', - ], - ] - ], - [ - 'previous_group' => 'select', - 'title' => 'Test Radio', - 'type' => 'radio', - 'is_require' => 1, - 'sort_order' => 0, - 'values' => [ - [ - 'option_type_id' => null, - 'title' => 'Option 1', - 'price' => 3, - 'price_type' => 'fixed', - 'sku' => '4-1-radio', - ], - [ - 'option_type_id' => null, - 'title' => 'Option 2', - 'price' => 3, - 'price_type' => 'fixed', - 'sku' => '4-2-radio', - ], - ] - ] -]; - -$options = []; - -/** @var ProductCustomOptionInterfaceFactory $customOptionFactory */ -$customOptionFactory = $objectManager->create(ProductCustomOptionInterfaceFactory::class); - -foreach ($oldOptions as $option) { - /** @var ProductCustomOptionInterface $option */ - $option = $customOptionFactory->create(['data' => $option]); - $option->setProductSku($product->getSku()); - - $options[] = $option; -} - -$product->setOptions($options); + ); /** @var ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(ProductRepositoryInterface::class); $productRepository->save($product); - -$categoryLinkManagement->assignProductToCategories( - $product->getSku(), - [2] -); diff --git a/Ui/Component/Listing/InitialImport/Column/ProgressLog.php b/Ui/Component/Listing/InitialImport/Column/ProgressLog.php index 93d018f..509e118 100644 --- a/Ui/Component/Listing/InitialImport/Column/ProgressLog.php +++ b/Ui/Component/Listing/InitialImport/Column/ProgressLog.php @@ -70,7 +70,7 @@ public function prepareDataSource(array $dataSource) } foreach ($dataSource['data']['items'] as &$item) { - if ($item[InitialExportStatus::STATUS] === StatusSource::PROCESSING) { + if (in_array($item[InitialExportStatus::STATUS], StatusSource::PROGRESS_LOG_VISIBLE_STATUSES)) { $item[$this->getData('name')]['view_progress'] = [ 'label' => __('View Progress'), 'url' => $this->urlBuilder->getUrl('bloomreach_engagement/initialImport/loadProgress'), @@ -78,6 +78,7 @@ public function prepareDataSource(array $dataSource) 'entityName' => $this->entityType->getEntityName($item[InitialExportStatus::ENTITY_TYPE]) ]; } + if ($item[InitialExportStatus::ERRORS]) { $item[$this->getData('name')][InitialExportStatus::ERRORS] = [ 'label' => __('View Errors') diff --git a/composer.json b/composer.json index 734c883..f292aae 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "bloomreach/bloomreach-engagement-connector-magento", - "version": "1.1.0", + "version": "1.1.1", "description": "The module provides the Bloomreach integration with Magento 2.", "type": "magento2-module", "authors": [ @@ -10,7 +10,8 @@ } ], "require": { - "php": "~7.3.0||~7.4.0||~8.1.0||~8.2.0" + "php": "~7.3.0||~7.4.0||~8.1.0||~8.2.0|~8.3.0", + "magento/framework": ">=103.0.0" }, "license": [ "MIT" diff --git a/doc/user_guide.pdf b/doc/user_guide.pdf index 2062d03..47f63a6 100644 Binary files a/doc/user_guide.pdf and b/doc/user_guide.pdf differ diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 02a38fb..5aac6b5 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -178,6 +178,15 @@ 1 + + + Bloomreach\EngagementConnector\Model\Config\Source\CatalogProductFields + Bloomreach\EngagementConnector\Model\Config\Backend\ValidateSearchableFields + + 1 + + required-entry + @@ -201,6 +210,15 @@ 1 + + + Bloomreach\EngagementConnector\Model\Config\Source\CatalogProductVariantsFields + Bloomreach\EngagementConnector\Model\Config\Backend\ValidateSearchableFields + + 1 + + required-entry + diff --git a/etc/bloomreach_entity_mapping.xml b/etc/bloomreach_entity_mapping.xml index 513809c..aca0346 100644 --- a/etc/bloomreach_entity_mapping.xml +++ b/etc/bloomreach_entity_mapping.xml @@ -315,8 +315,8 @@ - - + + diff --git a/etc/config.xml b/etc/config.xml index 294f63a..98a75d7 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -28,10 +28,12 @@ 0 0 + 0 0 + 0 diff --git a/etc/di.xml b/etc/di.xml index 35608d3..b7b8153 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -43,8 +43,10 @@ + BloomreachErrorLogger - Bloomreach\EngagementConnector\Logger\Handler + Bloomreach\EngagementConnector\Logger\Handler + Monolog\Handler\NullHandler @@ -97,6 +99,7 @@ Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\Discount Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\OnSale Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\StockLevel + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\FloatValue Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\ProductTypeRendererResolver @@ -124,6 +127,7 @@ Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\Text Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\Text Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\Url + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\FloatValue Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Customer\DefaultRenderer @@ -144,8 +148,9 @@ Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Order\ShippingCountry Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Order\ShippingAddressField Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Order\ShippingAddressField - Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Order\FloatValue - Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Order\FloatValue + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\DefaultFloatValueRenderer + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\DefaultFloatValueRenderer + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\DefaultFloatValueRenderer Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Order\PositiveFloatValue @@ -170,6 +175,7 @@ Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\OrderItem\Category Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\OrderItem\Category Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\OrderItem\Category + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\DefaultFloatValueRenderer Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\ViewItem\ProductTypeRendererResolver @@ -183,6 +189,7 @@ Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Quote\TotalPrice Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Quote\TotalPriceLocalCurrency Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Quote\TotalPriceWithoutTax + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\DefaultFloatValueRenderer @@ -219,6 +226,7 @@ Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\DiscountPercentage Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\DiscountValue Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\StockLevel + Bloomreach\EngagementConnector\Model\DataMapping\FieldValueRenderer\Product\FloatValue @@ -360,11 +368,6 @@ Bloomreach\EngagementConnector\Logger\Logger - - - Bloomreach\EngagementConnector\Logger\Logger - - Bloomreach\EngagementConnector\Logger\Logger @@ -413,8 +416,10 @@ + BloomreachDebugLogger - Bloomreach\EngagementConnector\Logger\Handler\Debugger + Bloomreach\EngagementConnector\Logger\Handler\Debugger + Monolog\Handler\NullHandler @@ -469,12 +474,14 @@ bloomreach_engagement/catalog_product_feed/import_id bloomreach_engagement/catalog_product_feed/catalog_id bloomreach_engagement/catalog_product_feed/real_time_updates + bloomreach_engagement/catalog_product_feed/searchable_fields bloomreach_engagement/catalog_product_variants_feed/enabled bloomreach_engagement/catalog_product_variants_feed/import_id bloomreach_engagement/catalog_product_variants_feed/catalog_id bloomreach_engagement/catalog_product_variants_feed/real_time_updates + bloomreach_engagement/catalog_product_variants_feed/searchable_fields bloomreach_engagement/customer_feed/enabled @@ -500,6 +507,7 @@ Bloomreach\EngagementConnector\Service\InitialExportStatus\ItemsGetter\Proxy Bloomreach\EngagementConnector\Model\Export\Queue\AddInitialExportDataToExportQueue\Proxy Bloomreach\EngagementConnector\Api\SaveInitialExportStatusInterface\Proxy + Bloomreach\EngagementConnector\Logger\Logger diff --git a/etc/module.xml b/etc/module.xml index 94df35c..7236e23 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -7,5 +7,5 @@ --> - + diff --git a/view/adminhtml/templates/initial_import/current-progress.phtml b/view/adminhtml/templates/initial_import/current-progress.phtml index 677d364..9d56450 100644 --- a/view/adminhtml/templates/initial_import/current-progress.phtml +++ b/view/adminhtml/templates/initial_import/current-progress.phtml @@ -6,36 +6,45 @@ use Bloomreach\EngagementConnector\Api\Data\ExportQueueInterface; use Bloomreach\EngagementConnector\Block\Adminhtml\InitialImport\CurrentProgress; +use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** @var CurrentProgress $block */ - +/** @var Escaper $escaper */ +/** @var SecureHtmlRenderer $secureRenderer */ $totalItems = $block->getTotalItems(); ?> isStarted() && $totalItems > 0): ?> - getTotalQueuedItems(); ?> + isFinished() + ? ($block->getTotalErrorItems() + $block->getTotalExportedItems()) + : $block->getTotalQueuedItems(); + ?>
- + isFinished() + ? __('Import complete') + : __('Adding items to the Export Queue'); + ?>
+ style="width: escapeHtmlAttr($totalProcessedPercent); ?>">
- +
@@ -43,7 +52,7 @@ $totalItems = $block->getTotalItems(); getTotalProcessedItems(); ?>
@@ -53,7 +62,7 @@ $totalItems = $block->getTotalItems();
+ style="width: escapeHtmlAttr($totalProcessedPercent); ?>">
@@ -81,28 +90,28 @@ $totalItems = $block->getTotalItems(); - escapeHtml($item->getEntityId()); ?> + escapeHtml($item->getEntityId()); ?> - escapeHtml($block->getDecoratedStatus($item), ['span']); ?> + escapeHtml($block->getDecoratedStatus($item), ['span']); ?> - escapeHtml($item->getRetries()); ?> + escapeHtml($item->getRetries()); ?> - escapeHtml($block->getEntitiesRange($item)); ?> + escapeHtml($block->getEntitiesRange($item)); ?> - + - escapeHtml($item->getNumberOfItems()); ?> + escapeHtml($item->getNumberOfItems()); ?> - escapeHtml($block->getTimeOfNextSendingAttempt($item)); ?> + escapeHtml($block->getTimeOfNextSendingAttempt($item)); ?> @@ -113,53 +122,45 @@ $totalItems = $block->getTotalItems();
- + + renderTag('style', [], $styleString, false); ?> - diff --git a/view/adminhtml/templates/system/config/button.phtml b/view/adminhtml/templates/system/config/button.phtml index f8a38d2..e56c222 100644 --- a/view/adminhtml/templates/system/config/button.phtml +++ b/view/adminhtml/templates/system/config/button.phtml @@ -3,37 +3,41 @@ * @author Bloomreach * @copyright Copyright (c) Bloomreach (https://www.bloomreach.com/) */ -/** @var \Bloomreach\EngagementConnector\Block\Adminhtml\System\Config\Button $block */ +use Bloomreach\EngagementConnector\Block\Adminhtml\System\Config\Button; +use Magento\Framework\Escaper; + +/** @var Button $block */ +/** @var Escaper $escaper */ $isInProgress = $block->getIsImportInProgress(); $isEnabled = $block->getIsEnabled() && !$isInProgress; -$messageContainer = $block->escapeHtmlAttr($block->getHtmlId() . '_message'); +$messageContainer = $escaper->escapeHtmlAttr($block->getHtmlId() . '_message'); ?>
- escapeHtml(__('Disabled.')) ?> - escapeHtml( + escapeHtml(__('Disabled.')) ?> + escapeHtml( __('Run initial imports is currently disabled, because there are imports already in progress.') ); ?>
-
- getChildHtml(); ?> - +if ($configViewModel) { + $scriptString = << - getChildHtml(); ?> - +if ($configViewModel) { + $scriptString = <<