55 */
66namespace Magento \AdvancedPricingImportExport \Model \Import ;
77
8+ use Magento \AdvancedPricingImportExport \Model \CurrencyResolver ;
89use Magento \CatalogImportExport \Model \Import \Product as ImportProduct ;
910use Magento \CatalogImportExport \Model \Import \Product \RowValidatorInterface as ValidatorInterface ;
11+ use Magento \Framework \App \ObjectManager ;
1012use Magento \ImportExport \Model \Import \ErrorProcessing \ProcessingErrorAggregatorInterface ;
1113
1214/**
1517 * @SuppressWarnings(PHPMD.ExcessiveParameterList)
1618 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1719 * @SuppressWarnings(PHPMD.TooManyFields)
20+ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
1821 */
1922class AdvancedPricing extends \Magento \ImportExport \Model \Import \Entity \AbstractEntity
2023{
@@ -42,6 +45,8 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract
4245 private const VALIDATOR_TEAR_PRICE = 'validator_tier_price ' ;
4346 private const VALIDATOR_TIER_PRICE = 'validator_tier_price ' ;
4447
48+ private const ERROR_DUPLICATE_TIER_PRICE = 'duplicateTierPrice ' ;
49+
4550 /**
4651 * Validation failure message template definitions.
4752 *
@@ -57,8 +62,10 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract
5762 ValidatorInterface::ERROR_INVALID_TIER_PRICE_TYPE => 'Value for \'tier_price_value_type \' ' .
5863 'attribute contains incorrect value, acceptable values are Fixed, Discount ' ,
5964 ValidatorInterface::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete ' ,
60- ValidatorInterface::ERROR_INVALID_ATTRIBUTE_DECIMAL =>
61- 'Value for \'%s \' attribute contains incorrect value, acceptable values are in decimal format ' ,
65+ ValidatorInterface::ERROR_INVALID_ATTRIBUTE_DECIMAL => 'Value for \'%s \' attribute contains incorrect value, ' .
66+ ' acceptable values are in decimal format ' ,
67+ self ::ERROR_DUPLICATE_TIER_PRICE => 'We found a duplicate website, tier price, customer group ' .
68+ ' and quantity. '
6269 ];
6370
6471 /**
@@ -155,6 +162,26 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract
155162 */
156163 private $ productEntityLinkField ;
157164
165+ /**
166+ * @var array
167+ */
168+ private $ websiteScopeTierPrice = [];
169+
170+ /**
171+ * @var array
172+ */
173+ private $ globalScopeTierPrice = [];
174+
175+ /**
176+ * @var array
177+ */
178+ private $ allProductIds = [];
179+
180+ /**
181+ * @var CurrencyResolver
182+ */
183+ private $ currencyResolver ;
184+
158185 /**
159186 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
160187 * @param \Magento\Framework\Json\Helper\Data $jsonHelper
@@ -172,8 +199,9 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract
172199 * @param AdvancedPricing\Validator $validator
173200 * @param AdvancedPricing\Validator\Website $websiteValidator
174201 * @param AdvancedPricing\Validator\TierPrice $tierPriceValidator
175- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
202+ * @param CurrencyResolver|null $currencyResolver
176203 * @throws \Exception
204+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
177205 */
178206 public function __construct (
179207 \Magento \Framework \Json \Helper \Data $ jsonHelper ,
@@ -190,7 +218,8 @@ public function __construct(
190218 ImportProduct $ importProduct ,
191219 AdvancedPricing \Validator $ validator ,
192220 AdvancedPricing \Validator \Website $ websiteValidator ,
193- AdvancedPricing \Validator \TierPrice $ tierPriceValidator
221+ AdvancedPricing \Validator \TierPrice $ tierPriceValidator ,
222+ ?CurrencyResolver $ currencyResolver = null
194223 ) {
195224 $ this ->dateTime = $ dateTime ;
196225 $ this ->jsonHelper = $ jsonHelper ;
@@ -209,6 +238,7 @@ public function __construct(
209238 $ this ->_validators [self ::VALIDATOR_WEBSITE ] = $ websiteValidator ;
210239 $ this ->_validators [self ::VALIDATOR_TIER_PRICE ] = $ tierPriceValidator ;
211240 $ this ->errorAggregator = $ errorAggregator ;
241+ $ this ->currencyResolver = $ currencyResolver ?? ObjectManager::getInstance ()->get (CurrencyResolver::class);
212242
213243 foreach (array_merge ($ this ->errorMessageTemplates , $ this ->_messageTemplates ) as $ errorCode => $ message ) {
214244 $ this ->getErrorAggregator ()->addErrorMessageTemplate ($ errorCode , $ message );
@@ -270,6 +300,11 @@ public function validateRow(array $rowData, $rowNum)
270300 if (false === $ sku ) {
271301 $ this ->addRowError (ValidatorInterface::ERROR_ROW_IS_ORPHAN , $ rowNum );
272302 }
303+
304+ if (!$ this ->getErrorAggregator ()->isRowInvalid ($ rowNum )) {
305+ $ this ->validateRowForDuplicate ($ rowData , $ rowNum );
306+ }
307+
273308 return !$ this ->getErrorAggregator ()->isRowInvalid ($ rowNum );
274309 }
275310
@@ -634,4 +669,160 @@ private function getProductEntityLinkField()
634669 }
635670 return $ this ->productEntityLinkField ;
636671 }
672+
673+ /**
674+ * @inheritdoc
675+ */
676+ protected function _saveValidatedBunches ()
677+ {
678+ if (\Magento \ImportExport \Model \Import::BEHAVIOR_APPEND === $ this ->getBehavior ()
679+ && !$ this ->_catalogData ->isPriceGlobal ()
680+ ) {
681+ $ source = $ this ->_getSource ();
682+ $ source ->rewind ();
683+ while ($ source ->valid ()) {
684+ try {
685+ $ rowData = $ source ->current ();
686+ } catch (\InvalidArgumentException $ exception ) {
687+ $ source ->next ();
688+ continue ;
689+ }
690+ $ this ->validateRow ($ rowData , $ source ->key ());
691+ $ source ->next ();
692+ }
693+ $ this ->validateRowsForDuplicate (self ::TABLE_TIER_PRICE );
694+ }
695+ return parent ::_saveValidatedBunches ();
696+ }
697+
698+ /**
699+ * Validate all row data with existing prices in the database for duplicate
700+ *
701+ * A row is considered a duplicate if the pair (product_id, all_groups, customer_group_id, qty) exists for
702+ * both global and website scopes. And the base currency is the same for both global and website scopes.
703+ *
704+ * @param string $table
705+ */
706+ private function validateRowsForDuplicate (string $ table ): void
707+ {
708+ if (!empty ($ this ->allProductIds )) {
709+ $ priceDataCollection = $ this ->getPrices (array_keys ($ this ->allProductIds ), $ table );
710+ $ defaultBaseCurrency = $ this ->currencyResolver ->getDefaultBaseCurrency ();
711+ $ websiteCodeBaseCurrencyMap = $ this ->currencyResolver ->getWebsitesBaseCurrency ();
712+ $ websiteIdCodeMap = array_flip ($ this ->_storeResolver ->getWebsiteCodeToId ());
713+ foreach ($ priceDataCollection as $ priceData ) {
714+ $ isDefaultScope = (int ) $ priceData ['website_id ' ] === 0 ;
715+ $ baseCurrency = $ isDefaultScope
716+ ? $ defaultBaseCurrency
717+ : $ websiteCodeBaseCurrencyMap [$ websiteIdCodeMap [$ priceData ['website_id ' ]] ?? null ] ?? null ;
718+ $ rowNums = [];
719+ $ key = $ this ->getUniqueKey ($ priceData , $ baseCurrency );
720+ if ($ isDefaultScope ) {
721+ if (isset ($ this ->websiteScopeTierPrice [$ key ])) {
722+ $ rowNums = $ this ->websiteScopeTierPrice [$ key ];
723+ }
724+ } else {
725+ if (isset ($ this ->globalScopeTierPrice [$ key ])) {
726+ $ rowNums = $ this ->globalScopeTierPrice [$ key ];
727+ }
728+ }
729+ foreach ($ rowNums as $ rowNum ) {
730+ $ this ->addRowError (self ::ERROR_DUPLICATE_TIER_PRICE , $ rowNum );
731+ }
732+ }
733+ }
734+ }
735+
736+ /**
737+ * Validate row data for duplicate
738+ *
739+ * A row is considered a duplicate if the pair (product_id, all_groups, customer_group_id, qty) exists for
740+ * both global and website scopes. And the base currency is the same for both global and website scopes.
741+ *
742+ * @param array $rowData
743+ * @param int $rowNum
744+ */
745+ private function validateRowForDuplicate (array $ rowData , int $ rowNum )
746+ {
747+ $ productId = $ this ->retrieveOldSkus ()[$ rowData [self ::COL_SKU ]] ?? null ;
748+ if ($ productId && !$ this ->_catalogData ->isPriceGlobal ()) {
749+ $ productEntityLinkField = $ this ->getProductEntityLinkField ();
750+ $ priceData = [
751+ $ productEntityLinkField => $ productId ,
752+ 'website_id ' => (int ) $ this ->getWebSiteId ($ rowData [self ::COL_TIER_PRICE_WEBSITE ]),
753+ 'all_groups ' => $ rowData [self ::COL_TIER_PRICE_CUSTOMER_GROUP ] == self ::VALUE_ALL_GROUPS ? 1 : 0 ,
754+ 'customer_group_id ' => $ this ->getCustomerGroupId ($ rowData [self ::COL_TIER_PRICE_CUSTOMER_GROUP ]),
755+ 'qty ' => $ rowData [self ::COL_TIER_PRICE_QTY ],
756+ ];
757+ $ defaultBaseCurrency = $ this ->currencyResolver ->getDefaultBaseCurrency ();
758+ $ websiteCodeBaseCurrencyMap = $ this ->currencyResolver ->getWebsitesBaseCurrency ();
759+ $ websiteIdCodeMap = array_flip ($ this ->_storeResolver ->getWebsiteCodeToId ());
760+ $ baseCurrency = $ priceData ['website_id ' ] === 0
761+ ? $ defaultBaseCurrency
762+ : $ websiteCodeBaseCurrencyMap [$ websiteIdCodeMap [$ priceData ['website_id ' ]] ?? null ] ?? null ;
763+
764+ $ this ->allProductIds [$ productId ][] = $ rowNum ;
765+ $ key = $ this ->getUniqueKey ($ priceData , $ baseCurrency );
766+ if ($ priceData ['website_id ' ] === 0 ) {
767+ $ this ->globalScopeTierPrice [$ key ][] = $ rowNum ;
768+ if (isset ($ this ->websiteScopeTierPrice [$ key ])) {
769+ $ this ->addRowError (self ::ERROR_DUPLICATE_TIER_PRICE , $ rowNum );
770+ }
771+ } else {
772+ $ this ->websiteScopeTierPrice [$ key ][] = $ rowNum ;
773+ if (isset ($ this ->globalScopeTierPrice [$ key ])) {
774+ $ this ->addRowError (self ::ERROR_DUPLICATE_TIER_PRICE , $ rowNum );
775+ }
776+ }
777+ }
778+ }
779+
780+ /**
781+ * Get the unique key of provided price
782+ *
783+ * @param array $priceData
784+ * @param string $baseCurrency
785+ * @return string
786+ */
787+ private function getUniqueKey (array $ priceData , string $ baseCurrency ): string
788+ {
789+ $ productEntityLinkField = $ this ->getProductEntityLinkField ();
790+ return sprintf (
791+ '%s-%s-%s-%s-%.4f ' ,
792+ $ baseCurrency ,
793+ $ priceData [$ productEntityLinkField ],
794+ $ priceData ['all_groups ' ],
795+ $ priceData ['customer_group_id ' ],
796+ $ priceData ['qty ' ]
797+ );
798+ }
799+
800+ /**
801+ * Get existing prices in the database
802+ *
803+ * @param int[] $productIds
804+ * @param string $table
805+ * @return array
806+ */
807+ private function getPrices (array $ productIds , string $ table )
808+ {
809+ $ productEntityLinkField = $ this ->getProductEntityLinkField ();
810+ return $ this ->_connection ->fetchAll (
811+ $ this ->_connection ->select ()
812+ ->from (
813+ $ this ->_resourceFactory ->create ()->getTable ($ table ),
814+ [
815+ $ productEntityLinkField ,
816+ 'all_groups ' ,
817+ 'customer_group_id ' ,
818+ 'qty ' ,
819+ 'website_id '
820+ ]
821+ )
822+ ->where (
823+ $ productEntityLinkField . ' IN (?) ' ,
824+ $ productIds
825+ )
826+ );
827+ }
637828}
0 commit comments