diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 6e5393c3..15e131ad 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -29,6 +29,7 @@
->in(__DIR__ . '/src/Services/SonetGroup/')
->in(__DIR__ . '/src/Services/IMOpenLines/')
->in(__DIR__ . '/src/Services/Landing/')
+ ->in(__DIR__ . '/src/Services/Catalog/')
->name('*.php')
->exclude(['vendor', 'storage', 'docker', 'docs']) // Exclude directories
->ignoreDotFiles(true)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4bc3b52..6cfc732e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,15 @@
### Added
+- Added service `Services\Catalog\ProductPropertyFeature` with support for `catalog.productPropertyFeature.*`
+ methods,
+ see [catalog.productPropertyFeature.* methods](https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/index.html) ([#553](https://github.com/bitrix24/b24phpsdk/issues/553)):
+ - `add` adds a parameter (feature) for a product or variation property
+ - `update` updates a parameter of a product or variation property by id
+ - `get` returns a product or variation property parameter by id
+ - `list` returns the list of product/variation property parameters matching the filter
+ - `getAvailableFeaturesByProperty` returns the list of available parameters for a given property
+ - `getFields` returns the description of product/variation property parameter fields
- Added service `Services\Landing\Site\Service\Site` with support methods,
see [landing.site.* methods](https://github.com/bitrix24/b24phpsdk/issues/267):
- `add` adds a site
diff --git a/Makefile b/Makefile
index c2fc4a9a..f0b19198 100644
--- a/Makefile
+++ b/Makefile
@@ -492,6 +492,10 @@ test-integration-landing-role:
test-integration-landing-repowidget:
docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_landing_repowidget
+.PHONY: test-integration-catalog-product-property-feature
+test-integration-catalog-product-property-feature:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_catalog_product_property_feature
+
# work dev environment
.PHONY: php-dev-server-up
php-dev-server-up:
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d0187ae5..52888893 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -262,6 +262,9 @@
./tests/Integration/Services/Landing/RepoWidget/
+
+ ./tests/Integration/Services/Catalog/ProductPropertyFeature/
+
diff --git a/src/Services/Catalog/Catalog/Result/CatalogItemResult.php b/src/Services/Catalog/Catalog/Result/CatalogItemResult.php
index e62fc7cb..189852bb 100644
--- a/src/Services/Catalog/Catalog/Result/CatalogItemResult.php
+++ b/src/Services/Catalog/Catalog/Result/CatalogItemResult.php
@@ -29,4 +29,4 @@
*/
class CatalogItemResult extends AbstractCatalogItem
{
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Catalog/Result/CatalogResult.php b/src/Services/Catalog/Catalog/Result/CatalogResult.php
index aa468aa8..c5222f11 100644
--- a/src/Services/Catalog/Catalog/Result/CatalogResult.php
+++ b/src/Services/Catalog/Catalog/Result/CatalogResult.php
@@ -21,4 +21,4 @@ public function catalog(): CatalogItemResult
{
return new CatalogItemResult($this->getCoreResponse()->getResponseData()->getResult()['catalog']);
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Catalog/Result/CatalogsResult.php b/src/Services/Catalog/Catalog/Result/CatalogsResult.php
index add5f259..f721c3bb 100644
--- a/src/Services/Catalog/Catalog/Result/CatalogsResult.php
+++ b/src/Services/Catalog/Catalog/Result/CatalogsResult.php
@@ -32,4 +32,4 @@ public function getCatalogs(): array
return $res;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Catalog/Service/Catalog.php b/src/Services/Catalog/Catalog/Service/Catalog.php
index d9cb55fe..0da76ea3 100644
--- a/src/Services/Catalog/Catalog/Service/Catalog.php
+++ b/src/Services/Catalog/Catalog/Service/Catalog.php
@@ -82,4 +82,4 @@ public function fields(): FieldsResult
{
return new FieldsResult($this->core->call('catalog.catalog.getFields'));
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/CatalogServiceBuilder.php b/src/Services/Catalog/CatalogServiceBuilder.php
index 57c91b88..b28d9366 100644
--- a/src/Services/Catalog/CatalogServiceBuilder.php
+++ b/src/Services/Catalog/CatalogServiceBuilder.php
@@ -17,6 +17,7 @@
use Bitrix24\SDK\Core\Credentials\Scope;
use Bitrix24\SDK\Services\AbstractServiceBuilder;
use Bitrix24\SDK\Services\Catalog;
+
#[ApiServiceBuilderMetadata(new Scope(['catalog']))]
class CatalogServiceBuilder extends AbstractServiceBuilder
{
@@ -44,4 +45,23 @@ public function catalog(): Catalog\Catalog\Service\Catalog
return $this->serviceCache[__METHOD__];
}
-}
\ No newline at end of file
+
+ public function productPropertyFeature(): Catalog\ProductPropertyFeature\Service\ProductPropertyFeature
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ // Use specialized Batch for ProductPropertyFeature to ensure correct REST parameter mapping
+ // (lowercase 'id' key, unlike the base Batch default of uppercase 'ID')
+ $productPropertyFeatureBatch = new Catalog\ProductPropertyFeature\Batch(
+ $this->core,
+ $this->log
+ );
+ $this->serviceCache[__METHOD__] = new Catalog\ProductPropertyFeature\Service\ProductPropertyFeature(
+ new Catalog\ProductPropertyFeature\Service\Batch($productPropertyFeatureBatch, $this->log),
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+}
diff --git a/src/Services/Catalog/Common/ProductType.php b/src/Services/Catalog/Common/ProductType.php
index 0bf8ef09..ad568a47 100644
--- a/src/Services/Catalog/Common/ProductType.php
+++ b/src/Services/Catalog/Common/ProductType.php
@@ -20,4 +20,4 @@ enum ProductType: int
case SKU = 3;
case productOffer = 4;
case genericOffer = 5;
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Common/Result/AbstractCatalogItem.php b/src/Services/Catalog/Common/Result/AbstractCatalogItem.php
index e2929d73..9761daed 100644
--- a/src/Services/Catalog/Common/Result/AbstractCatalogItem.php
+++ b/src/Services/Catalog/Common/Result/AbstractCatalogItem.php
@@ -109,4 +109,4 @@ protected function getKeyWithUserfieldByFieldName(string $fieldName)
return $this->$fieldName;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Result/ProductItemResult.php b/src/Services/Catalog/Product/Result/ProductItemResult.php
index c576d9b7..58c80851 100644
--- a/src/Services/Catalog/Product/Result/ProductItemResult.php
+++ b/src/Services/Catalog/Product/Result/ProductItemResult.php
@@ -52,4 +52,4 @@
*/
class ProductItemResult extends AbstractCatalogItem
{
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Result/ProductResult.php b/src/Services/Catalog/Product/Result/ProductResult.php
index 2751fda8..fc83fa8c 100644
--- a/src/Services/Catalog/Product/Result/ProductResult.php
+++ b/src/Services/Catalog/Product/Result/ProductResult.php
@@ -26,4 +26,4 @@ public function product(): ProductItemResult
return new ProductItemResult($this->getCoreResponse()->getResponseData()->getResult()['product']);
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Result/ProductsResult.php b/src/Services/Catalog/Product/Result/ProductsResult.php
index aa644fa3..268d6d8f 100644
--- a/src/Services/Catalog/Product/Result/ProductsResult.php
+++ b/src/Services/Catalog/Product/Result/ProductsResult.php
@@ -31,4 +31,4 @@ public function getProducts(): array
return $res;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Service/Batch.php b/src/Services/Catalog/Product/Service/Batch.php
index 45dddcb7..42970546 100644
--- a/src/Services/Catalog/Product/Service/Batch.php
+++ b/src/Services/Catalog/Product/Service/Batch.php
@@ -25,7 +25,7 @@
{
public function __construct(
protected BatchOperationsInterface $batch,
- protected LoggerInterface $log)
- {
+ protected LoggerInterface $log
+ ) {
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/Product/Service/Product.php b/src/Services/Catalog/Product/Service/Product.php
index 730fd7c9..d37d34f6 100644
--- a/src/Services/Catalog/Product/Service/Product.php
+++ b/src/Services/Catalog/Product/Service/Product.php
@@ -25,7 +25,6 @@
use Bitrix24\SDK\Services\Catalog\Common\ProductType;
use Bitrix24\SDK\Services\Catalog\Product\Result\ProductResult;
use Bitrix24\SDK\Services\Catalog\Product\Result\ProductsResult;
-
use Psr\Log\LoggerInterface;
#[ApiServiceMetadata(new Scope(['catalog']))]
@@ -35,8 +34,7 @@ public function __construct(
public Batch $batch,
CoreInterface $core,
LoggerInterface $logger
- )
- {
+ ) {
parent::__construct($core, $logger);
}
@@ -71,7 +69,9 @@ public function get(int $productId): ProductResult
)]
public function add(array $productFields): ProductResult
{
- return new ProductResult($this->core->call('catalog.product.add', [
+ return new ProductResult($this->core->call(
+ 'catalog.product.add',
+ [
'fields' => $productFields
]
));
@@ -140,4 +140,4 @@ public function fieldsByFilter(int $iblockId, ProductType $productType, ?array $
return new FieldsResult($this->core->call('catalog.product.getFieldsByFilter', ['filter' => $filter]));
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Batch.php b/src/Services/Catalog/ProductPropertyFeature/Batch.php
new file mode 100644
index 00000000..c4bccb19
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Batch.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature;
+
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+
+/**
+ * Class Batch
+ *
+ * Overrides base Batch to handle differences in catalog.productPropertyFeature.* REST methods:
+ * - the id key is lowercase 'id', not 'ID' as assumed by the base class default
+ * - catalog.productPropertyFeature.list wraps list items under the 'productPropertyFeatures' key
+ * instead of returning a flat array in 'result', as the base class assumes for non-CRM methods
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-list.html
+ */
+class Batch extends \Bitrix24\SDK\Core\Batch
+{
+ /**
+ * Determines the ID key — lowercase 'id' for catalog.productPropertyFeature.*
+ */
+ #[\Override]
+ protected function determineKeyId(string $apiMethod, ?array $additionalParameters): string
+ {
+ return 'id';
+ }
+
+ /**
+ * Extracts list items from the 'productPropertyFeatures' key of the batch/list result
+ */
+ #[\Override]
+ protected function extractElementsFromBatchResult(ResponseData $responseData, bool $isCrmItemsInBatch): array
+ {
+ return $responseData->getResult()['productPropertyFeatures'] ?? [];
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/AvailableFeatureItemResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/AvailableFeatureItemResult.php
new file mode 100644
index 00000000..a8aa02d2
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/AvailableFeatureItemResult.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read string $featureId
+ * @property-read string $featureName
+ * @property-read string $moduleId
+ */
+class AvailableFeatureItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/AvailableFeaturesResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/AvailableFeaturesResult.php
new file mode 100644
index 00000000..48c941ee
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/AvailableFeaturesResult.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class AvailableFeaturesResult extends AbstractResult
+{
+ /**
+ * @return AvailableFeatureItemResult[]
+ * @throws BaseException
+ */
+ public function features(): array
+ {
+ $items = [];
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+ foreach (($result['features'] ?? []) as $item) {
+ $items[] = new AvailableFeatureItemResult($item);
+ }
+
+ return $items;
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureAddedBatchResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureAddedBatchResult.php
new file mode 100644
index 00000000..48a5ccf6
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureAddedBatchResult.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Result\AddedItemBatchResult;
+
+class ProductPropertyFeatureAddedBatchResult extends AddedItemBatchResult
+{
+ #[\Override]
+ public function getId(): int
+ {
+ return (int)($this->getResponseData()->getResult()['productPropertyFeature']['id'] ?? 0);
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureAddedResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureAddedResult.php
new file mode 100644
index 00000000..36da9e5f
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureAddedResult.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+
+class ProductPropertyFeatureAddedResult extends ProductPropertyFeatureResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function getId(): int
+ {
+ return (int)($this->productPropertyFeature()->id ?? 0);
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureFieldsResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureFieldsResult.php
new file mode 100644
index 00000000..722dd97d
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureFieldsResult.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\FieldsResult;
+
+class ProductPropertyFeatureFieldsResult extends FieldsResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function getFieldsDescription(): array
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()['productPropertyFeature'] ?? [];
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureItemResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureItemResult.php
new file mode 100644
index 00000000..9ced4ecb
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureItemResult.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $id
+ * @property-read int $propertyId
+ * @property-read string $moduleId
+ * @property-read string $featureId
+ * @property-read bool $isEnabled
+ */
+class ProductPropertyFeatureItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return bool|int|mixed|null
+ */
+ public function __get($offset)
+ {
+ switch ($offset) {
+ case 'id':
+ case 'propertyId':
+ if ($this->data[$offset] !== '' && $this->data[$offset] !== null) {
+ return (int)$this->data[$offset];
+ }
+
+ return null;
+ case 'isEnabled':
+ return $this->data[$offset] === 'Y';
+ default:
+ return $this->data[$offset] ?? null;
+ }
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureResult.php
new file mode 100644
index 00000000..7cd312a9
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureResult.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class ProductPropertyFeatureResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function productPropertyFeature(): ProductPropertyFeatureItemResult
+ {
+ return new ProductPropertyFeatureItemResult(
+ $this->getCoreResponse()->getResponseData()->getResult()['productPropertyFeature'] ?? []
+ );
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureUpdatedBatchResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureUpdatedBatchResult.php
new file mode 100644
index 00000000..99909db7
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureUpdatedBatchResult.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Result\UpdatedItemBatchResult;
+
+class ProductPropertyFeatureUpdatedBatchResult extends UpdatedItemBatchResult
+{
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return isset($this->getResponseData()->getResult()['productPropertyFeature']);
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureUpdatedResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureUpdatedResult.php
new file mode 100644
index 00000000..93314758
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureUpdatedResult.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class ProductPropertyFeatureUpdatedResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function productPropertyFeature(): ProductPropertyFeatureItemResult
+ {
+ return new ProductPropertyFeatureItemResult(
+ $this->getCoreResponse()->getResponseData()->getResult()['productPropertyFeature'] ?? []
+ );
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function isSuccess(): bool
+ {
+ return isset($this->getCoreResponse()->getResponseData()->getResult()['productPropertyFeature']);
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeaturesResult.php b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeaturesResult.php
new file mode 100644
index 00000000..319a083b
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeaturesResult.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class ProductPropertyFeaturesResult extends AbstractResult
+{
+ /**
+ * @return ProductPropertyFeatureItemResult[]
+ * @throws BaseException
+ */
+ public function productPropertyFeatures(): array
+ {
+ $items = [];
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+ foreach (($result['productPropertyFeatures'] ?? []) as $item) {
+ $items[] = new ProductPropertyFeatureItemResult($item);
+ }
+
+ return $items;
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Service/Batch.php b/src/Services/Catalog/ProductPropertyFeature/Service/Batch.php
new file mode 100644
index 00000000..5001f8ab
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Service/Batch.php
@@ -0,0 +1,141 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service;
+
+use Bitrix24\SDK\Attributes\ApiBatchMethodMetadata;
+use Bitrix24\SDK\Attributes\ApiBatchServiceMetadata;
+use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureAddedBatchResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureItemResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureUpdatedBatchResult;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+#[ApiBatchServiceMetadata(new Scope(['catalog']))]
+class Batch
+{
+ public function __construct(protected BatchOperationsInterface $batch, protected LoggerInterface $log)
+ {
+ }
+
+ /**
+ * Batch list product/variation property parameters.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-list.html
+ *
+ * @param array $order
+ * @param array $filter
+ * @param array $select
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'catalog.productPropertyFeature.list',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-list.html',
+ 'Batch list product/variation property parameters'
+ )]
+ public function list(array $order = [], array $filter = [], array $select = [], ?int $limit = null): Generator
+ {
+ $this->log->debug(
+ 'batchList',
+ [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'limit' => $limit,
+ ]
+ );
+
+ foreach (
+ $this->batch->getTraversableListWithCount(
+ 'catalog.productPropertyFeature.list',
+ $order,
+ $filter,
+ $select,
+ $limit
+ ) as $key => $value
+ ) {
+ yield $key => new ProductPropertyFeatureItemResult($value);
+ }
+ }
+
+ /**
+ * Batch add product/variation property parameters.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-add.html
+ *
+ * @param array $productPropertyFeatures
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'catalog.productPropertyFeature.add',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-add.html',
+ 'Batch add product/variation property parameters'
+ )]
+ public function add(array $productPropertyFeatures): Generator
+ {
+ $items = [];
+ foreach ($productPropertyFeatures as $item) {
+ $items[] = [
+ 'fields' => $item,
+ ];
+ }
+
+ foreach ($this->batch->addEntityItems('catalog.productPropertyFeature.add', $items) as $key => $item) {
+ yield $key => new ProductPropertyFeatureAddedBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch update product/variation property parameters.
+ *
+ * Update elements in array with structure:
+ * id => [
+ * 'fields' => [] // fields to update
+ * ]
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-update.html
+ *
+ * @param array $entityItems
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'catalog.productPropertyFeature.update',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-update.html',
+ 'Batch update product/variation property parameters'
+ )]
+ public function update(array $entityItems): Generator
+ {
+ foreach (
+ $this->batch->updateEntityItems(
+ 'catalog.productPropertyFeature.update',
+ $entityItems
+ ) as $key => $item
+ ) {
+ yield $key => new ProductPropertyFeatureUpdatedBatchResult($item);
+ }
+ }
+}
diff --git a/src/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeature.php b/src/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeature.php
new file mode 100644
index 00000000..35770c53
--- /dev/null
+++ b/src/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeature.php
@@ -0,0 +1,199 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Contracts\CoreInterface;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\AvailableFeaturesResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureAddedResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureFieldsResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureUpdatedResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeaturesResult;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['catalog']))]
+class ProductPropertyFeature extends AbstractService
+{
+ public function __construct(public Batch $batch, CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Adds a parameter (feature) for a product or variation property.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-add.html
+ *
+ * @param array{
+ * propertyId: int,
+ * moduleId: string,
+ * featureId: string,
+ * isEnabled: string,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.productPropertyFeature.add',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-add.html',
+ 'Adds a parameter of a product or variation property'
+ )]
+ public function add(array $fields): ProductPropertyFeatureAddedResult
+ {
+ return new ProductPropertyFeatureAddedResult(
+ $this->core->call('catalog.productPropertyFeature.add', [
+ 'fields' => $fields,
+ ])
+ );
+ }
+
+ /**
+ * Updates a parameter of a product or variation property by id.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-update.html
+ *
+ * @param array{
+ * propertyId: int,
+ * moduleId: string,
+ * featureId: string,
+ * isEnabled: string,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws InvalidArgumentException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.productPropertyFeature.update',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-update.html',
+ 'Updates a parameter of a product or variation property'
+ )]
+ public function update(int $id, array $fields): ProductPropertyFeatureUpdatedResult
+ {
+ $this->guardPositiveId($id);
+
+ return new ProductPropertyFeatureUpdatedResult(
+ $this->core->call('catalog.productPropertyFeature.update', [
+ 'id' => $id,
+ 'fields' => $fields,
+ ])
+ );
+ }
+
+ /**
+ * Returns a product or variation property parameter by id.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-get.html
+ *
+ * @throws BaseException
+ * @throws InvalidArgumentException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.productPropertyFeature.get',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-get.html',
+ 'Returns a product or variation property parameter by id'
+ )]
+ public function get(int $id): ProductPropertyFeatureResult
+ {
+ $this->guardPositiveId($id);
+
+ return new ProductPropertyFeatureResult(
+ $this->core->call('catalog.productPropertyFeature.get', [
+ 'id' => $id,
+ ])
+ );
+ }
+
+ /**
+ * Returns the list of product/variation property parameters matching the filter.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-list.html
+ *
+ * @param array $select Fields to select
+ * @param array $filter Filter map
+ * @param array $order Sort order map
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.productPropertyFeature.list',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-list.html',
+ 'Returns the list of product/variation property parameters'
+ )]
+ public function list(array $select = [], array $filter = [], array $order = []): ProductPropertyFeaturesResult
+ {
+ return new ProductPropertyFeaturesResult(
+ $this->core->call('catalog.productPropertyFeature.list', [
+ 'select' => $select,
+ 'filter' => $filter,
+ 'order' => $order,
+ ])
+ );
+ }
+
+ /**
+ * Returns the list of available parameters (features) for the given product or variation property.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-get-available-features-by-property.html
+ *
+ * @throws BaseException
+ * @throws InvalidArgumentException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.productPropertyFeature.getAvailableFeaturesByProperty',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-get-available-features-by-property.html',
+ 'Returns the list of available parameters for the given product or variation property'
+ )]
+ public function getAvailableFeaturesByProperty(int $propertyId): AvailableFeaturesResult
+ {
+ $this->guardPositiveId($propertyId);
+
+ return new AvailableFeaturesResult(
+ $this->core->call('catalog.productPropertyFeature.getAvailableFeaturesByProperty', [
+ 'propertyId' => $propertyId,
+ ])
+ );
+ }
+
+ /**
+ * Returns the description of product/variation property parameter fields.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-get-fields.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.productPropertyFeature.getFields',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/product-property-feature/catalog-product-property-feature-get-fields.html',
+ 'Returns the description of product/variation property parameter fields'
+ )]
+ public function getFields(): ProductPropertyFeatureFieldsResult
+ {
+ return new ProductPropertyFeatureFieldsResult(
+ $this->core->call('catalog.productPropertyFeature.getFields')
+ );
+ }
+}
diff --git a/tests/Integration/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureItemResultTest.php b/tests/Integration/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureItemResultTest.php
new file mode 100644
index 00000000..0a97825d
--- /dev/null
+++ b/tests/Integration/Services/Catalog/ProductPropertyFeature/Result/ProductPropertyFeatureItemResultTest.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\Catalog\ProductPropertyFeature\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureItemResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service\ProductPropertyFeature;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(ProductPropertyFeatureItemResult::class)]
+class ProductPropertyFeatureItemResultTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private ProductPropertyFeature $productPropertyFeatureService;
+
+ private int $propertyId;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->productPropertyFeatureService = Fabric::getServiceBuilder()->getCatalogScope()->productPropertyFeature();
+ $this->propertyId = $this->createProductProperty();
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ $this->deleteProductProperty($this->propertyId);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in ProductPropertyFeatureItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllFieldsAreAnnotated(): void
+ {
+ $rawFields = $this->productPropertyFeatureService->getFields()->getFieldsDescription();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(array_keys($rawFields), ProductPropertyFeatureItemResult::class);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[Test]
+ #[TestDox('all fields in ProductPropertyFeatureItemResult have valid type casting in magic getters')]
+ public function testAllFieldsHasValidTypeCastingInMagicGetters(): void
+ {
+ $productPropertyFeatureItemResult = $this->productPropertyFeatureService->add([
+ 'propertyId' => $this->propertyId,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ])->productPropertyFeature();
+
+ self::assertIsInt($productPropertyFeatureItemResult->id);
+ self::assertIsInt($productPropertyFeatureItemResult->propertyId);
+ self::assertIsString($productPropertyFeatureItemResult->moduleId);
+ self::assertIsString($productPropertyFeatureItemResult->featureId);
+ self::assertIsBool($productPropertyFeatureItemResult->isEnabled);
+ }
+
+ protected function createProductProperty(): int
+ {
+ $core = Fabric::getCore();
+ $iblockId = (int)$core->call('catalog.catalog.list', [
+ 'select' => ['id', 'iblockId'],
+ ])->getResponseData()->getResult()['catalogs'][0]['iblockId'];
+
+ return (int)$core->call('catalog.productProperty.add', [
+ 'fields' => [
+ 'iblockId' => $iblockId,
+ 'name' => 'SDK Test Property ' . uniqid('', true),
+ 'propertyType' => 'S',
+ 'active' => 'Y',
+ 'sort' => 100,
+ ],
+ ])->getResponseData()->getResult()['productProperty']['id'];
+ }
+
+ protected function deleteProductProperty(int $id): void
+ {
+ $core = Fabric::getCore();
+ $core->call('catalog.productProperty.delete', [
+ 'id' => $id,
+ ]);
+ }
+}
diff --git a/tests/Integration/Services/Catalog/ProductPropertyFeature/Service/BatchTest.php b/tests/Integration/Services/Catalog/ProductPropertyFeature/Service/BatchTest.php
new file mode 100644
index 00000000..1c2a840b
--- /dev/null
+++ b/tests/Integration/Services/Catalog/ProductPropertyFeature/Service/BatchTest.php
@@ -0,0 +1,151 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\Catalog\ProductPropertyFeature\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureItemResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service\Batch;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service\ProductPropertyFeature;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ private ProductPropertyFeature $service;
+
+ /** @var int[] */
+ private array $propertyIds = [];
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->service = Fabric::getServiceBuilder()->getCatalogScope()->productPropertyFeature();
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ foreach ($this->propertyIds as $propertyId) {
+ $this->deleteProductProperty($propertyId);
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchAddAndList(): void
+ {
+ $propertyId = $this->createProductProperty();
+
+ $items = [
+ [
+ 'propertyId' => $propertyId,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ],
+ [
+ 'propertyId' => $propertyId,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'DETAIL_PAGE_SHOW',
+ 'isEnabled' => 'N',
+ ],
+ ];
+
+ $addedIds = [];
+ foreach ($this->service->batch->add($items) as $result) {
+ $addedIds[] = $result->getId();
+ self::assertGreaterThan(0, $result->getId());
+ }
+
+ self::assertCount(2, $addedIds);
+
+ $found = [];
+ foreach ($this->service->batch->list(['id' => 'asc'], ['propertyId' => $propertyId]) as $item) {
+ self::assertInstanceOf(ProductPropertyFeatureItemResult::class, $item);
+ $found[] = $item->id;
+ }
+
+ foreach ($addedIds as $addedId) {
+ self::assertContains($addedId, $found);
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchUpdate(): void
+ {
+ $propertyId = $this->createProductProperty();
+
+ $addedId = $this->service->add([
+ 'propertyId' => $propertyId,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ])->getId();
+
+ $entityItems = [
+ $addedId => [
+ 'fields' => [
+ 'propertyId' => $propertyId,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'N',
+ ],
+ ],
+ ];
+
+ foreach ($this->service->batch->update($entityItems) as $result) {
+ self::assertTrue($result->isSuccess());
+ }
+
+ self::assertFalse($this->service->get($addedId)->productPropertyFeature()->isEnabled);
+ }
+
+ protected function createProductProperty(): int
+ {
+ $core = Fabric::getCore();
+ $iblockId = (int)$core->call('catalog.catalog.list', [
+ 'select' => ['id', 'iblockId'],
+ ])->getResponseData()->getResult()['catalogs'][0]['iblockId'];
+
+ $propertyId = (int)$core->call('catalog.productProperty.add', [
+ 'fields' => [
+ 'iblockId' => $iblockId,
+ 'name' => 'SDK Batch Test Property ' . uniqid('', true),
+ 'propertyType' => 'S',
+ 'active' => 'Y',
+ 'sort' => 100,
+ ],
+ ])->getResponseData()->getResult()['productProperty']['id'];
+
+ $this->propertyIds[] = $propertyId;
+
+ return $propertyId;
+ }
+
+ protected function deleteProductProperty(int $id): void
+ {
+ $core = Fabric::getCore();
+ $core->call('catalog.productProperty.delete', [
+ 'id' => $id,
+ ]);
+ }
+}
diff --git a/tests/Integration/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeatureTest.php b/tests/Integration/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeatureTest.php
new file mode 100644
index 00000000..78731807
--- /dev/null
+++ b/tests/Integration/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeatureTest.php
@@ -0,0 +1,137 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Integration\Services\Catalog\ProductPropertyFeature\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureItemResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service\ProductPropertyFeature;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\TestCase;
+
+#[CoversMethod(ProductPropertyFeature::class, 'add')]
+#[CoversMethod(ProductPropertyFeature::class, 'update')]
+#[CoversMethod(ProductPropertyFeature::class, 'get')]
+#[CoversMethod(ProductPropertyFeature::class, 'list')]
+#[CoversMethod(ProductPropertyFeature::class, 'getAvailableFeaturesByProperty')]
+#[CoversMethod(ProductPropertyFeature::class, 'getFields')]
+#[CoversClass(ProductPropertyFeature::class)]
+class ProductPropertyFeatureTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private ProductPropertyFeature $service;
+
+ private int $propertyId;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->service = Fabric::getServiceBuilder()->getCatalogScope()->productPropertyFeature();
+ $this->propertyId = $this->createProductProperty();
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ $this->deleteProductProperty($this->propertyId);
+ }
+
+ public function testFields(): void
+ {
+ $fields = $this->service->getFields()->getFieldsDescription();
+ self::assertIsArray($fields);
+ $this->assertNotEmpty($fields);
+ $this->assertBitrix24AllResultItemFieldsAnnotated(array_keys($fields), ProductPropertyFeatureItemResult::class);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAddGetUpdateList(): void
+ {
+ $productPropertyFeatureAddedResult = $this->service->add([
+ 'propertyId' => $this->propertyId,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ]);
+ $id = $productPropertyFeatureAddedResult->getId();
+ self::assertGreaterThan(0, $id);
+
+ $productPropertyFeatureItemResult = $this->service->get($id)->productPropertyFeature();
+ self::assertEquals($this->propertyId, $productPropertyFeatureItemResult->propertyId);
+ self::assertEquals('iblock', $productPropertyFeatureItemResult->moduleId);
+ self::assertEquals('LIST_PAGE_SHOW', $productPropertyFeatureItemResult->featureId);
+ self::assertTrue($productPropertyFeatureItemResult->isEnabled);
+
+ $productPropertyFeatureUpdatedResult = $this->service->update($id, [
+ 'propertyId' => $this->propertyId,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'N',
+ ]);
+ self::assertTrue($productPropertyFeatureUpdatedResult->isSuccess());
+ self::assertFalse($this->service->get($id)->productPropertyFeature()->isEnabled);
+
+ $list = $this->service->list(
+ ['id', 'propertyId', 'moduleId', 'featureId', 'isEnabled'],
+ ['propertyId' => $this->propertyId],
+ ['id' => 'asc']
+ )->productPropertyFeatures();
+ self::assertNotEmpty($list);
+ self::assertEquals($id, $list[0]->id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testGetAvailableFeaturesByProperty(): void
+ {
+ $features = $this->service->getAvailableFeaturesByProperty($this->propertyId)->features();
+ self::assertNotEmpty($features);
+ self::assertEquals('iblock', $features[0]->moduleId);
+ }
+
+ protected function createProductProperty(): int
+ {
+ $core = Fabric::getCore();
+ $iblockId = (int)$core->call('catalog.catalog.list', [
+ 'select' => ['id', 'iblockId'],
+ ])->getResponseData()->getResult()['catalogs'][0]['iblockId'];
+
+ return (int)$core->call('catalog.productProperty.add', [
+ 'fields' => [
+ 'iblockId' => $iblockId,
+ 'name' => 'SDK Test Property ' . uniqid('', true),
+ 'propertyType' => 'S',
+ 'active' => 'Y',
+ 'sort' => 100,
+ ],
+ ])->getResponseData()->getResult()['productProperty']['id'];
+ }
+
+ protected function deleteProductProperty(int $id): void
+ {
+ $core = Fabric::getCore();
+ $core->call('catalog.productProperty.delete', [
+ 'id' => $id,
+ ]);
+ }
+}
diff --git a/tests/Unit/Services/Catalog/ProductPropertyFeature/Service/BatchTest.php b/tests/Unit/Services/Catalog/ProductPropertyFeature/Service/BatchTest.php
new file mode 100644
index 00000000..7e2dc1a8
--- /dev/null
+++ b/tests/Unit/Services/Catalog/ProductPropertyFeature/Service/BatchTest.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Unit\Services\Catalog\ProductPropertyFeature\Service;
+
+use Bitrix24\SDK\Core\Contracts\BatchOperationsInterface;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureAddedBatchResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureItemResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureUpdatedBatchResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service\Batch;
+use Bitrix24\SDK\Tests\Unit\Stubs\NullBatch;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ #[Test]
+ #[TestDox('list() yields ProductPropertyFeatureItemResult items')]
+ public function testListYieldsItemResults(): void
+ {
+ $batch = new Batch(new NullBatch(), new NullLogger());
+
+ foreach ($batch->list() as $item) {
+ $this->assertInstanceOf(ProductPropertyFeatureItemResult::class, $item);
+ }
+
+ $this->addToAssertionCount(1);
+ }
+
+ #[Test]
+ #[TestDox('add() yields ProductPropertyFeatureAddedBatchResult items and forwards fields wrapper')]
+ public function testAddYieldsAddedBatchResults(): void
+ {
+ $batchOperations = $this->createMock(BatchOperationsInterface::class);
+ $batchOperations->expects($this->once())
+ ->method('addEntityItems')
+ ->with(
+ 'catalog.productPropertyFeature.add',
+ [
+ [
+ 'fields' => [
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ],
+ ],
+ ]
+ )
+ ->willReturn((static function (): \Generator {
+ yield from [];
+ })());
+
+ $batch = new Batch($batchOperations, new NullLogger());
+ foreach ($batch->add([
+ [
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ],
+ ]) as $item) {
+ $this->assertInstanceOf(ProductPropertyFeatureAddedBatchResult::class, $item);
+ }
+
+ $this->addToAssertionCount(1);
+ }
+
+ #[Test]
+ #[TestDox('update() yields ProductPropertyFeatureUpdatedBatchResult items')]
+ public function testUpdateYieldsUpdatedBatchResults(): void
+ {
+ $batch = new Batch(new NullBatch(), new NullLogger());
+
+ foreach ($batch->update([101 => ['fields' => ['isEnabled' => 'N']]]) as $item) {
+ $this->assertInstanceOf(ProductPropertyFeatureUpdatedBatchResult::class, $item);
+ }
+
+ $this->addToAssertionCount(1);
+ }
+}
diff --git a/tests/Unit/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeatureTest.php b/tests/Unit/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeatureTest.php
new file mode 100644
index 00000000..6b9c1a88
--- /dev/null
+++ b/tests/Unit/Services/Catalog/ProductPropertyFeature/Service/ProductPropertyFeatureTest.php
@@ -0,0 +1,249 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Tests\Unit\Services\Catalog\ProductPropertyFeature\Service;
+
+use Bitrix24\SDK\Core\ApiLevelErrorHandler;
+use Bitrix24\SDK\Core\Commands\Command;
+use Bitrix24\SDK\Core\Contracts\CoreInterface;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Response\Response;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\AvailableFeaturesResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureAddedResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureFieldsResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureUpdatedResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeaturesResult;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service\Batch;
+use Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Service\ProductPropertyFeature;
+use Bitrix24\SDK\Tests\Unit\Stubs\NullBatch;
+use Bitrix24\SDK\Tests\Unit\Stubs\NullCore;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+use Symfony\Component\HttpClient\Response\MockResponse;
+
+#[CoversClass(ProductPropertyFeature::class)]
+class ProductPropertyFeatureTest extends TestCase
+{
+ private ProductPropertyFeature $service;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->service = new ProductPropertyFeature(
+ new Batch(new NullBatch(), new NullLogger()),
+ new NullCore(),
+ new NullLogger()
+ );
+ }
+
+ #[Test]
+ public function testAddReturnsProductPropertyFeatureAddedResult(): void
+ {
+ $this->assertInstanceOf(
+ ProductPropertyFeatureAddedResult::class,
+ $this->service->add([
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ])
+ );
+ }
+
+ #[Test]
+ public function testUpdateReturnsProductPropertyFeatureUpdatedResult(): void
+ {
+ $this->assertInstanceOf(
+ ProductPropertyFeatureUpdatedResult::class,
+ $this->service->update(101, [
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'N',
+ ])
+ );
+ }
+
+ #[Test]
+ public function testGetReturnsProductPropertyFeatureResult(): void
+ {
+ $this->assertInstanceOf(ProductPropertyFeatureResult::class, $this->service->get(101));
+ }
+
+ #[Test]
+ public function testListReturnsProductPropertyFeaturesResult(): void
+ {
+ $this->assertInstanceOf(ProductPropertyFeaturesResult::class, $this->service->list());
+ }
+
+ #[Test]
+ public function testGetAvailableFeaturesByPropertyReturnsAvailableFeaturesResult(): void
+ {
+ $this->assertInstanceOf(AvailableFeaturesResult::class, $this->service->getAvailableFeaturesByProperty(901));
+ }
+
+ #[Test]
+ public function testGetFieldsReturnsProductPropertyFeatureFieldsResult(): void
+ {
+ $this->assertInstanceOf(ProductPropertyFeatureFieldsResult::class, $this->service->getFields());
+ }
+
+ #[Test]
+ public function testUpdateThrowsExceptionForNonPositiveId(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->update(0, ['propertyId' => 901, 'moduleId' => 'iblock', 'featureId' => 'X', 'isEnabled' => 'Y']);
+ }
+
+ #[Test]
+ public function testGetThrowsExceptionForNonPositiveId(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->get(0);
+ }
+
+ #[Test]
+ public function testGetAvailableFeaturesByPropertyThrowsExceptionForNonPositivePropertyId(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->getAvailableFeaturesByProperty(0);
+ }
+
+ #[Test]
+ #[TestDox('add() calls catalog.productPropertyFeature.add with a nested fields object')]
+ public function testAddSendsNestedFields(): void
+ {
+ [$method, $captured] = $this->call(static fn (ProductPropertyFeature $productPropertyFeature): \Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureAddedResult => $productPropertyFeature->add([
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ]));
+
+ $this->assertSame('catalog.productPropertyFeature.add', $method);
+ $this->assertSame([
+ 'fields' => [
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'Y',
+ ],
+ ], $captured);
+ }
+
+ #[Test]
+ #[TestDox('update() sends id and nested fields')]
+ public function testUpdateSendsIdAndFields(): void
+ {
+ [$method, $captured] = $this->call(static fn (ProductPropertyFeature $productPropertyFeature): \Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureUpdatedResult => $productPropertyFeature->update(101, [
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'N',
+ ]));
+
+ $this->assertSame('catalog.productPropertyFeature.update', $method);
+ $this->assertSame(101, $captured['id']);
+ $this->assertSame([
+ 'propertyId' => 901,
+ 'moduleId' => 'iblock',
+ 'featureId' => 'LIST_PAGE_SHOW',
+ 'isEnabled' => 'N',
+ ], $captured['fields']);
+ }
+
+ #[Test]
+ #[TestDox('get() sends the id')]
+ public function testGetSendsId(): void
+ {
+ [$method, $captured] = $this->call(static fn (ProductPropertyFeature $productPropertyFeature): \Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureResult => $productPropertyFeature->get(101));
+
+ $this->assertSame('catalog.productPropertyFeature.get', $method);
+ $this->assertSame(101, $captured['id']);
+ }
+
+ #[Test]
+ #[TestDox('list() sends select, filter and order')]
+ public function testListSendsSelectFilterOrder(): void
+ {
+ [$method, $captured] = $this->call(static fn (ProductPropertyFeature $productPropertyFeature): \Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeaturesResult => $productPropertyFeature->list(
+ ['id', 'propertyId'],
+ ['propertyId' => 901],
+ ['id' => 'ASC']
+ ));
+
+ $this->assertSame('catalog.productPropertyFeature.list', $method);
+ $this->assertSame(['id', 'propertyId'], $captured['select']);
+ $this->assertSame(['propertyId' => 901], $captured['filter']);
+ $this->assertSame(['id' => 'ASC'], $captured['order']);
+ }
+
+ #[Test]
+ #[TestDox('getAvailableFeaturesByProperty() sends propertyId')]
+ public function testGetAvailableFeaturesByPropertySendsPropertyId(): void
+ {
+ [$method, $captured] = $this->call(
+ static fn (ProductPropertyFeature $productPropertyFeature): \Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\AvailableFeaturesResult => $productPropertyFeature->getAvailableFeaturesByProperty(901)
+ );
+
+ $this->assertSame('catalog.productPropertyFeature.getAvailableFeaturesByProperty', $method);
+ $this->assertSame(901, $captured['propertyId']);
+ }
+
+ #[Test]
+ #[TestDox('getFields() calls catalog.productPropertyFeature.getFields with no parameters')]
+ public function testGetFieldsSendsNoParameters(): void
+ {
+ [$method, $captured] = $this->call(static fn (ProductPropertyFeature $productPropertyFeature): \Bitrix24\SDK\Services\Catalog\ProductPropertyFeature\Result\ProductPropertyFeatureFieldsResult => $productPropertyFeature->getFields());
+
+ $this->assertSame('catalog.productPropertyFeature.getFields', $method);
+ $this->assertSame([], $captured);
+ }
+
+ /**
+ * @return array{0: string, 1: array}
+ */
+ private function call(callable $action): array
+ {
+ $method = null;
+ $captured = [];
+ $response = new Response(
+ new MockResponse(''),
+ new Command('', []),
+ new ApiLevelErrorHandler(new NullLogger()),
+ new NullLogger()
+ );
+
+ $core = $this->createStub(CoreInterface::class);
+ $core->method('call')->willReturnCallback(
+ function (string $apiMethod, array $parameters = []) use (&$method, &$captured, $response): Response {
+ $method = $apiMethod;
+ $captured = $parameters;
+
+ return $response;
+ }
+ );
+
+ $action(new ProductPropertyFeature(
+ new Batch(new NullBatch(), new NullLogger()),
+ $core,
+ new NullLogger()
+ ));
+
+ return [$method, $captured];
+ }
+}