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..6bf4f264 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@
### Added
+- Added service `Services\Catalog\Ratio` with support methods,
+ see [catalog.ratio.* methods](https://apidocs.bitrix24.com/api-reference/catalog/ratio/index.html) ([#570](https://github.com/bitrix24/b24phpsdk/issues/570)):
+ - `get` gets the values of a measurement unit ratio by identifier
+ - `list` gets the list of measurement unit ratios matching a filter
+ - `getFields` returns the description of measurement unit ratio 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..0b509e4d 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-ratio
+test-integration-catalog-ratio:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_catalog_ratio
+
# work dev environment
.PHONY: php-dev-server-up
php-dev-server-up:
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d0187ae5..10b7d3f8 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -262,6 +262,9 @@
./tests/Integration/Services/Landing/RepoWidget/
+
+ ./tests/Integration/Services/Catalog/Ratio/
+
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..a394a253 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,16 @@ public function catalog(): Catalog\Catalog\Service\Catalog
return $this->serviceCache[__METHOD__];
}
-}
\ No newline at end of file
+
+ public function ratio(): Catalog\Ratio\Service\Ratio
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Catalog\Ratio\Service\Ratio(
+ $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/Ratio/Result/RatioItemResult.php b/src/Services/Catalog/Ratio/Result/RatioItemResult.php
new file mode 100644
index 00000000..4176e4fe
--- /dev/null
+++ b/src/Services/Catalog/Ratio/Result/RatioItemResult.php
@@ -0,0 +1,26 @@
+
+ *
+ * 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\Ratio\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read int $id
+ * @property-read bool $isDefault
+ * @property-read int $productId
+ * @property-read float $ratio
+ */
+class RatioItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/Catalog/Ratio/Result/RatioResult.php b/src/Services/Catalog/Ratio/Result/RatioResult.php
new file mode 100644
index 00000000..c693067e
--- /dev/null
+++ b/src/Services/Catalog/Ratio/Result/RatioResult.php
@@ -0,0 +1,28 @@
+
+ *
+ * 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\Ratio\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class RatioResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function ratio(): RatioItemResult
+ {
+ return new RatioItemResult($this->getCoreResponse()->getResponseData()->getResult()['ratio']);
+ }
+}
diff --git a/src/Services/Catalog/Ratio/Result/RatiosResult.php b/src/Services/Catalog/Ratio/Result/RatiosResult.php
new file mode 100644
index 00000000..25ffdec2
--- /dev/null
+++ b/src/Services/Catalog/Ratio/Result/RatiosResult.php
@@ -0,0 +1,42 @@
+
+ *
+ * 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\Ratio\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class RatiosResult extends AbstractResult
+{
+ /**
+ * @return RatioItemResult[]
+ * @throws BaseException
+ */
+ public function getRatios(): array
+ {
+ $items = [];
+ foreach ($this->getCoreResponse()->getResponseData()->getResult()['ratios'] as $item) {
+ $items[] = new RatioItemResult($item);
+ }
+
+ return $items;
+ }
+
+ /**
+ * @throws BaseException
+ */
+ public function getTotal(): int
+ {
+ return $this->getCoreResponse()->getResponseData()->getPagination()->getTotal() ?? 0;
+ }
+}
diff --git a/src/Services/Catalog/Ratio/Service/Ratio.php b/src/Services/Catalog/Ratio/Service/Ratio.php
new file mode 100644
index 00000000..8741bf5a
--- /dev/null
+++ b/src/Services/Catalog/Ratio/Service/Ratio.php
@@ -0,0 +1,88 @@
+
+ *
+ * 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\Ratio\Service;
+
+use Bitrix24\SDK\Attributes\ApiEndpointMetadata;
+use Bitrix24\SDK\Attributes\ApiServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Result\FieldsResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\Catalog\Ratio\Result\RatioResult;
+use Bitrix24\SDK\Services\Catalog\Ratio\Result\RatiosResult;
+
+#[ApiServiceMetadata(new Scope(['catalog']))]
+class Ratio extends AbstractService
+{
+ /**
+ * Returns the values of the measurement unit ratio fields by identifier.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/ratio/catalog-ratio-get.html
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.ratio.get',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/ratio/catalog-ratio-get.html',
+ 'Returns the values of the measurement unit ratio fields by identifier.'
+ )]
+ public function get(int $id): RatioResult
+ {
+ $this->guardPositiveId($id);
+
+ return new RatioResult($this->core->call('catalog.ratio.get', ['id' => $id]));
+ }
+
+ /**
+ * Returns a list of measurement unit ratios from the catalog matching the given filter.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/ratio/catalog-ratio-list.html
+ *
+ * @param string[] $select
+ * @param array $filter
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.ratio.list',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/ratio/catalog-ratio-list.html',
+ 'Returns a list of measurement unit ratios from the catalog matching the given filter.'
+ )]
+ public function list(array $select = [], array $filter = []): RatiosResult
+ {
+ return new RatiosResult($this->core->call('catalog.ratio.list', [
+ 'select' => $select,
+ 'filter' => $filter,
+ ]));
+ }
+
+ /**
+ * Returns the available fields of a measurement unit ratio.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/catalog/ratio/catalog-ratio-get-fields.html
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'catalog.ratio.getFields',
+ 'https://apidocs.bitrix24.com/api-reference/catalog/ratio/catalog-ratio-get-fields.html',
+ 'Returns the available fields of a measurement unit ratio.'
+ )]
+ public function fields(): FieldsResult
+ {
+ return new FieldsResult($this->core->call('catalog.ratio.getFields'));
+ }
+}
diff --git a/tests/CustomAssertions/CustomBitrix24Assertions.php b/tests/CustomAssertions/CustomBitrix24Assertions.php
index dd1d32b7..8193ba03 100644
--- a/tests/CustomAssertions/CustomBitrix24Assertions.php
+++ b/tests/CustomAssertions/CustomBitrix24Assertions.php
@@ -22,9 +22,10 @@
use Carbon\CarbonImmutable;
use MoneyPHP\Percentage\Percentage;
use Typhoon\Reflection\TyphoonReflector;
-use function Typhoon\Type\stringify;
use Money\Currency;
+use function Typhoon\Type\stringify;
+
trait CustomBitrix24Assertions
{
/**
@@ -225,6 +226,20 @@ protected function assertBitrix24AllResultItemFieldsHasValidTypeAnnotation(
);
break;
}
+ if (str_contains(mb_strtoupper($fieldCode), 'RATIO')) {
+ $this->assertTrue(
+ str_contains($propsFromAnnotations[$fieldCode], 'float'),
+ sprintf(
+ 'class «%s» field «%s» has invalid type phpdoc annotation «%s», field type from bitrix24 is «%s», expected sdk-type «%s»',
+ $resultItemClassName,
+ $fieldCode,
+ $propsFromAnnotations[$fieldCode],
+ $fieldData['type'],
+ 'float'
+ )
+ );
+ break;
+ }
$this->assertTrue(
str_contains($propsFromAnnotations[$fieldCode], 'Money\Money'),
sprintf(
diff --git a/tests/Integration/Services/Catalog/Ratio/Result/RatioItemResultAnnotationsTest.php b/tests/Integration/Services/Catalog/Ratio/Result/RatioItemResultAnnotationsTest.php
new file mode 100644
index 00000000..5b359b73
--- /dev/null
+++ b/tests/Integration/Services/Catalog/Ratio/Result/RatioItemResultAnnotationsTest.php
@@ -0,0 +1,79 @@
+
+ *
+ * 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\Ratio\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Fields\FieldsFilter;
+use Bitrix24\SDK\Services\Catalog\Ratio\Result\RatioItemResult;
+use Bitrix24\SDK\Services\Catalog\Ratio\Service\Ratio;
+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(RatioItemResult::class)]
+class RatioItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Ratio $ratioService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->ratioService = Fabric::getServiceBuilder()->getCatalogScope()->ratio();
+ }
+
+ /**
+ * catalog.ratio.getFields wraps the actual field descriptions under a «ratio» key,
+ * unlike most other bitrix24 *.getFields methods that return a flat field map.
+ *
+ * @return array>
+ * @throws BaseException
+ * @throws TransportException
+ */
+ private function getRatioFieldsDescription(): array
+ {
+ return $this->ratioService->fields()->getFieldsDescription()['ratio'];
+ }
+
+ #[Test]
+ #[TestDox('all fields in RatioItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $propListFromApi = (new FieldsFilter())->filterSystemFields(array_keys($this->getRatioFieldsDescription()));
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ $propListFromApi,
+ RatioItemResult::class
+ );
+ }
+
+ #[Test]
+ #[TestDox('all fields in RatioItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $allFields = $this->getRatioFieldsDescription();
+ $systemFieldsCodes = (new FieldsFilter())->filterSystemFields(array_keys($allFields));
+ $systemFields = array_filter($allFields, static fn ($code): bool => in_array($code, $systemFieldsCodes, true), ARRAY_FILTER_USE_KEY);
+
+ $this->assertBitrix24AllResultItemFieldsHasValidTypeAnnotation(
+ $systemFields,
+ RatioItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/Catalog/Ratio/Service/RatioTest.php b/tests/Integration/Services/Catalog/Ratio/Service/RatioTest.php
new file mode 100644
index 00000000..eae82689
--- /dev/null
+++ b/tests/Integration/Services/Catalog/Ratio/Service/RatioTest.php
@@ -0,0 +1,85 @@
+
+ *
+ * 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\Ratio\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Catalog\Ratio\Result\RatioItemResult;
+use Bitrix24\SDK\Services\Catalog\Ratio\Service\Ratio;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversMethod;
+use PHPUnit\Framework\TestCase;
+
+#[CoversMethod(Ratio::class, 'get')]
+#[CoversMethod(Ratio::class, 'list')]
+#[CoversMethod(Ratio::class, 'fields')]
+class RatioTest extends TestCase
+{
+ private Ratio $ratioService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->ratioService = Fabric::getServiceBuilder()->getCatalogScope()->ratio();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testGetFields(): void
+ {
+ $fields = $this->ratioService->fields()->getFieldsDescription();
+
+ self::assertArrayHasKey('ratio', $fields);
+ self::assertArrayHasKey('id', $fields['ratio']);
+ self::assertArrayHasKey('isDefault', $fields['ratio']);
+ self::assertArrayHasKey('productId', $fields['ratio']);
+ self::assertArrayHasKey('ratio', $fields['ratio']);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testList(): void
+ {
+ $ratiosResult = $this->ratioService->list();
+
+ self::assertIsArray($ratiosResult->getRatios());
+ self::assertGreaterThanOrEqual(0, $ratiosResult->getTotal());
+ }
+
+ /**
+ * catalog.ratio has no REST method to create a ratio — ratios are created implicitly when a
+ * product's measurement unit ratio is configured.
+ * If the portal has none, this test is skipped as there is no way to fabricate one via REST.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testGet(): void
+ {
+ $ratios = $this->ratioService->list()->getRatios();
+ if ($ratios === []) {
+ $this->markTestSkipped('portal has no catalog ratios (catalog.ratio) configured to test get() against');
+ }
+
+ $firstRatio = $ratios[0];
+ $ratioItemResult = $this->ratioService->get($firstRatio->id)->ratio();
+
+ self::assertInstanceOf(RatioItemResult::class, $ratioItemResult);
+ self::assertEquals($firstRatio->id, $ratioItemResult->id);
+ }
+}
diff --git a/tests/Unit/Services/Catalog/Ratio/Service/RatioTest.php b/tests/Unit/Services/Catalog/Ratio/Service/RatioTest.php
new file mode 100644
index 00000000..ecf4a098
--- /dev/null
+++ b/tests/Unit/Services/Catalog/Ratio/Service/RatioTest.php
@@ -0,0 +1,62 @@
+
+ *
+ * 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\Ratio\Service;
+
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Result\FieldsResult;
+use Bitrix24\SDK\Services\Catalog\Ratio\Result\RatioResult;
+use Bitrix24\SDK\Services\Catalog\Ratio\Result\RatiosResult;
+use Bitrix24\SDK\Services\Catalog\Ratio\Service\Ratio;
+use Bitrix24\SDK\Tests\Unit\Stubs\NullCore;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+
+#[CoversClass(Ratio::class)]
+class RatioTest extends TestCase
+{
+ private Ratio $service;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->service = new Ratio(new NullCore(), new NullLogger());
+ }
+
+ #[Test]
+ public function testGetReturnsRatioResult(): void
+ {
+ $this->assertInstanceOf(RatioResult::class, $this->service->get(1));
+ }
+
+ #[Test]
+ public function testListReturnsRatiosResult(): void
+ {
+ $this->assertInstanceOf(RatiosResult::class, $this->service->list());
+ }
+
+ #[Test]
+ public function testFieldsReturnsFieldsResult(): void
+ {
+ $this->assertInstanceOf(FieldsResult::class, $this->service->fields());
+ }
+
+ #[Test]
+ public function testGetThrowsOnNonPositiveId(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->service->get(0);
+ }
+}