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,152 @@ private function getProductEntityLinkField()
634669 }
635670 return $ this ->productEntityLinkField ;
636671 }
672+
673+ /**
674+ * @inheritdoc
675+ */
676+ public function validateData ()
677+ {
678+ $ isDataValidated = $ this ->_dataValidated ;
679+ $ result = parent ::validateData ();
680+ if (!$ isDataValidated
681+ && $ this ->_dataValidated
682+ && \Magento \ImportExport \Model \Import::BEHAVIOR_APPEND === $ this ->getBehavior ()
683+ && !$ this ->_catalogData ->isPriceGlobal ()
684+ ) {
685+ $ this ->validateRowsForDuplicate (self ::TABLE_TIER_PRICE );
686+ }
687+ return $ result ;
688+ }
689+
690+ /**
691+ * Validate all row data with existing prices in the database for duplicate
692+ *
693+ * A row is considered a duplicate if the pair (product_id, all_groups, customer_group_id, qty) exists for
694+ * both global and website scopes. And the base currency is the same for both global and website scopes.
695+ *
696+ * @param string $table
697+ */
698+ private function validateRowsForDuplicate (string $ table ): void
699+ {
700+ if (!empty ($ this ->allProductIds )) {
701+ $ priceDataCollection = $ this ->getPrices (array_keys ($ this ->allProductIds ), $ table );
702+ $ defaultBaseCurrency = $ this ->currencyResolver ->getDefaultBaseCurrency ();
703+ $ websiteCodeBaseCurrencyMap = $ this ->currencyResolver ->getWebsitesBaseCurrency ();
704+ $ websiteIdCodeMap = array_flip ($ this ->_storeResolver ->getWebsiteCodeToId ());
705+ foreach ($ priceDataCollection as $ priceData ) {
706+ $ isDefaultScope = (int ) $ priceData ['website_id ' ] === 0 ;
707+ $ baseCurrency = $ isDefaultScope
708+ ? $ defaultBaseCurrency
709+ : $ websiteCodeBaseCurrencyMap [$ websiteIdCodeMap [$ priceData ['website_id ' ]] ?? null ] ?? null ;
710+ $ rowNums = [];
711+ $ key = $ this ->getUniqueKey ($ priceData , $ baseCurrency );
712+ if ($ isDefaultScope ) {
713+ if (isset ($ this ->websiteScopeTierPrice [$ key ])) {
714+ $ rowNums = $ this ->websiteScopeTierPrice [$ key ];
715+ }
716+ } else {
717+ if (isset ($ this ->globalScopeTierPrice [$ key ])) {
718+ $ rowNums = $ this ->globalScopeTierPrice [$ key ];
719+ }
720+ }
721+ foreach ($ rowNums as $ rowNum ) {
722+ $ this ->addRowError (self ::ERROR_DUPLICATE_TIER_PRICE , $ rowNum );
723+ }
724+ }
725+ }
726+ }
727+
728+ /**
729+ * Validate row data for duplicate
730+ *
731+ * A row is considered a duplicate if the pair (product_id, all_groups, customer_group_id, qty) exists for
732+ * both global and website scopes. And the base currency is the same for both global and website scopes.
733+ *
734+ * @param array $rowData
735+ * @param int $rowNum
736+ */
737+ private function validateRowForDuplicate (array $ rowData , int $ rowNum )
738+ {
739+ $ productId = $ this ->retrieveOldSkus ()[$ rowData [self ::COL_SKU ]] ?? null ;
740+ if ($ productId && !$ this ->_catalogData ->isPriceGlobal ()) {
741+ $ productEntityLinkField = $ this ->getProductEntityLinkField ();
742+ $ priceData = [
743+ $ productEntityLinkField => $ productId ,
744+ 'website_id ' => (int ) $ this ->getWebSiteId ($ rowData [self ::COL_TIER_PRICE_WEBSITE ]),
745+ 'all_groups ' => $ rowData [self ::COL_TIER_PRICE_CUSTOMER_GROUP ] == self ::VALUE_ALL_GROUPS ? 1 : 0 ,
746+ 'customer_group_id ' => $ this ->getCustomerGroupId ($ rowData [self ::COL_TIER_PRICE_CUSTOMER_GROUP ]),
747+ 'qty ' => $ rowData [self ::COL_TIER_PRICE_QTY ],
748+ ];
749+ $ defaultBaseCurrency = $ this ->currencyResolver ->getDefaultBaseCurrency ();
750+ $ websiteCodeBaseCurrencyMap = $ this ->currencyResolver ->getWebsitesBaseCurrency ();
751+ $ websiteIdCodeMap = array_flip ($ this ->_storeResolver ->getWebsiteCodeToId ());
752+ $ baseCurrency = $ priceData ['website_id ' ] === 0
753+ ? $ defaultBaseCurrency
754+ : $ websiteCodeBaseCurrencyMap [$ websiteIdCodeMap [$ priceData ['website_id ' ]] ?? null ] ?? null ;
755+
756+ $ this ->allProductIds [$ productId ][] = $ rowNum ;
757+ $ key = $ this ->getUniqueKey ($ priceData , $ baseCurrency );
758+ if ($ priceData ['website_id ' ] === 0 ) {
759+ $ this ->globalScopeTierPrice [$ key ][] = $ rowNum ;
760+ if (isset ($ this ->websiteScopeTierPrice [$ key ])) {
761+ $ this ->addRowError (self ::ERROR_DUPLICATE_TIER_PRICE , $ rowNum );
762+ }
763+ } else {
764+ $ this ->websiteScopeTierPrice [$ key ][] = $ rowNum ;
765+ if (isset ($ this ->globalScopeTierPrice [$ key ])) {
766+ $ this ->addRowError (self ::ERROR_DUPLICATE_TIER_PRICE , $ rowNum );
767+ }
768+ }
769+ }
770+ }
771+
772+ /**
773+ * Get the unique key of provided price
774+ *
775+ * @param array $priceData
776+ * @param string $baseCurrency
777+ * @return string
778+ */
779+ private function getUniqueKey (array $ priceData , string $ baseCurrency ): string
780+ {
781+ $ productEntityLinkField = $ this ->getProductEntityLinkField ();
782+ return sprintf (
783+ '%s-%s-%s-%s-%.4f ' ,
784+ $ baseCurrency ,
785+ $ priceData [$ productEntityLinkField ],
786+ $ priceData ['all_groups ' ],
787+ $ priceData ['customer_group_id ' ],
788+ $ priceData ['qty ' ]
789+ );
790+ }
791+
792+ /**
793+ * Get existing prices in the database
794+ *
795+ * @param int[] $productIds
796+ * @param string $table
797+ * @return array
798+ */
799+ private function getPrices (array $ productIds , string $ table )
800+ {
801+ $ productEntityLinkField = $ this ->getProductEntityLinkField ();
802+ return $ this ->_connection ->fetchAll (
803+ $ this ->_connection ->select ()
804+ ->from (
805+ $ this ->_resourceFactory ->create ()->getTable ($ table ),
806+ [
807+ $ productEntityLinkField ,
808+ 'all_groups ' ,
809+ 'customer_group_id ' ,
810+ 'qty ' ,
811+ 'website_id '
812+ ]
813+ )
814+ ->where (
815+ $ productEntityLinkField . ' IN (?) ' ,
816+ $ productIds
817+ )
818+ );
819+ }
637820}
0 commit comments