diff --git a/README.md b/README.md index 4558e28..cc00116 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ # Shopware Ergonode Integration +## Our modification devEcommerce +- Change php-graphql-client (not compatible with psr/http-message^2.0) +- **Variants**: + - Fix adding new variants (wrong grouping) + - Full inheritance for variants +- **Attributes**: + - Hide attributes with hide metadata value + - Disable default filterable attribute for new attributes +- Add filterProductsNotMatchingSkuPattern (filter not products and parts) +- Languages - updates only active languages +- Category: inject the main category as tree root +- Images: leave the original name as a prefix +- **Change default** + - DEFAULT_STOCK_VALUE to "0" + - DEFAULT_GROSS_PRICE to "99999" + - minPurchaseMapping to "1" +- Use only selected brands (temporary) + ## Description This plugin synchronizes data from Ergonode to Shopware. It takes advantage of Ergonode's GraphQL API and utilizes @@ -119,4 +137,4 @@ Available cache pools: |------------------|-------------| | 6.6 from 6.6.0.0 | Version 3.x | | 6.5 from 6.5.0.0 | Version 2.x | -| 6.4 from 6.4.0.0 | Version 1.x | \ No newline at end of file +| 6.4 from 6.4.0.0 | Version 1.x | diff --git a/composer.json b/composer.json index 942d6c0..143b8d1 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "ergonode/integration-shopware", "description": "Shopware Ergonode Integration", - "version": "3.0.1", + "version": "3.0.10-patch", "type": "shopware-platform-plugin", "license": "proprietary", "authors": [ @@ -24,9 +24,9 @@ "require": { "php": ">= 8.2", "ext-json": "*", - "shopware/administration": "6.6.*", - "shopware/core": "6.6.*", - "gmostafa/php-graphql-client": "^1.13" + "shopware/administration": ">=6.6.0 <6.7.0", + "shopware/core": ">=6.6.0 <6.7.0", + "carnage/php-graphql-client": "^1.14" }, "require-dev": { "phpunit/phpunit": "~9.5.17", diff --git a/src/DTO/ProductShopwareData.php b/src/DTO/ProductShopwareData.php index 77a5946..0f5630a 100644 --- a/src/DTO/ProductShopwareData.php +++ b/src/DTO/ProductShopwareData.php @@ -103,7 +103,7 @@ public function setMedia(array $payloads): void $this->data['media'] = $payloads; } - public function setTax(string $taxId): void + public function setTax(?string $taxId): void // devEcommerce change { $this->data['taxId'] = $taxId; } @@ -148,7 +148,7 @@ public function setProductNumber(string $sku): void $this->data['productNumber'] = $sku; } - public function setPrice(array $pricePayload): void + public function setPrice(?array $pricePayload): void // devEcommerce change { $this->data['price'] = $pricePayload; } @@ -170,7 +170,12 @@ public function addChild(ProductShopwareData $productShopwareData): void public function setDisplayParent(bool $display = true): void { - $this->data['displayParent'] = $display; + $this->data['variantListingConfig'] = [ // devEcommerce change + 'extensions' => [], + 'displayParent' => $display, + 'mainVariantId' => null, + 'configuratorGroupConfig' => null + ]; } public function addConfigrationSettings(array $configurationSettings): void diff --git a/src/Manager/FileManager.php b/src/Manager/FileManager.php index 0f59de1..618fcd1 100644 --- a/src/Manager/FileManager.php +++ b/src/Manager/FileManager.php @@ -86,6 +86,7 @@ private function buildFileName(ProductMultimediaTranslation $image): string 'url' => $image->getUrl(), ]; - return md5(json_encode($imageData)); + //return md5(json_encode($imageData)); + return pathinfo($imageData['name'], PATHINFO_FILENAME) . '_' . pathinfo($imageData['url'], PATHINFO_FILENAME); // devEcommerce change } } diff --git a/src/Persistor/ProductPersistor.php b/src/Persistor/ProductPersistor.php index aee79d9..78eed92 100644 --- a/src/Persistor/ProductPersistor.php +++ b/src/Persistor/ProductPersistor.php @@ -97,6 +97,8 @@ public function persist(array $productListData, Context $context): array $productListData = $this->filterMainProducts($productListData); + $productListData = $this->filterProductsNotMatchingSkuPattern($productListData); // devEcommerce change + foreach ($productListData as $productData) { $mainProductPayload = $this->getProductPayload($productData['node'] ?? [], $context); if ( @@ -306,6 +308,20 @@ private function filterMainProducts(array $productListData): array ); } + // start devEcommerce change + private function filterProductsNotMatchingSkuPattern(array $productListData): array + { + return array_values( + array_filter( + $productListData, + static function (array $productData): bool { + return preg_match('/^V[0-9]+$/', $productData['node']['sku']) === 1; // todo move to configuration + } + ) + ); + } + // stop devEcommerce change + private function extractSkus(array $productListData, bool $onlyVariants = false): array { foreach ($productListData as $productData) { diff --git a/src/Persistor/PropertyGroupPersistor.php b/src/Persistor/PropertyGroupPersistor.php index df84c87..4c4462c 100644 --- a/src/Persistor/PropertyGroupPersistor.php +++ b/src/Persistor/PropertyGroupPersistor.php @@ -9,6 +9,8 @@ use Ergonode\IntegrationShopware\Processor\Attribute\AttributeCustomProcessorResolver; use Ergonode\IntegrationShopware\Provider\PropertyGroupProvider; use Ergonode\IntegrationShopware\Transformer\PropertyGroupTransformer; +use Ergonode\IntegrationShopware\Util\YesNo; +use Ergonode\IntegrationShopware\Service\ConfigService; use Shopware\Core\Content\Property\PropertyGroupDefinition; use Shopware\Core\Framework\Context; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; @@ -33,7 +35,8 @@ public function __construct( EntityRepository $propertyGroupOptionRepository, PropertyGroupTransformer $propertyGroupTransformer, PropertyGroupProvider $propertyGroupProvider, - AttributeCustomProcessorResolver $attributeCustomProcessorResolver + AttributeCustomProcessorResolver $attributeCustomProcessorResolver, + private ConfigService $configService ) { $this->propertyGroupRepository = $propertyGroupRepository; $this->propertyGroupOptionRepository = $propertyGroupOptionRepository; @@ -57,6 +60,19 @@ public function persistStream(AttributeStreamResultsProxy $attributes, Context $ continue; } + // start devEcommerce change + $skipMetaHideAttribute = false; + foreach ($node['metadata'] as $metadata) { + if (($metadata['key'] === $this->configService->getHideMetaDataKey() && YesNo::cast($metadata['value']))) { + $skipMetaHideAttribute = true; + break; + } + } + if ($skipMetaHideAttribute) { + continue; + } + // end devEcommerce change + $propertyGroup = $this->propertyGroupProvider->getPropertyGroupByMapping($code, $context); $dto = new PropertyGroupTransformationDTO($node); diff --git a/src/Processor/Attribute/ManufacturerAttributeProcessor.php b/src/Processor/Attribute/ManufacturerAttributeProcessor.php index 0edb59f..4af75ad 100644 --- a/src/Processor/Attribute/ManufacturerAttributeProcessor.php +++ b/src/Processor/Attribute/ManufacturerAttributeProcessor.php @@ -59,6 +59,10 @@ public function process(array $node, Context $context): void $code = $option['code']; $manufacturerEntity = $this->getExistingManufacturerEntity($code, $context); + if(!in_array($code, ['gymtek', 'outtec', 'kedica', 'queenfit', 'xride', 'luverno', 'stars' ])) { // devEcommerce change + continue; + } + $translations = []; foreach ($option['name'] as $nameRow) { $translations[IsoCodeConverter::ergonodeToShopwareIso($nameRow['language'])] = [ diff --git a/src/Processor/CategoryTreeSyncProcessor.php b/src/Processor/CategoryTreeSyncProcessor.php index f75be94..046cfa2 100644 --- a/src/Processor/CategoryTreeSyncProcessor.php +++ b/src/Processor/CategoryTreeSyncProcessor.php @@ -113,7 +113,7 @@ public function processStream( $stopwatch->start('process'); try { - $leafEdges = $edge['node']['categoryTreeLeafList']['edges'] ?? []; + $leafEdges = $this->normalizeCategoryTreeEdges($edge); $primaryKeys = $this->categoryTreePersistor->persistLeaves($leafEdges, $currentTreeCode, $context); $this->categoryTreePersistor->markCategoriesAsActive($primaryKeys); @@ -178,6 +178,42 @@ public function processStream( return $counter; } + /** + * devEcommerce change + */ + private function normalizeCategoryTreeEdges(mixed $edge): mixed + { + $leafEdges = $edge['node']['categoryTreeLeafList']['edges'] ?? []; + $categoryTreeCode = $edge['node']['code']; + $categoryTreeNames = $edge['node']['name']; + $mainEdgeKeys = array_keys(array_filter( + $leafEdges, + fn($row) => empty($row['node']['parentCategory']) + )); + + if(!empty($mainEdgeKeys)) { + $firstCategory = [ + 'node' => [ + 'category' => [ + 'code' => $categoryTreeCode, + 'name' => $categoryTreeNames, + ], + 'parentCategory' => null, + ] + ]; + array_unshift($leafEdges,$firstCategory); + + foreach ($mainEdgeKeys as $key) { + $leafEdges[$key+1]['node']['parentCategory'] = ['code' => $categoryTreeCode]; + } + } + + file_put_contents('../custom/plugins/ergo_graph.log', print_r($mainEdgeKeys, true) . "\n-----\n" ,FILE_APPEND); + file_put_contents('../custom/plugins/ergo_graph.log', print_r($leafEdges, true) . "\n-----\n" ,FILE_APPEND); + + return $leafEdges; + } + /** * Gets ID of last existing top level category * diff --git a/src/Provider/LanguageProvider.php b/src/Provider/LanguageProvider.php index e89c717..78ab97b 100644 --- a/src/Provider/LanguageProvider.php +++ b/src/Provider/LanguageProvider.php @@ -9,6 +9,7 @@ use Shopware\Core\Framework\Context; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; +use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter; use Shopware\Core\System\Language\LanguageEntity; class LanguageProvider @@ -58,6 +59,8 @@ public function getActiveLocaleCodes(Context $context): array { $criteria = new Criteria(); $criteria->addAssociation('locale'); + $criteria->addAssociation('swagLanguagePackLanguage'); // devEcommerce change + $criteria->addFilter(new EqualsFilter('swagLanguagePackLanguage.salesChannelActive', true)); // devEcommerce change /** @var LanguageEntity[] $languages */ $languages = $this->languageRepository->search($criteria, $context); diff --git a/src/QueryBuilder/AttributeQueryBuilder.php b/src/QueryBuilder/AttributeQueryBuilder.php index 3378e2f..0c048d1 100644 --- a/src/QueryBuilder/AttributeQueryBuilder.php +++ b/src/QueryBuilder/AttributeQueryBuilder.php @@ -44,6 +44,11 @@ protected function getAttributeSelectionSet(): array ->setSelectionSet([ 'code', 'scope', + (new Query('metadata')) // devEcommerce change + ->setSelectionSet([ + 'key', + 'value', + ]), (new Query('name')) ->setSelectionSet([ 'language', diff --git a/src/QueryBuilder/CategoryQueryBuilder.php b/src/QueryBuilder/CategoryQueryBuilder.php index 50df7a2..bb59d10 100644 --- a/src/QueryBuilder/CategoryQueryBuilder.php +++ b/src/QueryBuilder/CategoryQueryBuilder.php @@ -154,6 +154,11 @@ public function buildTreeStream( (new Query('node')) ->setSelectionSet([ 'code', + (new Query('name')) // devEcommerce change + ->setSelectionSet([ + 'value', + 'language', + ]), (new Query('categoryTreeLeafList')) ->setArguments($categoryLeafArguments) ->setSelectionSet([ diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml index 15c18b5..50c0d40 100644 --- a/src/Resources/config/config.xml +++ b/src/Resources/config/config.xml @@ -26,6 +26,17 @@ + + Fields configuration* + + + hideMetaDataKey + ukryty_w_sklepie + + Enter the exact name of the metadata key used in Ergonode. Example: "ukryty_w_sklepie". When this key is set on an attribute, that attribute will be hidden in the storefront. This value is inherited by all sales channels. + + + Fields mapping Fields mapping @@ -80,8 +91,8 @@ - Product sync - Product sync + Template sync + Template sync templateLayoutMapping diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index f2827b3..1a72217 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -215,6 +215,7 @@ services: - '@Ergonode\IntegrationShopware\Transformer\ProductScaleUnitTransformer' - '@Ergonode\IntegrationShopware\Transformer\ProductMinMaxQuantityTransformer' - '@Ergonode\IntegrationShopware\Transformer\ProductLayoutTransformer' + - '@Ergonode\IntegrationShopware\Transformer\ProductCustomBusinessTransformer' Ergonode\IntegrationShopware\Transformer\CategoryAttributesTransformerChain: arguments: diff --git a/src/Service/ConfigService.php b/src/Service/ConfigService.php index 2ed5804..0908977 100644 --- a/src/Service/ConfigService.php +++ b/src/Service/ConfigService.php @@ -186,4 +186,11 @@ public function getTemplateLayoutMapping(): array }, $value) ); } + + // start devEcommerce change + public function getHideMetaDataKey(): string + { + return $this->configService->getString(self::CONFIG_NAMESPACE . 'hideMetaDataKey'); + } + // end devEcommerce change } diff --git a/src/Transformer/ProductCustomBusinessTransformer.php b/src/Transformer/ProductCustomBusinessTransformer.php new file mode 100644 index 0000000..d97df7d --- /dev/null +++ b/src/Transformer/ProductCustomBusinessTransformer.php @@ -0,0 +1,43 @@ +configService = $configService; + } + + public function transform(ProductTransformationDTO $productData, Context $context): ProductTransformationDTO { + $swData = $productData->getShopwareData(); + $ergonodeData = $productData->getErgonodeData(); + $sku = $ergonodeData->getSku(); + + if (preg_match('/^V[0-9]+_.+$/', $sku)) { // handle for good parts + // inherit from a parent + $swData->setName(''); + $swData->setTax(null); + $swData->setPrice(null); + $swData->setData('isCloseout', null); + $swData->setData('shippingFree', null); + $swData->setData('markAsTopseller', null); + $swData->setData('minPurchase', null); + $swData->setData('purchaseSteps', null); + $swData->setData('active', null); + $swData->setProperties([]); + } + + $productData->setShopwareData($swData); + + return $productData; + } + +} diff --git a/src/Transformer/ProductDefaultValuesTransformer.php b/src/Transformer/ProductDefaultValuesTransformer.php index 3d02e53..9ceb3dc 100644 --- a/src/Transformer/ProductDefaultValuesTransformer.php +++ b/src/Transformer/ProductDefaultValuesTransformer.php @@ -12,7 +12,7 @@ class ProductDefaultValuesTransformer implements ProductDataTransformerInterface { - private const DEFAULT_STOCK_VALUE = 999; + private const DEFAULT_STOCK_VALUE = 0; private ConfigService $configService; diff --git a/src/Transformer/ProductMinMaxQuantityTransformer.php b/src/Transformer/ProductMinMaxQuantityTransformer.php index 4599564..48b75a4 100644 --- a/src/Transformer/ProductMinMaxQuantityTransformer.php +++ b/src/Transformer/ProductMinMaxQuantityTransformer.php @@ -83,7 +83,7 @@ private function getMinPurchase( $existingMinPurchase = $productData->getSwProduct()?->getMinPurchase(); // if unmapped, return current value if ($minPurchaseMapping === false) { - return $existingMinPurchase; + return $existingMinPurchase ?? 1; // devEcommerce change } $minPurchase = $minPurchaseMapping?->getTranslation($defaultLanguage)?->getValue(); diff --git a/src/Transformer/ProductPriceTransformer.php b/src/Transformer/ProductPriceTransformer.php index c40248f..7816438 100644 --- a/src/Transformer/ProductPriceTransformer.php +++ b/src/Transformer/ProductPriceTransformer.php @@ -11,6 +11,8 @@ class ProductPriceTransformer implements ProductDataTransformerInterface { + const DEFAULT_GROSS_PRICE = 99999; // devEcommerce change + public function transform(ProductTransformationDTO $productData, Context $context): ProductTransformationDTO { $swData = $productData->getShopwareData(); @@ -18,20 +20,20 @@ public function transform(ProductTransformationDTO $productData, Context $contex $defaultLanguage = $productData->getDefaultLanguage(); $pricePayload = [ - 'linked' => false, + 'linked' => true, // devEcommerce change 'currencyId' => Defaults::CURRENCY, ]; if (!$productData->getSwProduct()?->getPrice()) { - $pricePayload['gross'] = 0; + $pricePayload['gross'] = self::DEFAULT_GROSS_PRICE; // devEcommerce change if ($ergonodeData->getPriceGross() instanceof ProductAttribute) { - $pricePayload['gross'] = (float)$ergonodeData->getPriceGross()->getTranslation($defaultLanguage)?->getValue() ?? 0; + $pricePayload['gross'] = (float)$ergonodeData->getPriceGross()->getTranslation($defaultLanguage)?->getValue() ?? self::DEFAULT_GROSS_PRICE; // devEcommerce change } - $pricePayload['net'] = 0; + $pricePayload['net'] = self::DEFAULT_GROSS_PRICE; // devEcommerce change if ($ergonodeData->getPriceNet() instanceof ProductAttribute) { $pricePayload['net'] = (float)$ergonodeData->getPriceNet()->getTranslation($defaultLanguage)?->getValue( - ) ?? 0; + ) ?? self::DEFAULT_GROSS_PRICE; // devEcommerce change } } else { $pricePayload['gross'] = $ergonodeData->getPriceGross() diff --git a/src/Transformer/PropertyGroupTransformer.php b/src/Transformer/PropertyGroupTransformer.php index 59125c1..f7626fd 100644 --- a/src/Transformer/PropertyGroupTransformer.php +++ b/src/Transformer/PropertyGroupTransformer.php @@ -87,6 +87,7 @@ public function transformAttributeNode(PropertyGroupTransformationDTO $dto): Pro $dto->setPropertyGroupPayload([ 'id' => $propertyGroup?->getId(), 'name' => $code, + 'filterable' => $propertyGroup?->getFilterable() ?? false, // devEcommerce change 'options' => $options, 'translations' => $translations, 'extensions' => [ diff --git a/src/Transformer/VariantsTransformer.php b/src/Transformer/VariantsTransformer.php index 85126a0..42595ca 100644 --- a/src/Transformer/VariantsTransformer.php +++ b/src/Transformer/VariantsTransformer.php @@ -15,6 +15,7 @@ use Shopware\Core\Content\Product\Aggregate\ProductConfiguratorSetting\ProductConfiguratorSettingDefinition; use Shopware\Core\Content\Product\ProductEntity; use Shopware\Core\Framework\Context; +use Shopware\Core\Framework\Uuid\Uuid; class VariantsTransformer { @@ -78,14 +79,35 @@ public function transform(ProductTransformationDTO $productData, Context $contex foreach ($transformedVariants as $variant) { $shopwareData = $variant->getShopwareData(); + // start devEcommerce change - fix configuratorSettings when add a new option if (null !== $parentProduct) { - $shopwareData->setParentId($parentProduct->getId()); + $parentProductId = $parentProduct->getId(); + } else{ + if(!$swData->getData('id')) { + $swData->setId(Uuid::randomHex()); + } + + $parentProductId = $swData->getData('id'); } + $shopwareData->setParentId($parentProductId); + $swData->addChild($shopwareData); - if (property_exists(ProductEntity::class, 'displayParent')) { + if (property_exists(ProductEntity::class, 'variantListingConfig')) { // devEcommerce change $swData->setDisplayParent(); } + + if(!$variant->getSwProduct()) { + foreach ($variant->getShopwareData()->getData('options') as $option) { + $swData->addConfigrationSettings([ + 'id' => null, + 'productId' => $parentProductId, + 'optionId' => $option['id'], + ]); + } + } + // end start devEcommerce change + foreach ($variant->getSwProduct()?->getOptionIds() ?? [] as $optionId) { if ( diff --git a/src/Util/YesNo.php b/src/Util/YesNo.php index c4d92ad..b89c16b 100644 --- a/src/Util/YesNo.php +++ b/src/Util/YesNo.php @@ -13,11 +13,13 @@ class YesNo '1', 'YES', 'yes', + 'Yes', 'Y', 'y', 'A', 'a', - 'tak' + 'tak', + 'Tak' ]; private const FALSE_LIKE = [ @@ -27,11 +29,13 @@ class YesNo '0', 'NO', 'no', + 'No', 'N', 'n', 'B', 'b', - 'nie' + 'nie', + 'Nie' ]; public static function cast(string|bool|int $value): bool