Skip to content

Commit de30e9c

Browse files
fix delete special prices only for specified store
1 parent 1c3837c commit de30e9c

File tree

2 files changed

+152
-69
lines changed

2 files changed

+152
-69
lines changed

app/code/Magento/Catalog/Model/ResourceModel/Product/Price/SpecialPrice.php

Lines changed: 88 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@
66

77
namespace Magento\Catalog\Model\ResourceModel\Product\Price;
88

9+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
10+
use Magento\Catalog\Api\SpecialPriceInterface;
11+
use Magento\Catalog\Helper\Data;
12+
use Magento\Catalog\Model\ProductIdLocatorInterface;
13+
use Magento\Catalog\Model\ResourceModel\Attribute;
14+
use Magento\Framework\EntityManager\MetadataPool;
15+
use Magento\Framework\Exception\CouldNotDeleteException;
16+
use Magento\Framework\App\ObjectManager;
17+
918
/**
1019
* Special price resource.
1120
*/
12-
class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface
21+
class SpecialPrice implements SpecialPriceInterface
1322
{
1423
/**
1524
* Price storage table.
@@ -26,24 +35,24 @@ class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface
2635
private $datetimeTable = 'catalog_product_entity_datetime';
2736

2837
/**
29-
* @var \Magento\Catalog\Model\ResourceModel\Attribute
38+
* @var Attribute
3039
*/
3140
private $attributeResource;
3241

3342
/**
34-
* @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface
43+
* @var ProductAttributeRepositoryInterface
3544
*/
3645
private $attributeRepository;
3746

3847
/**
39-
* @var \Magento\Catalog\Model\ProductIdLocatorInterface
48+
* @var ProductIdLocatorInterface
4049
*/
4150
private $productIdLocator;
4251

4352
/**
4453
* Metadata pool.
4554
*
46-
* @var \Magento\Framework\EntityManager\MetadataPool
55+
* @var MetadataPool
4756
*/
4857
private $metadataPool;
4958

@@ -68,6 +77,11 @@ class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface
6877
*/
6978
private $priceToAttributeId;
7079

80+
/**
81+
* @var Data
82+
*/
83+
private $catalogData;
84+
7185
/**
7286
* Items per operation.
7387
*
@@ -76,25 +90,28 @@ class SpecialPrice implements \Magento\Catalog\Api\SpecialPriceInterface
7690
private $itemsPerOperation = 500;
7791

7892
/**
79-
* @param \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource
80-
* @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository
81-
* @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator
82-
* @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
93+
* @param Attribute $attributeResource
94+
* @param ProductAttributeRepositoryInterface $attributeRepository
95+
* @param ProductIdLocatorInterface $productIdLocator
96+
* @param MetadataPool $metadataPool
97+
* @param Data|null $catalogData
8398
*/
8499
public function __construct(
85-
\Magento\Catalog\Model\ResourceModel\Attribute $attributeResource,
86-
\Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository,
87-
\Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator,
88-
\Magento\Framework\EntityManager\MetadataPool $metadataPool
100+
Attribute $attributeResource,
101+
ProductAttributeRepositoryInterface $attributeRepository,
102+
ProductIdLocatorInterface $productIdLocator,
103+
MetadataPool $metadataPool,
104+
?Data $catalogData = null
89105
) {
90106
$this->attributeResource = $attributeResource;
91107
$this->attributeRepository = $attributeRepository;
92108
$this->productIdLocator = $productIdLocator;
93109
$this->metadataPool = $metadataPool;
110+
$this->catalogData = $catalogData ?: ObjectManager::getInstance()->get(Data::class);
94111
}
95112

96113
/**
97-
* {@inheritdoc}
114+
* @inheritdoc
98115
*/
99116
public function get(array $skus)
100117
{
@@ -132,7 +149,7 @@ public function get(array $skus)
132149
}
133150

134151
/**
135-
* {@inheritdoc}
152+
* @inheritdoc
136153
*/
137154
public function update(array $prices)
138155
{
@@ -187,47 +204,69 @@ public function update(array $prices)
187204
}
188205

189206
/**
190-
* {@inheritdoc}
207+
* @inheritdoc
191208
*/
192209
public function delete(array $prices)
193210
{
194-
$skus = array_unique(
195-
array_map(function ($price) {
196-
return $price->getSku();
197-
}, $prices)
198-
);
199-
$ids = $this->retrieveAffectedIds($skus);
200211
$connection = $this->attributeResource->getConnection();
201-
$connection->beginTransaction();
202-
try {
203-
foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
204-
$this->attributeResource->getConnection()->delete(
205-
$this->attributeResource->getTable($this->priceTable),
206-
[
207-
'attribute_id = ?' => $this->getPriceAttributeId(),
208-
$this->getEntityLinkField() . ' IN (?)' => $idsBunch
209-
]
210-
);
212+
213+
foreach ($this->getStoreSkus($prices) as $storeId => $skus) {
214+
215+
$ids = $this->retrieveAffectedIds(array_unique($skus));
216+
$connection->beginTransaction();
217+
try {
218+
foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
219+
$connection->delete(
220+
$this->attributeResource->getTable($this->priceTable),
221+
[
222+
'attribute_id = ?' => $this->getPriceAttributeId(),
223+
'store_id = ?' => $storeId,
224+
$this->getEntityLinkField() . ' IN (?)' => $idsBunch
225+
]
226+
);
227+
}
228+
foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
229+
$connection->delete(
230+
$this->attributeResource->getTable($this->datetimeTable),
231+
[
232+
'attribute_id IN (?)' => [$this->getPriceFromAttributeId(), $this->getPriceToAttributeId()],
233+
'store_id = ?' => $storeId,
234+
$this->getEntityLinkField() . ' IN (?)' => $idsBunch
235+
]
236+
);
237+
}
238+
$connection->commit();
239+
} catch (\Exception $e) {
240+
$connection->rollBack();
241+
throw new CouldNotDeleteException(__('Could not delete Prices'), $e);
211242
}
212-
foreach (array_chunk($ids, $this->itemsPerOperation) as $idsBunch) {
213-
$this->attributeResource->getConnection()->delete(
214-
$this->attributeResource->getTable($this->datetimeTable),
215-
[
216-
'attribute_id IN (?)' => [$this->getPriceFromAttributeId(), $this->getPriceToAttributeId()],
217-
$this->getEntityLinkField() . ' IN (?)' => $idsBunch
218-
]
243+
}
244+
245+
return true;
246+
}
247+
248+
/**
249+
* Returns associative arrays of store_id as key and array of skus as value.
250+
*
251+
* @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $priceItems
252+
* @return array
253+
* @throws CouldNotDeleteException
254+
*/
255+
private function getStoreSkus(array $priceItems): array
256+
{
257+
$isPriceGlobal = $this->catalogData->isPriceGlobal();
258+
259+
$storeSkus = [];
260+
foreach ($priceItems as $priceItem) {
261+
if ($isPriceGlobal && $priceItem->getStoreId() !== 0) {
262+
throw new CouldNotDeleteException(
263+
__('Could not delete Prices for non-default store when price scope is global.')
219264
);
220265
}
221-
$connection->commit();
222-
} catch (\Exception $e) {
223-
$connection->rollBack();
224-
throw new \Magento\Framework\Exception\CouldNotDeleteException(
225-
__('Could not delete Prices'),
226-
$e
227-
);
266+
$storeSkus[$priceItem->getStoreId()][] = $priceItem->getSku();
228267
}
229268

230-
return true;
269+
return $storeSkus;
231270
}
232271

233272
/**
@@ -312,9 +351,9 @@ private function retrieveAffectedIds(array $skus)
312351
$affectedIds = [];
313352

314353
foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $productIds) {
315-
$affectedIds = array_merge($affectedIds, array_keys($productIds));
354+
$affectedIds[] = array_keys($productIds);
316355
}
317356

318-
return array_unique($affectedIds);
357+
return array_unique(array_merge([], ...$affectedIds));
319358
}
320359
}

dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@
66
namespace Magento\Catalog\Api;
77

88
use Magento\Catalog\Api\Data\ProductInterface;
9-
use Magento\Framework\Exception\CouldNotSaveException;
10-
use Magento\Framework\Exception\InputException;
11-
use Magento\Framework\Exception\NoSuchEntityException;
12-
use Magento\Framework\Exception\StateException;
139
use Magento\Framework\Webapi\Rest\Request;
10+
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
1411
use Magento\TestFramework\Helper\Bootstrap;
1512
use Magento\TestFramework\ObjectManager;
1613
use Magento\TestFramework\TestCase\WebapiAbstract;
@@ -31,11 +28,17 @@ class SpecialPriceStorageTest extends WebapiAbstract
3128
private $objectManager;
3229

3330
/**
34-
* Set up.
31+
* @var ProductResource
32+
*/
33+
private $productResource;
34+
35+
/**
36+
* @ingeritdoc
3537
*/
3638
protected function setUp(): void
3739
{
3840
$this->objectManager = Bootstrap::getObjectManager();
41+
$this->productResource = $this->objectManager->get(ProductResource::class);
3942
}
4043

4144
/**
@@ -128,28 +131,67 @@ public function testUpdate(array $data)
128131
$this->assertEmpty($response);
129132
}
130133

134+
/**
135+
* Delete special price for specified store when price scope is global
136+
*
137+
* @magentoApiDataFixture Magento/Catalog/_files/product_simple.php
138+
*
139+
* @return void
140+
*/
141+
public function testDeleteWhenPriceIsGlobal(): void
142+
{
143+
$serviceInfo = [
144+
'rest' => [
145+
'resourcePath' => '/V1/products/special-price-delete',
146+
'httpMethod' => Request::HTTP_METHOD_POST
147+
],
148+
'soap' => [
149+
'service' => self::SERVICE_NAME,
150+
'serviceVersion' => self::SERVICE_VERSION,
151+
'operation' => self::SERVICE_NAME . 'Delete',
152+
],
153+
];
154+
155+
$this->expectErrorMessage('Could not delete Prices for non-default store when price scope is global.');
156+
157+
$this->_webApiCall(
158+
$serviceInfo,
159+
[
160+
'prices' => [
161+
['price' => 777, 'store_id' => 1, 'sku' => self::SIMPLE_PRODUCT_SKU]
162+
]
163+
]
164+
);
165+
}
166+
131167
/**
132168
* Test delete method.
133169
*
134170
* @magentoApiDataFixture Magento/Catalog/_files/product_simple.php
171+
* @magentoConfigFixture catalog/price/scope 1
135172
* @dataProvider deleteData
136173
* @param array $data
137-
* @throws CouldNotSaveException
138-
* @throws InputException
139-
* @throws NoSuchEntityException
140-
* @throws StateException
174+
* @return void
141175
*/
142-
public function testDelete(array $data)
176+
public function testDelete(array $data): void
143177
{
144178
/** @var ProductRepositoryInterface $productRepository */
145179
$productRepository = $this->objectManager->create(ProductRepositoryInterface::class);
146-
$product = $productRepository->get($data['sku'], true);
180+
$product = $productRepository->get($data['sku'], true, $data['store_id'], true);
147181
$product->setData('special_price', $data['price']);
148182
$product->setData('special_from_date', $data['price_from']);
149183
if ($data['price_to']) {
150184
$product->setData('special_to_date', $data['price_to']);
151185
}
152-
$productRepository->save($product);
186+
$this->productResource->saveAttribute($product, 'special_price');
187+
$this->productResource->saveAttribute($product, 'special_from_date');
188+
$this->productResource->saveAttribute($product, 'special_to_date');
189+
190+
$product->setData('store_id', 1);
191+
$this->productResource->saveAttribute($product, 'special_price');
192+
$this->productResource->saveAttribute($product, 'special_from_date');
193+
$this->productResource->saveAttribute($product, 'special_to_date');
194+
153195
$serviceInfo = [
154196
'rest' => [
155197
'resourcePath' => '/V1/products/special-price-delete',
@@ -161,17 +203,21 @@ public function testDelete(array $data)
161203
'operation' => self::SERVICE_NAME . 'Delete',
162204
],
163205
];
206+
164207
$response = $this->_webApiCall(
165208
$serviceInfo,
166209
[
167-
'prices' => [
168-
$data
169-
]
210+
'prices' => [$data]
170211
]
171212
);
172-
$product = $productRepository->get($data['sku'], false, null, true);
213+
173214
$this->assertEmpty($response);
215+
216+
$product = $productRepository->get($data['sku'], false, $data['store_id'], true);
174217
$this->assertNull($product->getSpecialPrice());
218+
219+
$product = $productRepository->get($data['sku'], false, 1, true);
220+
$this->assertNotNull($product->getSpecialPrice());
175221
}
176222

177223
/**
@@ -219,8 +265,7 @@ public function deleteData(): array
219265
$toDate = '2038-01-19 03:14:07';
220266

221267
return [
222-
[
223-
// data set with 'price_to' specified
268+
'data set with price_to specified' => [
224269
[
225270
'price' => 3057,
226271
'store_id' => 0,
@@ -229,8 +274,7 @@ public function deleteData(): array
229274
'price_to' => $toDate
230275
]
231276
],
232-
[
233-
// data set without 'price_to' specified
277+
'data set without price_to specified' => [
234278
[
235279
'price' => 3057,
236280
'store_id' => 0,

0 commit comments

Comments
 (0)