diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f141291..170f1206 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,37 @@
### Added
+- Added service `Services\Biconnector\Dataset` with support methods,
+ see [biconnector.dataset.* methods](https://apidocs.bitrix24.com/api-reference/biconnector/dataset/index.html) ([#469](https://github.com/bitrix24/b24phpsdk/issues/469)):
+ - `add` adds a new dataset, with batch calls support
+ - `update` updates an existing dataset description, with batch calls support
+ - `get` gets information about the dataset by its identifier
+ - `list` gets the list of datasets, with batch calls support
+ - `delete` deletes a dataset, with batch calls support
+ - `fields` returns the fields description
+ - `updateFields` adds, updates visibility of, or deletes individual dataset columns (`biconnector.dataset.fields.update`)
+ - `count` counts datasets
+- Added `dataset()` accessor to `BiconnectorServiceBuilder` ([#469](https://github.com/bitrix24/b24phpsdk/issues/469))
+- Added service `Services\Biconnector\Source` with support methods,
+ see [biconnector.source.* methods](https://apidocs.bitrix24.com/api-reference/biconnector/source/index.html) ([#469](https://github.com/bitrix24/b24phpsdk/issues/469)):
+ - `add` adds a new data source, with batch calls support
+ - `update` updates an existing data source, with batch calls support
+ - `get` gets information about the data source by its identifier
+ - `list` gets the list of data sources, with batch calls support
+ - `delete` deletes a data source, with batch calls support
+ - `fields` returns the fields description
+ - `count` counts data sources
+- Added `source()` accessor to `BiconnectorServiceBuilder` ([#469](https://github.com/bitrix24/b24phpsdk/issues/469))
+- Added service `Services\Biconnector\Connector` with support methods,
+ see [biconnector.connector.* methods](https://github.com/bitrix24/b24phpsdk/issues/469):
+ - `add` adds a new connector, with batch calls support
+ - `update` updates an existing connector, with batch calls support
+ - `get` gets information about the connector by its identifier
+ - `list` gets the list of connectors, with batch calls support
+ - `delete` deletes a connector, with batch calls support
+ - `fields` returns the fields description
+ - `count` counts connectors
+
- Added support for events:
- `onCrmDocumentGeneratorDocumentAdd` — fires when a document is created,
see [event documentation](https://apidocs.bitrix24.com/api-reference/crm/document-generator/documents/events/on-crm-document-generator-document-add.html)
diff --git a/Makefile b/Makefile
index d3555c4f..616e36ef 100644
--- a/Makefile
+++ b/Makefile
@@ -445,6 +445,22 @@ integration_tests_crm_documentgenerator_document:
integration_tests_crm_documentgenerator_template:
docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_crm_documentgenerator_template
+.PHONY: test-integration-scope-biconnector
+test-integration-scope-biconnector:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_biconnector
+
+.PHONY: test-integration-biconnector-connector
+test-integration-biconnector-connector:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_biconnector_connector
+
+.PHONY: test-integration-biconnector-source
+test-integration-biconnector-source:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_biconnector_source
+
+.PHONY: test-integration-biconnector-dataset
+test-integration-biconnector-dataset:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_biconnector_dataset
+
# work dev environment
.PHONY: php-dev-server-up
php-dev-server-up:
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e9c12ee3..37d354cb 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -232,6 +232,21 @@
./tests/Integration/Services/SonetGroup/
+
+ ./tests/Integration/Services/Biconnector/
+
+
+ ./tests/Integration/Services/Biconnector/Connector/Service/
+ ./tests/Integration/Services/Biconnector/Connector/Result/
+
+
+ ./tests/Integration/Services/Biconnector/Source/Service/
+ ./tests/Integration/Services/Biconnector/Source/Result/
+
+
+ ./tests/Integration/Services/Biconnector/Dataset/Service/
+ ./tests/Integration/Services/Biconnector/Dataset/Result/
+
diff --git a/src/Services/Biconnector/BiconnectorServiceBuilder.php b/src/Services/Biconnector/BiconnectorServiceBuilder.php
new file mode 100644
index 00000000..925f9da9
--- /dev/null
+++ b/src/Services/Biconnector/BiconnectorServiceBuilder.php
@@ -0,0 +1,103 @@
+
+ *
+ * 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\Biconnector;
+
+use Bitrix24\SDK\Attributes\ApiServiceBuilderMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Services\AbstractServiceBuilder;
+use Bitrix24\SDK\Services\Biconnector\Connector\Batch as ConnectorBatch;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Batch;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Batch as DatasetBatch;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Service\Batch as DatasetServiceBatch;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Service\Dataset;
+use Bitrix24\SDK\Services\Biconnector\Source\Batch as SourceBatch;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Batch as SourceServiceBatch;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Source;
+
+#[ApiServiceBuilderMetadata(new Scope(['biconnector']))]
+class BiconnectorServiceBuilder extends AbstractServiceBuilder
+{
+ /**
+ * Get the Connector service
+ *
+ * Uses a specialized ConnectorBatch to handle biconnector.connector.* REST API differences:
+ * - list uses 'page' parameter (page number) instead of standard 'start' (offset)
+ * - delete uses lowercase 'id' instead of 'ID'
+ */
+ public function connector(): Connector
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ // Use specialized Batch for Connector to ensure correct REST parameter mapping
+ $connectorBatch = new ConnectorBatch(
+ $this->core,
+ $this->log
+ );
+ $this->serviceCache[__METHOD__] = new Connector(
+ new Batch($connectorBatch, $this->log),
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Get the Dataset service
+ *
+ * Uses a specialized DatasetBatch to handle biconnector.dataset.* REST API differences:
+ * - list uses 'page' parameter (page number) instead of standard 'start' (offset)
+ * - delete uses lowercase 'id' instead of 'ID'
+ */
+ public function dataset(): Dataset
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $datasetBatch = new DatasetBatch(
+ $this->core,
+ $this->log
+ );
+ $this->serviceCache[__METHOD__] = new Dataset(
+ new DatasetServiceBatch($datasetBatch, $this->log),
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
+ /**
+ * Get the Source service
+ *
+ * Uses a specialized SourceBatch to handle biconnector.source.* REST API differences:
+ * - delete uses lowercase 'id' instead of 'ID'
+ */
+ public function source(): Source
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $sourceBatch = new SourceBatch(
+ $this->core,
+ $this->log
+ );
+ $this->serviceCache[__METHOD__] = new Source(
+ new SourceServiceBatch($sourceBatch, $this->log),
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Batch.php b/src/Services/Biconnector/Connector/Batch.php
new file mode 100644
index 00000000..b1323e80
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Batch.php
@@ -0,0 +1,266 @@
+
+ *
+ * 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\Biconnector\Connector;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+use Generator;
+
+/**
+ * Class Batch
+ *
+ * Overrides base Batch to handle parameter naming differences in biconnector.connector.* REST methods:
+ * - list uses 'page' (page number, 50 records per page) instead of 'start' (offset) for pagination
+ * - delete uses lowercase 'id' instead of 'ID'
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-list.html
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-delete.html
+ */
+class Batch extends \Bitrix24\SDK\Core\Batch
+{
+ /**
+ * Determines the ID key — lowercase 'id' for biconnector connector
+ */
+ #[\Override]
+ protected function determineKeyId(string $apiMethod, ?array $additionalParameters): string
+ {
+ return 'id';
+ }
+
+ /**
+ * Delete entity items with batch call using lowercase 'id' parameter
+ *
+ * @param int[] $entityItemId
+ * @param array|null $additionalParameters
+ *
+ * @return Generator|ResponseData[]
+ * @throws BaseException
+ */
+ #[\Override]
+ public function deleteEntityItems(
+ string $apiMethod,
+ array $entityItemId,
+ ?array $additionalParameters = null
+ ): Generator {
+ $this->logger->debug(
+ 'deleteEntityItems.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'entityItems' => $entityItemId,
+ 'additionalParameters' => $additionalParameters,
+ ]
+ );
+
+ try {
+ $this->clearCommands();
+ foreach ($entityItemId as $cnt => $itemId) {
+ if (!is_int($itemId)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'invalid type «%s» of connector id «%s» at position %s, connector id must be integer type',
+ gettype($itemId),
+ $itemId,
+ $cnt
+ )
+ );
+ }
+
+ $this->registerCommand($apiMethod, ['id' => $itemId]);
+ }
+
+ foreach ($this->getTraversable(true) as $cnt => $deletedItemResult) {
+ yield $cnt => $deletedItemResult;
+ }
+ } catch (InvalidArgumentException $exception) {
+ $errorMessage = sprintf('batch delete connector items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+ throw $exception;
+ } catch (\Throwable $exception) {
+ $errorMessage = sprintf('batch delete connector items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+
+ throw new BaseException($errorMessage, $exception->getCode(), $exception);
+ }
+
+ $this->logger->debug('deleteEntityItems.finish');
+ }
+
+ /**
+ * Get traversable list using page-based pagination.
+ *
+ * The biconnector.connector.list method uses 'page' parameter (page number, 50 records per page)
+ * instead of the standard 'start' (offset) parameter used by most other REST methods.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-list.html
+ *
+ * @param array $order
+ * @param array $filter
+ * @param array $select
+ *
+ * @return Generator
+ * @throws BaseException
+ * @throws \Bitrix24\SDK\Core\Exceptions\TransportException
+ */
+ #[\Override]
+ public function getTraversableList(
+ string $apiMethod,
+ ?array $order = [],
+ ?array $filter = [],
+ ?array $select = [],
+ ?int $limit = null,
+ ?array $additionalParameters = null
+ ): Generator {
+ yield from $this->getTraversableListWithCount(
+ $apiMethod,
+ $order ?? [],
+ $filter ?? [],
+ $select ?? [],
+ $limit,
+ $additionalParameters
+ );
+ }
+
+ /**
+ * Get traversable list using page-based pagination (page number, 50 records per page).
+ *
+ * The biconnector.connector.list method accepts 'page' parameter instead of 'start'.
+ * Page 1 returns items 1–50, page 2 returns items 51–100, etc.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-list.html
+ *
+ * @param array $order
+ * @param array $filter
+ * @param array $select
+ *
+ * @return Generator
+ * @throws BaseException
+ * @throws \Bitrix24\SDK\Core\Exceptions\TransportException
+ */
+ #[\Override]
+ public function getTraversableListWithCount(
+ string $apiMethod,
+ array $order,
+ array $filter,
+ array $select,
+ ?int $limit = null,
+ ?array $additionalParameters = null
+ ): Generator {
+ $this->logger->debug(
+ 'getTraversableListWithCount.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'limit' => $limit,
+ 'additionalParameters' => $additionalParameters,
+ ]
+ );
+
+ $this->clearCommands();
+
+ // Fetch first page to determine total count
+ $params = [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => 1,
+ ];
+
+ if ($additionalParameters !== null) {
+ $params = array_merge($params, $additionalParameters);
+ }
+
+ $response = $this->core->call($apiMethod, $params);
+ $total = $response->getResponseData()->getPagination()->getTotal();
+
+ $this->logger->debug(
+ 'getTraversableListWithCount.totalElementsCount',
+ [
+ 'totalElementsCount' => $total,
+ ]
+ );
+
+ if ($total <= self::MAX_ELEMENTS_IN_PAGE) {
+ $elementsCounter = 0;
+ foreach ($response->getResponseData()->getResult() as $item) {
+ $elementsCounter++;
+ if ($limit !== null && $elementsCounter > $limit) {
+ return;
+ }
+
+ yield $item;
+ }
+
+ return;
+ }
+
+ // Register batch commands for all pages
+ $totalPages = (int)ceil($total / self::MAX_ELEMENTS_IN_PAGE);
+ for ($page = 1; $page <= $totalPages; $page++) {
+ $pageParams = [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => $page,
+ ];
+
+ if ($additionalParameters !== null) {
+ $pageParams = array_merge($pageParams, $additionalParameters);
+ }
+
+ $this->registerCommand($apiMethod, $pageParams);
+
+ if ($limit !== null && $limit < $page * self::MAX_ELEMENTS_IN_PAGE) {
+ break;
+ }
+ }
+
+ $this->logger->debug(
+ 'getTraversableListWithCount.commandsRegistered',
+ [
+ 'commandsCount' => $this->commands->count(),
+ 'totalItemsToSelect' => $total,
+ ]
+ );
+
+ $elementsCounter = 0;
+ foreach ($this->getTraversable(true) as $queryResultData) {
+ $resultElements = $this->extractElementsFromBatchResult($queryResultData, false);
+ foreach ($resultElements as $resultElement) {
+ ++$elementsCounter;
+ if ($limit !== null && $elementsCounter > $limit) {
+ return;
+ }
+
+ yield $resultElement;
+ }
+ }
+
+ $this->logger->debug('getTraversableListWithCount.finish');
+ }
+}
+
+
diff --git a/src/Services/Biconnector/Connector/Result/AddedConnectorBatchResult.php b/src/Services/Biconnector/Connector/Result/AddedConnectorBatchResult.php
new file mode 100644
index 00000000..6a3c7f13
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/AddedConnectorBatchResult.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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Result\AddedItemBatchResult;
+
+/**
+ * Class AddedConnectorBatchResult
+ */
+class AddedConnectorBatchResult extends AddedItemBatchResult
+{
+ #[\Override]
+ public function getId(): int
+ {
+ $result = $this->getResponseData()->getResult();
+
+ if (!empty($result['connector']['id'])) {
+ return (int)$result['connector']['id'];
+ }
+
+ if (!empty($result['id'])) {
+ return (int)$result['id'];
+ }
+
+ return (int)$result;
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/AddedConnectorResult.php b/src/Services/Biconnector/Connector/Result/AddedConnectorResult.php
new file mode 100644
index 00000000..6f16fc0e
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/AddedConnectorResult.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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AddedItemResult;
+
+/**
+ * Class AddedConnectorResult
+ */
+class AddedConnectorResult extends AddedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function getId(): int
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (!empty($result['connector']['id'])) {
+ return (int)$result['connector']['id'];
+ }
+
+ if (!empty($result['id'])) {
+ return (int)$result['id'];
+ }
+
+ return (int)$result;
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/ConnectorItemResult.php b/src/Services/Biconnector/Connector/Result/ConnectorItemResult.php
new file mode 100644
index 00000000..479b16b6
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/ConnectorItemResult.php
@@ -0,0 +1,53 @@
+
+ *
+ * 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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * Class ConnectorItemResult
+ *
+ * Field names correspond to the actual API response returned by biconnector.connector.get / biconnector.connector.list.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-fields.html
+ *
+ * @property-read int $id
+ * @property-read string $title
+ * @property-read string $logo
+ * @property-read string|null $description
+ * @property-read int|null $sort
+ * @property-read string|null $urlCheck
+ * @property-read string|null $urlData
+ * @property-read string|null $urlTableList
+ * @property-read string|null $urlTableDescription
+ * @property-read array|null $settings
+ * @property-read bool|null $supportMapping
+ * @property-read CarbonImmutable $dateCreate
+ */
+class ConnectorItemResult extends AbstractItem
+{
+ #[\Override]
+ public function __get($offset): mixed
+ {
+ return match ($offset) {
+ 'id', 'sort' => isset($this->data[$offset]) ? (int)$this->data[$offset] : null,
+ 'supportMapping' => isset($this->data[$offset]) ? (bool)$this->data[$offset] : null,
+ 'dateCreate' => isset($this->data[$offset])
+ ? CarbonImmutable::parse($this->data[$offset])
+ : null,
+ default => $this->data[$offset] ?? null,
+ };
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/ConnectorResult.php b/src/Services/Biconnector/Connector/Result/ConnectorResult.php
new file mode 100644
index 00000000..ae162bce
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/ConnectorResult.php
@@ -0,0 +1,39 @@
+
+ *
+ * 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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Class ConnectorResult
+ */
+class ConnectorResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function connector(): ConnectorItemResult
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ // biconnector.connector.get returns the item under the 'item' key
+ if (!empty($result['item']) && is_array($result['item'])) {
+ return new ConnectorItemResult($result['item']);
+ }
+
+ // Fallback: flat object at result level {"id": ..., "title": ..., ...}
+ return new ConnectorItemResult($result);
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/ConnectorsResult.php b/src/Services/Biconnector/Connector/Result/ConnectorsResult.php
new file mode 100644
index 00000000..96530ccf
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/ConnectorsResult.php
@@ -0,0 +1,49 @@
+
+ *
+ * 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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Class ConnectorsResult
+ */
+class ConnectorsResult extends AbstractResult
+{
+ /**
+ * @return ConnectorItemResult[]
+ * @throws BaseException
+ */
+ public function getConnectors(): array
+ {
+ $items = [];
+ $source = [];
+
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (!empty($result['connectors']) && is_array($result['connectors'])) {
+ $source = $result['connectors'];
+ } elseif (!empty($result['items']) && is_array($result['items'])) {
+ $source = $result['items'];
+ } elseif (is_array($result) && array_is_list($result)) {
+ $source = $result;
+ }
+
+ foreach ($source as $item) {
+ $items[] = new ConnectorItemResult($item);
+ }
+
+ return $items;
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/DeletedConnectorBatchResult.php b/src/Services/Biconnector/Connector/Result/DeletedConnectorBatchResult.php
new file mode 100644
index 00000000..a3790231
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/DeletedConnectorBatchResult.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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Result\DeletedItemBatchResult;
+
+/**
+ * Class DeletedConnectorBatchResult
+ */
+class DeletedConnectorBatchResult extends DeletedItemBatchResult
+{
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/DeletedConnectorResult.php b/src/Services/Biconnector/Connector/Result/DeletedConnectorResult.php
new file mode 100644
index 00000000..b06bba3a
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/DeletedConnectorResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * 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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\DeletedItemResult;
+
+/**
+ * Class DeletedConnectorResult
+ */
+class DeletedConnectorResult extends DeletedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getCoreResponse()->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/UpdatedConnectorBatchResult.php b/src/Services/Biconnector/Connector/Result/UpdatedConnectorBatchResult.php
new file mode 100644
index 00000000..b0cbe9c4
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/UpdatedConnectorBatchResult.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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Result\UpdatedItemBatchResult;
+
+/**
+ * Class UpdatedConnectorBatchResult
+ */
+class UpdatedConnectorBatchResult extends UpdatedItemBatchResult
+{
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Result/UpdatedConnectorResult.php b/src/Services/Biconnector/Connector/Result/UpdatedConnectorResult.php
new file mode 100644
index 00000000..5687dfc4
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Result/UpdatedConnectorResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * 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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+
+/**
+ * Class UpdatedConnectorResult
+ */
+class UpdatedConnectorResult extends UpdatedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getCoreResponse()->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Service/Batch.php b/src/Services/Biconnector/Connector/Service/Batch.php
new file mode 100644
index 00000000..bdfbcf25
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Service/Batch.php
@@ -0,0 +1,169 @@
+
+ *
+ * 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\Biconnector\Connector\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\Biconnector\Connector\Result\AddedConnectorBatchResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\ConnectorItemResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\DeletedConnectorBatchResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\UpdatedConnectorBatchResult;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+#[ApiBatchServiceMetadata(new Scope(['biconnector']))]
+class Batch
+{
+ /**
+ * Batch constructor
+ */
+ public function __construct(protected BatchOperationsInterface $batch, protected LoggerInterface $log)
+ {
+ }
+
+ /**
+ * Batch list connectors
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-list.html
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.connector.list',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-list.html',
+ 'Batch list connectors'
+ )]
+ 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,
+ ]
+ );
+
+ $connectorListGenerator = $this->batch->getTraversableListWithCount(
+ 'biconnector.connector.list',
+ $order,
+ $filter,
+ $select,
+ $limit
+ );
+ foreach ($connectorListGenerator as $key => $value) {
+ yield $key => new ConnectorItemResult($value);
+ }
+ }
+
+ /**
+ * Batch add connectors
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-add.html
+ *
+ * @param array $connectors
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.connector.add',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-add.html',
+ 'Batch add connectors'
+ )]
+ public function add(array $connectors): Generator
+ {
+ $items = [];
+ foreach ($connectors as $item) {
+ $items[] = [
+ 'fields' => $item,
+ ];
+ }
+
+ foreach ($this->batch->addEntityItems('biconnector.connector.add', $items) as $key => $item) {
+ yield $key => new AddedConnectorBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch update connectors
+ *
+ * Update elements in array with structure:
+ * id => [ // Connector id
+ * 'fields' => [] // Connector fields to update
+ * ]
+ *
+ * @param array $entityItems
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.connector.update',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-update.html',
+ 'Batch update connectors'
+ )]
+ public function update(array $entityItems): Generator
+ {
+ foreach (
+ $this->batch->updateEntityItems(
+ 'biconnector.connector.update',
+ $entityItems
+ ) as $key => $item
+ ) {
+ yield $key => new UpdatedConnectorBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch delete connectors
+ *
+ * @param int[] $connectorIds
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.connector.delete',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-delete.html',
+ 'Batch delete connectors'
+ )]
+ public function delete(array $connectorIds): Generator
+ {
+ foreach (
+ $this->batch->deleteEntityItems(
+ 'biconnector.connector.delete',
+ $connectorIds
+ ) as $key => $item
+ ) {
+ yield $key => new DeletedConnectorBatchResult($item);
+ }
+ }
+}
diff --git a/src/Services/Biconnector/Connector/Service/Connector.php b/src/Services/Biconnector/Connector/Service/Connector.php
new file mode 100644
index 00000000..1ef76aeb
--- /dev/null
+++ b/src/Services/Biconnector/Connector/Service/Connector.php
@@ -0,0 +1,237 @@
+
+ *
+ * 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\Biconnector\Connector\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\TransportException;
+use Bitrix24\SDK\Core\Result\FieldsResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\AddedConnectorResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\ConnectorResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\ConnectorsResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\DeletedConnectorResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\UpdatedConnectorResult;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['biconnector']))]
+class Connector extends AbstractService
+{
+ /**
+ * Connector constructor
+ */
+ public function __construct(public Batch $batch, CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Add a new connector
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-add.html
+ *
+ * @param array{
+ * title: string,
+ * logo: string,
+ * urlCheck: string,
+ * urlData: string,
+ * urlTableList: string,
+ * urlTableDescription: string,
+ * settings: array,
+ * description?: string,
+ * sort?: int,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.connector.add',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-add.html',
+ 'Add a new connector'
+ )]
+ public function add(array $fields): AddedConnectorResult
+ {
+ return new AddedConnectorResult(
+ $this->core->call(
+ 'biconnector.connector.add',
+ [
+ 'fields' => $fields,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Update an existing connector
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-update.html
+ *
+ * @param array{
+ * title?: string,
+ * logo?: string,
+ * description?: string,
+ * sort?: int,
+ * urlCheck?: string,
+ * urlData?: string,
+ * urlTableList?: string,
+ * urlTableDescription?: string,
+ * settings?: array,
+ * supportMapping?: bool,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.connector.update',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-update.html',
+ 'Update an existing connector'
+ )]
+ public function update(int $id, array $fields): UpdatedConnectorResult
+ {
+ return new UpdatedConnectorResult(
+ $this->core->call(
+ 'biconnector.connector.update',
+ [
+ 'id' => $id,
+ 'fields' => $fields,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get a connector by its ID
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.connector.get',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-get.html',
+ 'Get a connector by its ID'
+ )]
+ public function get(int $id): ConnectorResult
+ {
+ return new ConnectorResult(
+ $this->core->call(
+ 'biconnector.connector.get',
+ [
+ 'id' => $id,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get a list of connectors
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-list.html
+ *
+ * @param array $order - sort fields, e.g. ['id' => 'ASC']
+ * @param array $filter - filter fields
+ * @param array $select - fields to include in the result
+ * @param int $page - page number for pagination (page size is 50 records per page)
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.connector.list',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-list.html',
+ 'Get a list of connectors'
+ )]
+ public function list(array $order = [], array $filter = [], array $select = [], int $page = 1): ConnectorsResult
+ {
+ return new ConnectorsResult(
+ $this->core->call(
+ 'biconnector.connector.list',
+ [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => $page,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Delete a connector by its ID
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.connector.delete',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-delete.html',
+ 'Delete a connector by its ID'
+ )]
+ public function delete(int $id): DeletedConnectorResult
+ {
+ return new DeletedConnectorResult(
+ $this->core->call(
+ 'biconnector.connector.delete',
+ [
+ 'id' => $id,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get the fields description for connectors
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-fields.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.connector.fields',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/connector/biconnector-connector-fields.html',
+ 'Get the fields description for connectors'
+ )]
+ public function fields(): FieldsResult
+ {
+ return new FieldsResult($this->core->call('biconnector.connector.fields'));
+ }
+
+ /**
+ * Count connectors
+ *
+ * Note: biconnector.connector.list does not return a total count in pagination,
+ * so we iterate all available items via batch to count them.
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function count(): int
+ {
+ $count = 0;
+ foreach ($this->batch->list() as $item) {
+ $count++;
+ }
+
+ return $count;
+ }
+}
diff --git a/src/Services/Biconnector/Dataset/Batch.php b/src/Services/Biconnector/Dataset/Batch.php
new file mode 100644
index 00000000..fea884f6
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Batch.php
@@ -0,0 +1,266 @@
+
+ *
+ * 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\Biconnector\Dataset;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+use Generator;
+
+/**
+ * Class Batch
+ *
+ * Overrides base Batch to handle parameter naming differences in biconnector.dataset.* REST methods:
+ * - list uses 'page' (page number, 50 records per page) instead of 'start' (offset) for pagination
+ * - delete uses lowercase 'id' instead of 'ID'
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-list.html
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-delete.html
+ */
+class Batch extends \Bitrix24\SDK\Core\Batch
+{
+ /**
+ * Determines the ID key — lowercase 'id' for biconnector dataset
+ */
+ #[\Override]
+ protected function determineKeyId(string $apiMethod, ?array $additionalParameters): string
+ {
+ return 'id';
+ }
+
+ /**
+ * Delete entity items with batch call using lowercase 'id' parameter
+ *
+ * @param int[] $entityItemId
+ * @param array|null $additionalParameters
+ *
+ * @return Generator|ResponseData[]
+ * @throws BaseException
+ */
+ #[\Override]
+ public function deleteEntityItems(
+ string $apiMethod,
+ array $entityItemId,
+ ?array $additionalParameters = null
+ ): Generator {
+ $this->logger->debug(
+ 'deleteEntityItems.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'entityItems' => $entityItemId,
+ 'additionalParameters' => $additionalParameters,
+ ]
+ );
+
+ try {
+ $this->clearCommands();
+ foreach ($entityItemId as $cnt => $itemId) {
+ if (!is_int($itemId)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'invalid type «%s» of dataset id «%s» at position %s, dataset id must be integer type',
+ gettype($itemId),
+ $itemId,
+ $cnt
+ )
+ );
+ }
+
+ $this->registerCommand($apiMethod, ['id' => $itemId]);
+ }
+
+ foreach ($this->getTraversable(true) as $cnt => $deletedItemResult) {
+ yield $cnt => $deletedItemResult;
+ }
+ } catch (InvalidArgumentException $exception) {
+ $errorMessage = sprintf('batch delete dataset items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+ throw $exception;
+ } catch (\Throwable $exception) {
+ $errorMessage = sprintf('batch delete dataset items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+
+ throw new BaseException($errorMessage, $exception->getCode(), $exception);
+ }
+
+ $this->logger->debug('deleteEntityItems.finish');
+ }
+
+ /**
+ * Get traversable list using page-based pagination.
+ *
+ * The biconnector.dataset.list method uses 'page' parameter (page number, 50 records per page)
+ * instead of the standard 'start' (offset) parameter used by most other REST methods.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-list.html
+ *
+ * @param array $order
+ * @param array $filter
+ * @param array $select
+ *
+ * @return Generator
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ public function getTraversableList(
+ string $apiMethod,
+ ?array $order = [],
+ ?array $filter = [],
+ ?array $select = [],
+ ?int $limit = null,
+ ?array $additionalParameters = null
+ ): Generator {
+ yield from $this->getTraversableListWithCount(
+ $apiMethod,
+ $order ?? [],
+ $filter ?? [],
+ $select ?? [],
+ $limit,
+ $additionalParameters
+ );
+ }
+
+ /**
+ * Get traversable list using page-based pagination (page number, 50 records per page).
+ *
+ * The biconnector.dataset.list method accepts 'page' parameter instead of 'start'.
+ * Page 1 returns items 1–50, page 2 returns items 51–100, etc.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-list.html
+ *
+ * @param array $order
+ * @param array $filter
+ * @param array $select
+ *
+ * @return Generator
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ public function getTraversableListWithCount(
+ string $apiMethod,
+ array $order,
+ array $filter,
+ array $select,
+ ?int $limit = null,
+ ?array $additionalParameters = null
+ ): Generator {
+ $this->logger->debug(
+ 'getTraversableListWithCount.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'limit' => $limit,
+ 'additionalParameters' => $additionalParameters,
+ ]
+ );
+
+ $this->clearCommands();
+
+ // Fetch first page to determine total count
+ $params = [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => 1,
+ ];
+
+ if ($additionalParameters !== null) {
+ $params = array_merge($params, $additionalParameters);
+ }
+
+ $response = $this->core->call($apiMethod, $params);
+ $total = $response->getResponseData()->getPagination()->getTotal();
+
+ $this->logger->debug(
+ 'getTraversableListWithCount.totalElementsCount',
+ [
+ 'totalElementsCount' => $total,
+ ]
+ );
+
+ if ($total <= self::MAX_ELEMENTS_IN_PAGE) {
+ $elementsCounter = 0;
+ foreach ($response->getResponseData()->getResult() as $item) {
+ $elementsCounter++;
+ if ($limit !== null && $elementsCounter > $limit) {
+ return;
+ }
+
+ yield $item;
+ }
+
+ return;
+ }
+
+ // Register batch commands for all pages
+ $totalPages = (int)ceil($total / self::MAX_ELEMENTS_IN_PAGE);
+ for ($page = 1; $page <= $totalPages; $page++) {
+ $pageParams = [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => $page,
+ ];
+
+ if ($additionalParameters !== null) {
+ $pageParams = array_merge($pageParams, $additionalParameters);
+ }
+
+ $this->registerCommand($apiMethod, $pageParams);
+
+ if ($limit !== null && $limit < $page * self::MAX_ELEMENTS_IN_PAGE) {
+ break;
+ }
+ }
+
+ $this->logger->debug(
+ 'getTraversableListWithCount.commandsRegistered',
+ [
+ 'commandsCount' => $this->commands->count(),
+ 'totalItemsToSelect' => $total,
+ ]
+ );
+
+ $elementsCounter = 0;
+ foreach ($this->getTraversable(true) as $queryResultData) {
+ $resultElements = $this->extractElementsFromBatchResult($queryResultData, false);
+ foreach ($resultElements as $resultElement) {
+ ++$elementsCounter;
+ if ($limit !== null && $elementsCounter > $limit) {
+ return;
+ }
+
+ yield $resultElement;
+ }
+ }
+
+ $this->logger->debug('getTraversableListWithCount.finish');
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/AddedDatasetBatchResult.php b/src/Services/Biconnector/Dataset/Result/AddedDatasetBatchResult.php
new file mode 100644
index 00000000..0d87d7f0
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/AddedDatasetBatchResult.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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Result\AddedItemBatchResult;
+
+/**
+ * Class AddedDatasetBatchResult
+ */
+class AddedDatasetBatchResult extends AddedItemBatchResult
+{
+ #[\Override]
+ public function getId(): int
+ {
+ $result = $this->getResponseData()->getResult();
+
+ if (!empty($result['id'])) {
+ return (int)$result['id'];
+ }
+
+ return (int)$result;
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/AddedDatasetResult.php b/src/Services/Biconnector/Dataset/Result/AddedDatasetResult.php
new file mode 100644
index 00000000..1ae6707e
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/AddedDatasetResult.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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AddedItemResult;
+
+/**
+ * Class AddedDatasetResult
+ *
+ * Wraps the response from biconnector.dataset.add.
+ * The API returns: result.id (integer)
+ */
+class AddedDatasetResult extends AddedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function getId(): int
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (!empty($result['id'])) {
+ return (int)$result['id'];
+ }
+
+ return (int)$result;
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/DatasetItemResult.php b/src/Services/Biconnector/Dataset/Result/DatasetItemResult.php
new file mode 100644
index 00000000..c807c4ba
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/DatasetItemResult.php
@@ -0,0 +1,55 @@
+
+ *
+ * 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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * Class DatasetItemResult
+ *
+ * Field names correspond to the actual API response returned by biconnector.dataset.get / biconnector.dataset.list.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-fields.html
+ *
+ * @property-read int $id
+ * @property-read int|null $sourceId
+ * @property-read string|null $name
+ * @property-read string|null $type
+ * @property-read string|null $description
+ * @property-read string|null $externalName
+ * @property-read string|null $externalCode
+ * @property-read int|null $externalId
+ * @property-read CarbonImmutable $dateCreate
+ * @property-read CarbonImmutable $dateUpdate
+ * @property-read int $createdById
+ * @property-read int $updatedById
+ * @property-read array|null $fields
+ */
+class DatasetItemResult extends AbstractItem
+{
+ #[\Override]
+ public function __get($offset): mixed
+ {
+ return match ($offset) {
+ 'id', 'createdById', 'updatedById' => isset($this->data[$offset]) ? (int)$this->data[$offset] : null,
+ 'sourceId', 'externalId' => isset($this->data[$offset]) ? (int)$this->data[$offset] : null,
+ 'dateCreate', 'dateUpdate' => isset($this->data[$offset])
+ ? CarbonImmutable::parse($this->data[$offset])
+ : null,
+ default => $this->data[$offset] ?? null,
+ };
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/DatasetResult.php b/src/Services/Biconnector/Dataset/Result/DatasetResult.php
new file mode 100644
index 00000000..09352c67
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/DatasetResult.php
@@ -0,0 +1,41 @@
+
+ *
+ * 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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Class DatasetResult
+ *
+ * Wraps the response from biconnector.dataset.get.
+ * The API returns: result.item (object)
+ */
+class DatasetResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function dataset(): DatasetItemResult
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (!empty($result['item']) && is_array($result['item'])) {
+ return new DatasetItemResult($result['item']);
+ }
+
+ return new DatasetItemResult($result);
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/DatasetsResult.php b/src/Services/Biconnector/Dataset/Result/DatasetsResult.php
new file mode 100644
index 00000000..bf17fcf1
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/DatasetsResult.php
@@ -0,0 +1,45 @@
+
+ *
+ * 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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Class DatasetsResult
+ *
+ * Wraps the response from biconnector.dataset.list.
+ * The API returns a flat array of dataset items.
+ */
+class DatasetsResult extends AbstractResult
+{
+ /**
+ * @return DatasetItemResult[]
+ * @throws BaseException
+ */
+ public function getDatasets(): array
+ {
+ $items = [];
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (array_is_list($result)) {
+ foreach ($result as $item) {
+ $items[] = new DatasetItemResult($item);
+ }
+ }
+
+ return $items;
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/DeletedDatasetBatchResult.php b/src/Services/Biconnector/Dataset/Result/DeletedDatasetBatchResult.php
new file mode 100644
index 00000000..74d79e70
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/DeletedDatasetBatchResult.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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Result\DeletedItemBatchResult;
+
+/**
+ * Class DeletedDatasetBatchResult
+ */
+class DeletedDatasetBatchResult extends DeletedItemBatchResult
+{
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getResponseData()->getResult();
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/DeletedDatasetResult.php b/src/Services/Biconnector/Dataset/Result/DeletedDatasetResult.php
new file mode 100644
index 00000000..08afd5f7
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/DeletedDatasetResult.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\DeletedItemResult;
+
+/**
+ * Class DeletedDatasetResult
+ */
+class DeletedDatasetResult extends DeletedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getCoreResponse()->getResponseData()->getResult();
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/UpdatedDatasetBatchResult.php b/src/Services/Biconnector/Dataset/Result/UpdatedDatasetBatchResult.php
new file mode 100644
index 00000000..f9818607
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/UpdatedDatasetBatchResult.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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Result\UpdatedItemBatchResult;
+
+/**
+ * Class UpdatedDatasetBatchResult
+ */
+class UpdatedDatasetBatchResult extends UpdatedItemBatchResult
+{
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getResponseData()->getResult();
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Result/UpdatedDatasetResult.php b/src/Services/Biconnector/Dataset/Result/UpdatedDatasetResult.php
new file mode 100644
index 00000000..9a209f8c
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Result/UpdatedDatasetResult.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+
+/**
+ * Class UpdatedDatasetResult
+ */
+class UpdatedDatasetResult extends UpdatedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getCoreResponse()->getResponseData()->getResult();
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Service/Batch.php b/src/Services/Biconnector/Dataset/Service/Batch.php
new file mode 100644
index 00000000..ad51a3ec
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Service/Batch.php
@@ -0,0 +1,171 @@
+
+ *
+ * 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\Biconnector\Dataset\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\Biconnector\Dataset\Result\AddedDatasetBatchResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DatasetItemResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DeletedDatasetBatchResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\UpdatedDatasetBatchResult;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+#[ApiBatchServiceMetadata(new Scope(['biconnector']))]
+class Batch
+{
+ /**
+ * Batch constructor
+ */
+ public function __construct(protected BatchOperationsInterface $batch, protected LoggerInterface $log)
+ {
+ }
+
+ /**
+ * Batch list datasets
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-list.html
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.dataset.list',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-list.html',
+ 'Batch list datasets'
+ )]
+ 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->getTraversableList(
+ 'biconnector.dataset.list',
+ $order,
+ $filter,
+ $select,
+ $limit
+ ) as $key => $value
+ ) {
+ yield $key => new DatasetItemResult($value);
+ }
+ }
+
+ /**
+ * Batch add datasets
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-add.html
+ *
+ * @param array $datasets
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.dataset.add',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-add.html',
+ 'Batch add datasets'
+ )]
+ public function add(array $datasets): Generator
+ {
+ $items = [];
+ foreach ($datasets as $item) {
+ $items[] = [
+ 'fields' => $item,
+ ];
+ }
+
+ foreach ($this->batch->addEntityItems('biconnector.dataset.add', $items) as $key => $item) {
+ yield $key => new AddedDatasetBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch update datasets
+ *
+ * Update elements in array with structure:
+ * id => [ // Dataset id
+ * 'fields' => [] // Dataset fields to update
+ * ]
+ *
+ * @param array $entityItems
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.dataset.update',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-update.html',
+ 'Batch update datasets'
+ )]
+ public function update(array $entityItems): Generator
+ {
+ foreach (
+ $this->batch->updateEntityItems(
+ 'biconnector.dataset.update',
+ $entityItems
+ ) as $key => $item
+ ) {
+ yield $key => new UpdatedDatasetBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch delete datasets
+ *
+ * @param int[] $datasetIds
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.dataset.delete',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-delete.html',
+ 'Batch delete datasets'
+ )]
+ public function delete(array $datasetIds): Generator
+ {
+ foreach (
+ $this->batch->deleteEntityItems(
+ 'biconnector.dataset.delete',
+ $datasetIds
+ ) as $key => $item
+ ) {
+ yield $key => new DeletedDatasetBatchResult($item);
+ }
+ }
+}
+
diff --git a/src/Services/Biconnector/Dataset/Service/Dataset.php b/src/Services/Biconnector/Dataset/Service/Dataset.php
new file mode 100644
index 00000000..3767c851
--- /dev/null
+++ b/src/Services/Biconnector/Dataset/Service/Dataset.php
@@ -0,0 +1,261 @@
+
+ *
+ * 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\Biconnector\Dataset\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\TransportException;
+use Bitrix24\SDK\Core\Result\FieldsResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\AddedDatasetResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DatasetResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DatasetsResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DeletedDatasetResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\UpdatedDatasetResult;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['biconnector']))]
+class Dataset extends AbstractService
+{
+ /**
+ * Dataset constructor
+ */
+ public function __construct(public Batch $batch, CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Add a new dataset
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-add.html
+ *
+ * @param array{
+ * name: string,
+ * externalName: string,
+ * externalCode: string,
+ * sourceId: int,
+ * description?: string,
+ * fields?: array,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.dataset.add',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-add.html',
+ 'Add a new dataset'
+ )]
+ public function add(array $fields): AddedDatasetResult
+ {
+ return new AddedDatasetResult(
+ $this->core->call(
+ 'biconnector.dataset.add',
+ [
+ 'fields' => $fields,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Update an existing dataset
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-update.html
+ *
+ * @param array{
+ * description?: string,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.dataset.update',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-update.html',
+ 'Update an existing dataset'
+ )]
+ public function update(int $id, array $fields): UpdatedDatasetResult
+ {
+ return new UpdatedDatasetResult(
+ $this->core->call(
+ 'biconnector.dataset.update',
+ [
+ 'id' => $id,
+ 'fields' => $fields,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get a dataset by its ID
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.dataset.get',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-get.html',
+ 'Get a dataset by its ID'
+ )]
+ public function get(int $id): DatasetResult
+ {
+ return new DatasetResult(
+ $this->core->call(
+ 'biconnector.dataset.get',
+ [
+ 'id' => $id,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get a list of datasets
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-list.html
+ *
+ * @param array $order - sort fields, e.g. ['dateCreate' => 'DESC']
+ * @param array $filter - filter fields
+ * @param array $select - fields to include in the result
+ * @param int $page - page number for pagination (page size is 50 records per page)
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.dataset.list',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-list.html',
+ 'Get a list of datasets'
+ )]
+ public function list(array $order = [], array $filter = [], array $select = [], int $page = 1): DatasetsResult
+ {
+ return new DatasetsResult(
+ $this->core->call(
+ 'biconnector.dataset.list',
+ [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => $page,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Delete a dataset by its ID
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.dataset.delete',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-delete.html',
+ 'Delete a dataset by its ID'
+ )]
+ public function delete(int $id): DeletedDatasetResult
+ {
+ return new DeletedDatasetResult(
+ $this->core->call(
+ 'biconnector.dataset.delete',
+ [
+ 'id' => $id,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get the fields description for datasets
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-fields.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.dataset.fields',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-fields.html',
+ 'Get the fields description for datasets'
+ )]
+ public function fields(): FieldsResult
+ {
+ return new FieldsResult($this->core->call('biconnector.dataset.fields'));
+ }
+
+ /**
+ * Update fields of an existing dataset (add, update visibility, or delete dataset columns)
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-fields-update.html
+ *
+ * @param array $add - fields to add
+ * @param array $update - fields to update (visibility)
+ * @param int[] $delete - field IDs to delete
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.dataset.fields.update',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/dataset/biconnector-dataset-fields-update.html',
+ 'Update fields of an existing dataset'
+ )]
+ public function updateFields(int $id, array $add = [], array $update = [], array $delete = []): UpdatedDatasetResult
+ {
+ $params = ['id' => $id];
+
+ if ($add !== []) {
+ $params['add'] = $add;
+ }
+
+ if ($update !== []) {
+ $params['update'] = $update;
+ }
+
+ if ($delete !== []) {
+ $params['delete'] = $delete;
+ }
+
+ return new UpdatedDatasetResult(
+ $this->core->call('biconnector.dataset.fields.update', $params)
+ );
+ }
+
+ /**
+ * Count datasets
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function count(): int
+ {
+ $count = 0;
+ foreach ($this->batch->list() as $item) {
+ $count++;
+ }
+
+ return $count;
+ }
+}
+
diff --git a/src/Services/Biconnector/Source/Batch.php b/src/Services/Biconnector/Source/Batch.php
new file mode 100644
index 00000000..9cddffd0
--- /dev/null
+++ b/src/Services/Biconnector/Source/Batch.php
@@ -0,0 +1,265 @@
+
+ *
+ * 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\Biconnector\Source;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+use Generator;
+
+/**
+ * Class Batch
+ *
+ * Overrides base Batch to handle parameter naming differences in biconnector.source.* REST methods:
+ * - list uses 'page' (page number, 50 records per page) instead of 'start' (offset) for pagination
+ * - delete uses lowercase 'id' instead of 'ID'
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-list.html
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-delete.html
+ */
+class Batch extends \Bitrix24\SDK\Core\Batch
+{
+ /**
+ * Determines the ID key — lowercase 'id' for biconnector source
+ */
+ #[\Override]
+ protected function determineKeyId(string $apiMethod, ?array $additionalParameters): string
+ {
+ return 'id';
+ }
+
+ /**
+ * Delete entity items with batch call using lowercase 'id' parameter
+ *
+ * @param int[] $entityItemId
+ * @param array|null $additionalParameters
+ *
+ * @return Generator|ResponseData[]
+ * @throws BaseException
+ */
+ #[\Override]
+ public function deleteEntityItems(
+ string $apiMethod,
+ array $entityItemId,
+ ?array $additionalParameters = null
+ ): Generator {
+ $this->logger->debug(
+ 'deleteEntityItems.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'entityItems' => $entityItemId,
+ 'additionalParameters' => $additionalParameters,
+ ]
+ );
+
+ try {
+ $this->clearCommands();
+ foreach ($entityItemId as $cnt => $itemId) {
+ if (!is_int($itemId)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'invalid type «%s» of source id «%s» at position %s, source id must be integer type',
+ gettype($itemId),
+ $itemId,
+ $cnt
+ )
+ );
+ }
+
+ $this->registerCommand($apiMethod, ['id' => $itemId]);
+ }
+
+ foreach ($this->getTraversable(true) as $cnt => $deletedItemResult) {
+ yield $cnt => $deletedItemResult;
+ }
+ } catch (InvalidArgumentException $exception) {
+ $errorMessage = sprintf('batch delete source items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+ throw $exception;
+ } catch (\Throwable $exception) {
+ $errorMessage = sprintf('batch delete source items: %s', $exception->getMessage());
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'trace' => $exception->getTrace(),
+ ]
+ );
+
+ throw new BaseException($errorMessage, $exception->getCode(), $exception);
+ }
+
+ $this->logger->debug('deleteEntityItems.finish');
+ }
+
+ /**
+ * Get traversable list using page-based pagination.
+ *
+ * The biconnector.source.list method uses 'page' parameter (page number, 50 records per page)
+ * instead of the standard 'start' (offset) parameter used by most other REST methods.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-list.html
+ *
+ * @param array $order
+ * @param array $filter
+ * @param array $select
+ *
+ * @return Generator
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ public function getTraversableList(
+ string $apiMethod,
+ ?array $order = [],
+ ?array $filter = [],
+ ?array $select = [],
+ ?int $limit = null,
+ ?array $additionalParameters = null
+ ): Generator {
+ yield from $this->getTraversableListWithCount(
+ $apiMethod,
+ $order ?? [],
+ $filter ?? [],
+ $select ?? [],
+ $limit,
+ $additionalParameters
+ );
+ }
+
+ /**
+ * Get traversable list using page-based pagination (page number, 50 records per page).
+ *
+ * The biconnector.source.list method accepts 'page' parameter instead of 'start'.
+ * Page 1 returns items 1–50, page 2 returns items 51–100, etc.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-list.html
+ *
+ * @param array $order
+ * @param array $filter
+ * @param array $select
+ *
+ * @return Generator
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ public function getTraversableListWithCount(
+ string $apiMethod,
+ array $order,
+ array $filter,
+ array $select,
+ ?int $limit = null,
+ ?array $additionalParameters = null
+ ): Generator {
+ $this->logger->debug(
+ 'getTraversableListWithCount.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'limit' => $limit,
+ 'additionalParameters' => $additionalParameters,
+ ]
+ );
+
+ $this->clearCommands();
+
+ // Fetch first page to determine total count
+ $params = [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => 1,
+ ];
+
+ if ($additionalParameters !== null) {
+ $params = array_merge($params, $additionalParameters);
+ }
+
+ $response = $this->core->call($apiMethod, $params);
+ $total = $response->getResponseData()->getPagination()->getTotal();
+
+ $this->logger->debug(
+ 'getTraversableListWithCount.totalElementsCount',
+ [
+ 'totalElementsCount' => $total,
+ ]
+ );
+
+ if ($total <= self::MAX_ELEMENTS_IN_PAGE) {
+ $elementsCounter = 0;
+ foreach ($response->getResponseData()->getResult() as $item) {
+ $elementsCounter++;
+ if ($limit !== null && $elementsCounter > $limit) {
+ return;
+ }
+
+ yield $item;
+ }
+
+ return;
+ }
+
+ // Register batch commands for all pages
+ $totalPages = (int)ceil($total / self::MAX_ELEMENTS_IN_PAGE);
+ for ($page = 1; $page <= $totalPages; $page++) {
+ $pageParams = [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => $page,
+ ];
+
+ if ($additionalParameters !== null) {
+ $pageParams = array_merge($pageParams, $additionalParameters);
+ }
+
+ $this->registerCommand($apiMethod, $pageParams);
+
+ if ($limit !== null && $limit < $page * self::MAX_ELEMENTS_IN_PAGE) {
+ break;
+ }
+ }
+
+ $this->logger->debug(
+ 'getTraversableListWithCount.commandsRegistered',
+ [
+ 'commandsCount' => $this->commands->count(),
+ 'totalItemsToSelect' => $total,
+ ]
+ );
+
+ $elementsCounter = 0;
+ foreach ($this->getTraversable(true) as $queryResultData) {
+ $resultElements = $this->extractElementsFromBatchResult($queryResultData, false);
+ foreach ($resultElements as $resultElement) {
+ ++$elementsCounter;
+ if ($limit !== null && $elementsCounter > $limit) {
+ return;
+ }
+
+ yield $resultElement;
+ }
+ }
+
+ $this->logger->debug('getTraversableListWithCount.finish');
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/AddedSourceBatchResult.php b/src/Services/Biconnector/Source/Result/AddedSourceBatchResult.php
new file mode 100644
index 00000000..f9107407
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/AddedSourceBatchResult.php
@@ -0,0 +1,34 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Result\AddedItemBatchResult;
+
+/**
+ * Class AddedSourceBatchResult
+ */
+class AddedSourceBatchResult extends AddedItemBatchResult
+{
+ #[\Override]
+ public function getId(): int
+ {
+ $result = $this->getResponseData()->getResult();
+
+ if (!empty($result['id'])) {
+ return (int)$result['id'];
+ }
+
+ return (int)$result;
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/AddedSourceResult.php b/src/Services/Biconnector/Source/Result/AddedSourceResult.php
new file mode 100644
index 00000000..a4c73f89
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/AddedSourceResult.php
@@ -0,0 +1,41 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AddedItemResult;
+
+/**
+ * Class AddedSourceResult
+ *
+ * Wraps the response from biconnector.source.add.
+ * The API returns: result.id (integer)
+ */
+class AddedSourceResult extends AddedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function getId(): int
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (!empty($result['id'])) {
+ return (int)$result['id'];
+ }
+
+ return (int)$result;
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/DeletedSourceBatchResult.php b/src/Services/Biconnector/Source/Result/DeletedSourceBatchResult.php
new file mode 100644
index 00000000..f3e927f9
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/DeletedSourceBatchResult.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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Result\DeletedItemBatchResult;
+
+/**
+ * Class DeletedSourceBatchResult
+ */
+class DeletedSourceBatchResult extends DeletedItemBatchResult
+{
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/DeletedSourceResult.php b/src/Services/Biconnector/Source/Result/DeletedSourceResult.php
new file mode 100644
index 00000000..3808c934
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/DeletedSourceResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\DeletedItemResult;
+
+/**
+ * Class DeletedSourceResult
+ */
+class DeletedSourceResult extends DeletedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getCoreResponse()->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/SourceItemResult.php b/src/Services/Biconnector/Source/Result/SourceItemResult.php
new file mode 100644
index 00000000..2cde005d
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/SourceItemResult.php
@@ -0,0 +1,53 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * Class SourceItemResult
+ *
+ * Field names correspond to the actual API response returned by biconnector.source.get / biconnector.source.list.
+ *
+ * @see https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-fields.html
+ *
+ * @property-read int $id
+ * @property-read string $title
+ * @property-read string|null $type
+ * @property-read string|null $code
+ * @property-read string|null $description
+ * @property-read bool|null $active
+ * @property-read CarbonImmutable $dateCreate
+ * @property-read CarbonImmutable $dateUpdate
+ * @property-read int $createdById
+ * @property-read int $updatedById
+ * @property-read int $connectorId
+ * @property-read array|null $settings
+ */
+class SourceItemResult extends AbstractItem
+{
+ #[\Override]
+ public function __get($offset): mixed
+ {
+ return match ($offset) {
+ 'id', 'createdById', 'updatedById', 'connectorId' => isset($this->data[$offset]) ? (int)$this->data[$offset] : null,
+ 'active' => isset($this->data[$offset]) ? (bool)$this->data[$offset] : null,
+ 'dateCreate', 'dateUpdate' => isset($this->data[$offset])
+ ? CarbonImmutable::parse($this->data[$offset])
+ : null,
+ default => $this->data[$offset] ?? null,
+ };
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/SourceResult.php b/src/Services/Biconnector/Source/Result/SourceResult.php
new file mode 100644
index 00000000..3f8534fa
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/SourceResult.php
@@ -0,0 +1,55 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Class SourceResult
+ *
+ * Wraps the response from biconnector.source.get.
+ *
+ * The API returns:
+ * result.item.connection.{id, type, code, title, description, active, dateCreate, dateUpdate, createdById, updatedById}
+ * result.item.connectorId
+ * result.item.settings
+ *
+ * We flatten connection fields to the root level so SourceItemResult has a consistent flat structure.
+ */
+class SourceResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function source(): SourceItemResult
+ {
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (!empty($result['item']) && is_array($result['item'])) {
+ $item = $result['item'];
+
+ // Flatten nested 'connection' fields to root level
+ if (!empty($item['connection']) && is_array($item['connection'])) {
+ $connection = $item['connection'];
+ unset($item['connection']);
+ $item = array_merge($connection, $item);
+ }
+
+ return new SourceItemResult($item);
+ }
+
+ return new SourceItemResult($result);
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/SourcesResult.php b/src/Services/Biconnector/Source/Result/SourcesResult.php
new file mode 100644
index 00000000..eddc14b3
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/SourcesResult.php
@@ -0,0 +1,44 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Class SourcesResult
+ *
+ * Wraps the response from biconnector.source.list.
+ * The API returns a flat array of source items.
+ */
+class SourcesResult extends AbstractResult
+{
+ /**
+ * @return SourceItemResult[]
+ * @throws BaseException
+ */
+ public function getSources(): array
+ {
+ $items = [];
+ $result = $this->getCoreResponse()->getResponseData()->getResult();
+
+ if (array_is_list($result)) {
+ foreach ($result as $item) {
+ $items[] = new SourceItemResult($item);
+ }
+ }
+
+ return $items;
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/UpdatedSourceBatchResult.php b/src/Services/Biconnector/Source/Result/UpdatedSourceBatchResult.php
new file mode 100644
index 00000000..99525485
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/UpdatedSourceBatchResult.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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Result\UpdatedItemBatchResult;
+
+/**
+ * Class UpdatedSourceBatchResult
+ */
+class UpdatedSourceBatchResult extends UpdatedItemBatchResult
+{
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Source/Result/UpdatedSourceResult.php b/src/Services/Biconnector/Source/Result/UpdatedSourceResult.php
new file mode 100644
index 00000000..aa01634f
--- /dev/null
+++ b/src/Services/Biconnector/Source/Result/UpdatedSourceResult.php
@@ -0,0 +1,32 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\UpdatedItemResult;
+
+/**
+ * Class UpdatedSourceResult
+ */
+class UpdatedSourceResult extends UpdatedItemResult
+{
+ /**
+ * @throws BaseException
+ */
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return (bool)$this->getCoreResponse()->getResponseData()->getResult();
+ }
+}
diff --git a/src/Services/Biconnector/Source/Service/Batch.php b/src/Services/Biconnector/Source/Service/Batch.php
new file mode 100644
index 00000000..5a988272
--- /dev/null
+++ b/src/Services/Biconnector/Source/Service/Batch.php
@@ -0,0 +1,169 @@
+
+ *
+ * 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\Biconnector\Source\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\Biconnector\Source\Result\AddedSourceBatchResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\DeletedSourceBatchResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\SourceItemResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\UpdatedSourceBatchResult;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+#[ApiBatchServiceMetadata(new Scope(['biconnector']))]
+class Batch
+{
+ /**
+ * Batch constructor
+ */
+ public function __construct(protected BatchOperationsInterface $batch, protected LoggerInterface $log)
+ {
+ }
+
+ /**
+ * Batch list sources
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-list.html
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.source.list',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-list.html',
+ 'Batch list sources'
+ )]
+ 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->getTraversableList(
+ 'biconnector.source.list',
+ $order,
+ $filter,
+ $select,
+ $limit
+ ) as $key => $value
+ ) {
+ yield $key => new SourceItemResult($value);
+ }
+ }
+
+ /**
+ * Batch add sources
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-add.html
+ *
+ * @param array $sources
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.source.add',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-add.html',
+ 'Batch add sources'
+ )]
+ public function add(array $sources): Generator
+ {
+ $items = [];
+ foreach ($sources as $item) {
+ $items[] = [
+ 'fields' => $item,
+ ];
+ }
+
+ foreach ($this->batch->addEntityItems('biconnector.source.add', $items) as $key => $item) {
+ yield $key => new AddedSourceBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch update sources
+ *
+ * Update elements in array with structure:
+ * id => [ // Source id
+ * 'fields' => [] // Source fields to update
+ * ]
+ *
+ * @param array $entityItems
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.source.update',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-update.html',
+ 'Batch update sources'
+ )]
+ public function update(array $entityItems): Generator
+ {
+ foreach (
+ $this->batch->updateEntityItems(
+ 'biconnector.source.update',
+ $entityItems
+ ) as $key => $item
+ ) {
+ yield $key => new UpdatedSourceBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch delete sources
+ *
+ * @param int[] $sourceIds
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'biconnector.source.delete',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-delete.html',
+ 'Batch delete sources'
+ )]
+ public function delete(array $sourceIds): Generator
+ {
+ foreach (
+ $this->batch->deleteEntityItems(
+ 'biconnector.source.delete',
+ $sourceIds
+ ) as $key => $item
+ ) {
+ yield $key => new DeletedSourceBatchResult($item);
+ }
+ }
+}
diff --git a/src/Services/Biconnector/Source/Service/Source.php b/src/Services/Biconnector/Source/Service/Source.php
new file mode 100644
index 00000000..3c4d7e4c
--- /dev/null
+++ b/src/Services/Biconnector/Source/Service/Source.php
@@ -0,0 +1,224 @@
+
+ *
+ * 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\Biconnector\Source\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\TransportException;
+use Bitrix24\SDK\Core\Result\FieldsResult;
+use Bitrix24\SDK\Services\AbstractService;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\AddedSourceResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\DeletedSourceResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\SourceResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\SourcesResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\UpdatedSourceResult;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['biconnector']))]
+class Source extends AbstractService
+{
+ /**
+ * Source constructor
+ */
+ public function __construct(public Batch $batch, CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Add a new data source
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-add.html
+ *
+ * @param array{
+ * title: string,
+ * connectorId: int,
+ * description?: string,
+ * active?: bool,
+ * settings?: array,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.source.add',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-add.html',
+ 'Add a new data source'
+ )]
+ public function add(array $fields): AddedSourceResult
+ {
+ return new AddedSourceResult(
+ $this->core->call(
+ 'biconnector.source.add',
+ [
+ 'fields' => $fields,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Update an existing data source
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-update.html
+ *
+ * @param array{
+ * title?: string,
+ * description?: string,
+ * active?: bool,
+ * settings?: array,
+ * } $fields
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.source.update',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-update.html',
+ 'Update an existing data source'
+ )]
+ public function update(int $id, array $fields): UpdatedSourceResult
+ {
+ return new UpdatedSourceResult(
+ $this->core->call(
+ 'biconnector.source.update',
+ [
+ 'id' => $id,
+ 'fields' => $fields,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get a data source by its ID
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-get.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.source.get',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-get.html',
+ 'Get a data source by its ID'
+ )]
+ public function get(int $id): SourceResult
+ {
+ return new SourceResult(
+ $this->core->call(
+ 'biconnector.source.get',
+ [
+ 'id' => $id,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get a list of data sources
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-list.html
+ *
+ * @param array $order - sort fields, e.g. ['id' => 'ASC']
+ * @param array $filter - filter fields
+ * @param array $select - fields to include in the result
+ * @param int $page - page number for pagination (page size is 50 records per page)
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.source.list',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-list.html',
+ 'Get a list of data sources'
+ )]
+ public function list(array $order = [], array $filter = [], array $select = [], int $page = 1): SourcesResult
+ {
+ return new SourcesResult(
+ $this->core->call(
+ 'biconnector.source.list',
+ [
+ 'order' => $order,
+ 'filter' => $filter,
+ 'select' => $select,
+ 'page' => $page,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Delete a data source by its ID
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-delete.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.source.delete',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-delete.html',
+ 'Delete a data source by its ID'
+ )]
+ public function delete(int $id): DeletedSourceResult
+ {
+ return new DeletedSourceResult(
+ $this->core->call(
+ 'biconnector.source.delete',
+ [
+ 'id' => $id,
+ ]
+ )
+ );
+ }
+
+ /**
+ * Get the fields description for data sources
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-fields.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'biconnector.source.fields',
+ 'https://apidocs.bitrix24.com/api-reference/biconnector/source/biconnector-source-fields.html',
+ 'Get the fields description for data sources'
+ )]
+ public function fields(): FieldsResult
+ {
+ return new FieldsResult($this->core->call('biconnector.source.fields'));
+ }
+
+ /**
+ * Count data sources
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function count(): int
+ {
+ $count = 0;
+ foreach ($this->batch->list() as $item) {
+ $count++;
+ }
+
+ return $count;
+ }
+}
diff --git a/src/Services/ServiceBuilder.php b/src/Services/ServiceBuilder.php
index e76b5e06..992f1587 100644
--- a/src/Services/ServiceBuilder.php
+++ b/src/Services/ServiceBuilder.php
@@ -17,6 +17,7 @@
use Bitrix24\SDK\Core\Contracts\BulkItemsReaderInterface;
use Bitrix24\SDK\Core\Contracts\CoreInterface;
use Bitrix24\SDK\Services\AI\AIServiceBuilder;
+use Bitrix24\SDK\Services\Biconnector\BiconnectorServiceBuilder;
use Bitrix24\SDK\Services\Catalog\CatalogServiceBuilder;
use Bitrix24\SDK\Services\CRM\CRMServiceBuilder;
use Bitrix24\SDK\Services\Disk\DiskServiceBuilder;
@@ -49,6 +50,20 @@ public function __construct(
parent::__construct($core, $batch, $bulkItemsReader, $log);
}
+ public function getBiconnectorScope(): BiconnectorServiceBuilder
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new BiconnectorServiceBuilder(
+ $this->core,
+ $this->batch,
+ $this->bulkItemsReader,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+
public function getSaleScope(): SaleServiceBuilder
{
if (!isset($this->serviceCache[__METHOD__])) {
diff --git a/tests/Integration/Services/Biconnector/Connector/Result/ConnectorItemResultAnnotationsTest.php b/tests/Integration/Services/Biconnector/Connector/Result/ConnectorItemResultAnnotationsTest.php
new file mode 100644
index 00000000..cb342962
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Connector/Result/ConnectorItemResultAnnotationsTest.php
@@ -0,0 +1,104 @@
+
+ *
+ * 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\Biconnector\Connector\Result;
+
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\ConnectorItemResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+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(ConnectorItemResult::class)]
+class ConnectorItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Connector $connectorService;
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->connectorService = Fabric::getServiceBuilder(true)->getBiconnectorScope()->connector();
+ }
+
+ #[Test]
+ #[TestDox('all fields in ConnectorItemResult are annotated and match live API fields schema')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $fieldCodes = $this->getConnectorFieldCodes();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ $fieldCodes,
+ ConnectorItemResult::class
+ );
+ }
+
+ #[Test]
+ #[TestDox('all fields in ConnectorItemResult have valid type casting matching API fields schema')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $fieldTypesMap = $this->getConnectorFieldTypesMap();
+
+ $this->assertBitrix24AllResultItemFieldsHasValidTypeAnnotation(
+ $fieldTypesMap,
+ ConnectorItemResult::class
+ );
+ }
+
+ /**
+ * Returns list of field codes from biconnector.connector.fields API.
+ *
+ * @return array
+ */
+ private function getConnectorFieldCodes(): array
+ {
+ $raw = $this->connectorService->fields()->getFieldsDescription();
+ $fields = $raw['fields'] ?? [];
+
+ return array_column($fields, 'title');
+ }
+
+ /**
+ * Returns field type map compatible with assertBitrix24AllResultItemFieldsHasValidTypeAnnotation.
+ * Normalises biconnector-specific types to types known by the shared assertion.
+ *
+ * @return array
+ */
+ private function getConnectorFieldTypesMap(): array
+ {
+ $raw = $this->connectorService->fields()->getFieldsDescription();
+ $fields = $raw['fields'] ?? [];
+
+ $result = [];
+ foreach ($fields as $field) {
+ $apiType = $field['type'];
+
+ // biconnector uses 'boolean' — map to 'char' which the shared assertion handles as bool
+ if ($apiType === 'boolean') {
+ $apiType = 'char';
+ }
+
+ $result[$field['title']] = ['type' => $apiType];
+ }
+
+ return $result;
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Connector/Service/BatchTest.php b/tests/Integration/Services/Biconnector/Connector/Service/BatchTest.php
new file mode 100644
index 00000000..5314b6fc
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Connector/Service/BatchTest.php
@@ -0,0 +1,160 @@
+
+ *
+ * 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\Biconnector\Connector\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\ConnectorItemResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Batch;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->connectorService = Fabric::getServiceBuilder(true)->getBiconnectorScope()->connector();
+ $this->faker = Faker\Factory::create();
+ }
+
+ /**
+ * Returns the minimum set of required fields to create a connector.
+ *
+ * @return array{
+ * title: string,
+ * logo: string,
+ * urlCheck: string,
+ * urlData: string,
+ * urlTableList: string,
+ * urlTableDescription: string,
+ * settings: array,
+ * }
+ */
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://example.com/api/check',
+ 'urlTableList' => 'https://example.com/api/table_list',
+ 'urlTableDescription' => 'https://example.com/api/table_description',
+ 'urlData' => 'https://example.com/api/data',
+ 'settings' => [
+ [
+ 'name' => 'Host',
+ 'type' => 'STRING',
+ 'code' => 'host',
+ ],
+ [
+ 'name' => 'Port',
+ 'type' => 'STRING',
+ 'code' => 'port',
+ ],
+ [
+ 'name' => 'Database',
+ 'type' => 'STRING',
+ 'code' => 'database',
+ ],
+ [
+ 'name' => 'Username',
+ 'type' => 'STRING',
+ 'code' => 'username',
+ ],
+ [
+ 'name' => 'Password',
+ 'type' => 'STRING',
+ 'code' => 'password',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchList(): void
+ {
+ $title = 'connector-' . $this->faker->uuid();
+ $id = $this->connectorService->add($this->makeConnectorFields($title))->getId();
+
+ $count = 0;
+ foreach ($this->connectorService->batch->list([], [], [], 10) as $item) {
+ self::assertInstanceOf(ConnectorItemResult::class, $item);
+ $count++;
+ }
+
+ self::assertGreaterThanOrEqual(1, $count);
+
+ // Cleanup
+ $this->connectorService->delete($id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchAdd(): void
+ {
+ $connectors = [];
+ for ($i = 0; $i < 3; $i++) {
+ $connectors[] = $this->makeConnectorFields('connector-batch-' . $this->faker->uuid());
+ }
+
+ $addedIds = [];
+ foreach ($this->connectorService->batch->add($connectors) as $result) {
+ $addedIds[] = $result->getId();
+ self::assertGreaterThanOrEqual(1, $result->getId());
+ }
+
+ self::assertCount(3, $addedIds);
+
+ // Cleanup
+ foreach ($this->connectorService->batch->delete($addedIds) as $deleteResult) {
+ self::assertTrue($deleteResult->isSuccess());
+ }
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchDelete(): void
+ {
+ $ids = [];
+ for ($i = 0; $i < 2; $i++) {
+ $ids[] = $this->connectorService->add(
+ $this->makeConnectorFields('connector-del-batch-' . $this->faker->uuid())
+ )->getId();
+ }
+
+ foreach ($this->connectorService->batch->delete($ids) as $result) {
+ self::assertTrue($result->isSuccess());
+ }
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Connector/Service/ConnectorTest.php b/tests/Integration/Services/Biconnector/Connector/Service/ConnectorTest.php
new file mode 100644
index 00000000..86e51ac5
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Connector/Service/ConnectorTest.php
@@ -0,0 +1,211 @@
+
+ *
+ * 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\Biconnector\Connector\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Result\ConnectorItemResult;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(Connector::class)]
+class ConnectorTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->connectorService = Fabric::getServiceBuilder(true)->getBiconnectorScope()->connector();
+ $this->faker = Faker\Factory::create();
+ }
+
+ /**
+ * Returns the minimum set of required fields to create a connector.
+ *
+ * @return array{
+ * title: string,
+ * logo: string,
+ * urlCheck: string,
+ * urlData: string,
+ * urlTableList: string,
+ * urlTableDescription: string,
+ * settings: array,
+ * }
+ */
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://example.com/api/check',
+ 'urlTableList' => 'https://example.com/api/table_list',
+ 'urlTableDescription' => 'https://example.com/api/table_description',
+ 'urlData' => 'https://example.com/api/data',
+ 'settings' => [
+ [
+ 'name' => 'Host',
+ 'type' => 'STRING',
+ 'code' => 'host',
+ ],
+ [
+ 'name' => 'Port',
+ 'type' => 'STRING',
+ 'code' => 'port',
+ ],
+ [
+ 'name' => 'Database',
+ 'type' => 'STRING',
+ 'code' => 'database',
+ ],
+ [
+ 'name' => 'Username',
+ 'type' => 'STRING',
+ 'code' => 'username',
+ ],
+ [
+ 'name' => 'Password',
+ 'type' => 'STRING',
+ 'code' => 'password',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAdd(): void
+ {
+ $title = 'connector-' . $this->faker->uuid();
+ $id = $this->connectorService->add($this->makeConnectorFields($title))->getId();
+
+ self::assertGreaterThanOrEqual(1, $id);
+
+ // Cleanup
+ $this->connectorService->delete($id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testGet(): void
+ {
+ $title = 'connector-' . $this->faker->uuid();
+ $id = $this->connectorService->add($this->makeConnectorFields($title))->getId();
+
+ $connectorItemResult = $this->connectorService->get($id)->connector();
+ self::assertInstanceOf(ConnectorItemResult::class, $connectorItemResult);
+ self::assertEquals($id, $connectorItemResult->id);
+ self::assertEquals($title, $connectorItemResult->title);
+
+ // Cleanup
+ $this->connectorService->delete($id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testList(): void
+ {
+ $title = 'connector-' . $this->faker->uuid();
+ $id = $this->connectorService->add($this->makeConnectorFields($title))->getId();
+
+ $list = $this->connectorService->list()->getConnectors();
+ self::assertIsArray($list);
+ self::assertGreaterThanOrEqual(1, count($list));
+
+ // Cleanup
+ $this->connectorService->delete($id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testUpdate(): void
+ {
+ $title = 'connector-' . $this->faker->uuid();
+ $id = $this->connectorService->add($this->makeConnectorFields($title))->getId();
+
+ $newTitle = $title . '-updated';
+ self::assertTrue(
+ $this->connectorService->update($id, [
+ 'title' => $newTitle,
+ ])->isSuccess()
+ );
+
+ self::assertEquals($newTitle, $this->connectorService->get($id)->connector()->title);
+
+ // Cleanup
+ $this->connectorService->delete($id);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testDelete(): void
+ {
+ $title = 'connector-' . $this->faker->uuid();
+ $id = $this->connectorService->add($this->makeConnectorFields($title))->getId();
+
+ self::assertTrue($this->connectorService->delete($id)->isSuccess());
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testFields(): void
+ {
+ $fields = $this->connectorService->fields()->getFieldsDescription();
+ self::assertIsArray($fields);
+ self::assertNotEmpty($fields);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testCount(): void
+ {
+ $countBefore = $this->connectorService->count();
+
+ $title = 'connector-' . $this->faker->uuid();
+ $id = $this->connectorService->add($this->makeConnectorFields($title))->getId();
+
+ $countAfter = $this->connectorService->count();
+ self::assertEquals($countBefore + 1, $countAfter);
+
+ // Cleanup
+ $this->connectorService->delete($id);
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Dataset/Result/DatasetItemResultAnnotationsTest.php b/tests/Integration/Services/Biconnector/Dataset/Result/DatasetItemResultAnnotationsTest.php
new file mode 100644
index 00000000..5e69101b
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Dataset/Result/DatasetItemResultAnnotationsTest.php
@@ -0,0 +1,207 @@
+
+ *
+ * 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\Biconnector\Dataset\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DatasetItemResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Service\Dataset;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Source;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(DatasetItemResult::class)]
+class DatasetItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Dataset $datasetService;
+
+ private Source $sourceService;
+
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ private int $connectorId;
+
+ private int $sourceId;
+
+ private int $datasetId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ // setUp body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $biconnectorServiceBuilder = Fabric::getServiceBuilder(true)->getBiconnectorScope();
+ $this->datasetService = $biconnectorServiceBuilder->dataset();
+ $this->sourceService = $biconnectorServiceBuilder->source();
+ $this->connectorService = $biconnectorServiceBuilder->connector();
+ $this->faker = Faker\Factory::create();
+
+ // Create connector, source, and dataset for annotation tests
+ $this->connectorId = $this->connectorService->add($this->makeConnectorFields(
+ 'connector-annotations-' . $this->faker->uuid()
+ ))->getId();
+
+ $this->sourceId = $this->sourceService->add([
+ 'title' => 'source-annotations-' . $this->faker->uuid(),
+ 'connectorId' => $this->connectorId,
+ 'settings' => [
+ 'host' => '172.18.0.2',
+ 'port' => '3306',
+ 'database' => 'customer_db',
+ 'username' => 'testuser',
+ 'password' => 'testpass123',
+ ],
+ ])->getId();
+
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $this->datasetId = $this->datasetService->add([
+ 'sourceId' => $this->sourceId,
+ 'name' => $name,
+ 'externalName' => 'order_items',
+ 'externalCode' => 'order_items',
+ 'fields' => [
+ ['type' => 'int', 'name' => 'ID', 'externalCode' => 'ID'],
+ ],
+ ])->getId();
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // tearDown body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ /*
+ try {
+ $this->datasetService->delete($this->datasetId);
+ } catch (\Throwable) {
+ }
+
+ try {
+ $this->sourceService->delete($this->sourceId);
+ } catch (\Throwable) {
+ }
+
+ try {
+ $this->connectorService->delete($this->connectorId);
+ } catch (\Throwable) {
+ }
+ */
+ }
+
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=check',
+ 'urlTableList' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_list',
+ 'urlTableDescription' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_description',
+ 'urlData' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=data',
+ 'settings' => [
+ ['name' => 'Host', 'type' => 'STRING', 'code' => 'host'],
+ ['name' => 'Port', 'type' => 'STRING', 'code' => 'port'],
+ ['name' => 'Database', 'type' => 'STRING', 'code' => 'database'],
+ ['name' => 'Username', 'type' => 'STRING', 'code' => 'username'],
+ ['name' => 'Password', 'type' => 'STRING', 'code' => 'password'],
+ ],
+ ];
+ }
+
+ #[Test]
+ #[TestDox('all fields in DatasetItemResult are annotated and match live API fields schema')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $fieldCodes = $this->getDatasetFieldCodes();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ $fieldCodes,
+ DatasetItemResult::class
+ );
+ */
+ }
+
+ #[Test]
+ #[TestDox('all fields in DatasetItemResult have valid type casting matching API fields schema')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $fieldTypesMap = $this->getDatasetFieldTypesMap();
+
+ $this->assertBitrix24AllResultItemFieldsHasValidTypeAnnotation(
+ $fieldTypesMap,
+ DatasetItemResult::class
+ );
+ */
+ }
+
+ /**
+ * Returns list of field codes from biconnector.dataset.fields API.
+ *
+ * @return array
+ */
+ private function getDatasetFieldCodes(): array
+ {
+ $raw = $this->datasetService->fields()->getFieldsDescription();
+ $fields = $raw['fields'] ?? [];
+
+ return array_column($fields, 'title');
+ }
+
+ /**
+ * Returns field type map compatible with assertBitrix24AllResultItemFieldsHasValidTypeAnnotation.
+ *
+ * @return array
+ */
+ private function getDatasetFieldTypesMap(): array
+ {
+ $raw = $this->datasetService->fields()->getFieldsDescription();
+ $fields = $raw['fields'] ?? [];
+
+ $result = [];
+ foreach ($fields as $field) {
+ $result[$field['title']] = ['type' => $field['type']];
+ }
+
+ return $result;
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Dataset/Service/BatchTest.php b/tests/Integration/Services/Biconnector/Dataset/Service/BatchTest.php
new file mode 100644
index 00000000..a79969c9
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Dataset/Service/BatchTest.php
@@ -0,0 +1,212 @@
+
+ *
+ * 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\Biconnector\Dataset\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DatasetItemResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Service\Batch;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Service\Dataset;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Source;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ private Dataset $datasetService;
+
+ private Source $sourceService;
+
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ private int $connectorId;
+
+ private int $sourceId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ // setUp body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $biconnectorServiceBuilder = Fabric::getServiceBuilder(true)->getBiconnectorScope();
+ $this->datasetService = $biconnectorServiceBuilder->dataset();
+ $this->sourceService = $biconnectorServiceBuilder->source();
+ $this->connectorService = $biconnectorServiceBuilder->connector();
+ $this->faker = Faker\Factory::create();
+
+ // Create a connector and source to use for dataset batch tests
+ $this->connectorId = $this->connectorService->add($this->makeConnectorFields(
+ 'connector-for-dataset-batch-' . $this->faker->uuid()
+ ))->getId();
+
+ $this->sourceId = $this->sourceService->add([
+ 'title' => 'source-for-dataset-batch-' . $this->faker->uuid(),
+ 'connectorId' => $this->connectorId,
+ 'settings' => [
+ 'host' => '172.18.0.2',
+ 'port' => '3306',
+ 'database' => 'customer_db',
+ 'username' => 'testuser',
+ 'password' => 'testpass123',
+ ],
+ ])->getId();
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // tearDown body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ /*
+ try {
+ $this->sourceService->delete($this->sourceId);
+ } catch (\Throwable) {
+ }
+
+ try {
+ $this->connectorService->delete($this->connectorId);
+ } catch (\Throwable) {
+ }
+ */
+ }
+
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=check',
+ 'urlTableList' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_list',
+ 'urlTableDescription' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_description',
+ 'urlData' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=data',
+ 'settings' => [
+ ['name' => 'Host', 'type' => 'STRING', 'code' => 'host'],
+ ['name' => 'Port', 'type' => 'STRING', 'code' => 'port'],
+ ['name' => 'Database', 'type' => 'STRING', 'code' => 'database'],
+ ['name' => 'Username', 'type' => 'STRING', 'code' => 'username'],
+ ['name' => 'Password', 'type' => 'STRING', 'code' => 'password'],
+ ],
+ ];
+ }
+
+ private function makeDatasetFields(string $name): array
+ {
+ return [
+ 'sourceId' => $this->sourceId,
+ 'name' => $name,
+ 'externalName' => 'order_items',
+ 'externalCode' => 'order_items',
+ 'fields' => [
+ ['type' => 'int', 'name' => 'ID', 'externalCode' => 'ID'],
+ ],
+ ];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchList(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ $count = 0;
+ foreach ($this->datasetService->batch->list([], [], [], 10) as $item) {
+ self::assertInstanceOf(DatasetItemResult::class, $item);
+ $count++;
+ }
+
+ self::assertGreaterThanOrEqual(1, $count);
+
+ // Cleanup
+ $this->datasetService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchAdd(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $datasets = [];
+ for ($i = 0; $i < 3; $i++) {
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $datasets[] = $this->makeDatasetFields($name);
+ }
+
+ $addedIds = [];
+ foreach ($this->datasetService->batch->add($datasets) as $result) {
+ $addedIds[] = $result->getId();
+ self::assertGreaterThanOrEqual(1, $result->getId());
+ }
+
+ self::assertCount(3, $addedIds);
+
+ // Cleanup
+ foreach ($this->datasetService->batch->delete($addedIds) as $deleteResult) {
+ self::assertTrue($deleteResult->isSuccess());
+ }
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchDelete(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $ids = [];
+ for ($i = 0; $i < 2; $i++) {
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $ids[] = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+ }
+
+ foreach ($this->datasetService->batch->delete($ids) as $result) {
+ self::assertTrue($result->isSuccess());
+ }
+ */
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Dataset/Service/DatasetTest.php b/tests/Integration/Services/Biconnector/Dataset/Service/DatasetTest.php
new file mode 100644
index 00000000..92951817
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Dataset/Service/DatasetTest.php
@@ -0,0 +1,317 @@
+
+ *
+ * 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\Biconnector\Dataset\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Result\DatasetItemResult;
+use Bitrix24\SDK\Services\Biconnector\Dataset\Service\Dataset;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Source;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(Dataset::class)]
+class DatasetTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Dataset $datasetService;
+
+ private Source $sourceService;
+
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ private int $connectorId;
+
+ private int $sourceId;
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ // setUp body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $biconnectorServiceBuilder = Fabric::getServiceBuilder(true)->getBiconnectorScope();
+ $this->datasetService = $biconnectorServiceBuilder->dataset();
+ $this->sourceService = $biconnectorServiceBuilder->source();
+ $this->connectorService = $biconnectorServiceBuilder->connector();
+ $this->faker = Faker\Factory::create();
+
+ // Create a connector and source to use for dataset tests
+ $this->connectorId = $this->connectorService->add($this->makeConnectorFields(
+ 'connector-for-dataset-' . $this->faker->uuid()
+ ))->getId();
+
+ $this->sourceId = $this->sourceService->add([
+ 'title' => 'source-for-dataset-' . $this->faker->uuid(),
+ 'connectorId' => $this->connectorId,
+ 'settings' => [
+ 'host' => '172.18.0.2',
+ 'port' => '3306',
+ 'database' => 'customer_db',
+ 'username' => 'testuser',
+ 'password' => 'testpass123',
+ ],
+ ])->getId();
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // tearDown body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ /*
+ try {
+ $this->sourceService->delete($this->sourceId);
+ } catch (\Throwable) {
+ }
+
+ try {
+ $this->connectorService->delete($this->connectorId);
+ } catch (\Throwable) {
+ }
+ */
+ }
+
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=check',
+ 'urlTableList' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_list',
+ 'urlTableDescription' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_description',
+ 'urlData' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=data',
+ 'settings' => [
+ ['name' => 'Host', 'type' => 'STRING', 'code' => 'host'],
+ ['name' => 'Port', 'type' => 'STRING', 'code' => 'port'],
+ ['name' => 'Database', 'type' => 'STRING', 'code' => 'database'],
+ ['name' => 'Username', 'type' => 'STRING', 'code' => 'username'],
+ ['name' => 'Password', 'type' => 'STRING', 'code' => 'password'],
+ ],
+ ];
+ }
+
+ /**
+ * Returns the minimum set of required fields to create a dataset.
+ */
+ private function makeDatasetFields(string $name): array
+ {
+ return [
+ 'sourceId' => $this->sourceId,
+ 'name' => $name,
+ 'externalName' => 'order_items',
+ 'externalCode' => 'order_items',
+ 'fields' => [
+ ['type' => 'int', 'name' => 'ID', 'externalCode' => 'ID'],
+ ],
+ ];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAdd(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ self::assertGreaterThanOrEqual(1, $id);
+
+ // Cleanup
+ $this->datasetService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testGet(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ $datasetItemResult = $this->datasetService->get($id)->dataset();
+ self::assertInstanceOf(DatasetItemResult::class, $datasetItemResult);
+ self::assertEquals($id, $datasetItemResult->id);
+ self::assertEquals($name, $datasetItemResult->name);
+
+ // Cleanup
+ $this->datasetService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testList(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ $list = $this->datasetService->list()->getDatasets();
+ self::assertIsArray($list);
+ self::assertGreaterThanOrEqual(1, count($list));
+
+ // Cleanup
+ $this->datasetService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testUpdate(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ $newDescription = 'Updated description ' . $this->faker->sentence();
+ self::assertTrue(
+ $this->datasetService->update($id, ['description' => $newDescription])->isSuccess()
+ );
+
+ self::assertEquals($newDescription, $this->datasetService->get($id)->dataset()->description);
+
+ // Cleanup
+ $this->datasetService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testDelete(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ self::assertTrue($this->datasetService->delete($id)->isSuccess());
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testFields(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $fields = $this->datasetService->fields()->getFieldsDescription();
+ self::assertIsArray($fields);
+ self::assertNotEmpty($fields);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testUpdateFields(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ // Get current field IDs
+ $datasetItemResult = $this->datasetService->get($id)->dataset();
+ $existingFields = $datasetItemResult->fields ?? [];
+ $fieldId = $existingFields[0]['id'] ?? null;
+
+ // Add a new field
+ $updatedDatasetResult = $this->datasetService->updateFields(
+ $id,
+ [['type' => 'string', 'name' => 'EXTRA', 'externalCode' => 'EXTRA']],
+ $fieldId !== null ? [['id' => $fieldId, 'visible' => false]] : [],
+ []
+ );
+
+ self::assertTrue($updatedDatasetResult->isSuccess());
+
+ // Cleanup
+ $this->datasetService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testCount(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $countBefore = $this->datasetService->count();
+
+ $name = 'ds' . substr(str_replace('-', '', $this->faker->uuid()), 0, 20);
+ $id = $this->datasetService->add($this->makeDatasetFields($name))->getId();
+
+ $countAfter = $this->datasetService->count();
+ self::assertEquals($countBefore + 1, $countAfter);
+
+ // Cleanup
+ $this->datasetService->delete($id);
+ */
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Source/Result/SourceItemResultAnnotationsTest.php b/tests/Integration/Services/Biconnector/Source/Result/SourceItemResultAnnotationsTest.php
new file mode 100644
index 00000000..a66ebd7b
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Source/Result/SourceItemResultAnnotationsTest.php
@@ -0,0 +1,215 @@
+
+ *
+ * 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\Biconnector\Source\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\SourceItemResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Source;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(SourceItemResult::class)]
+class SourceItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Source $sourceService;
+
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ private int $connectorId;
+
+ private int $sourceId;
+
+ /**
+ * @throws InvalidArgumentException
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ // setUp body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $builder = Fabric::getServiceBuilder(true)->getBiconnectorScope();
+ $this->sourceService = $builder->source();
+ $this->connectorService = $builder->connector();
+ $this->faker = Faker\Factory::create();
+
+ // Create a connector, then a source for annotation tests
+ $this->connectorId = $this->connectorService->add($this->makeConnectorFields(
+ 'connector-annotations-' . $this->faker->uuid()
+ ))->getId();
+
+ $this->sourceId = $this->sourceService->add([
+ 'title' => 'source-annotations-' . $this->faker->uuid(),
+ 'connectorId' => $this->connectorId,
+ 'settings' => [
+ 'host' => '172.18.0.2',
+ 'port' => '3306',
+ 'database' => 'customer_db',
+ 'username' => 'testuser',
+ 'password' => 'testpass123',
+ ],
+ ])->getId();
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // tearDown body is commented out: this test class requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ /*
+ try {
+ $this->sourceService->delete($this->sourceId);
+ } catch (\Throwable) {
+ }
+
+ try {
+ $this->connectorService->delete($this->connectorId);
+ } catch (\Throwable) {
+ }
+ */
+ }
+
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=check',
+ 'urlTableList' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_list',
+ 'urlTableDescription' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_description',
+ 'urlData' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=data',
+ 'settings' => [
+ [
+ 'name' => 'Host',
+ 'type' => 'STRING',
+ 'code' => 'host',
+ ],
+ [
+ 'name' => 'Port',
+ 'type' => 'STRING',
+ 'code' => 'port',
+ ],
+ [
+ 'name' => 'Database',
+ 'type' => 'STRING',
+ 'code' => 'database',
+ ],
+ [
+ 'name' => 'Username',
+ 'type' => 'STRING',
+ 'code' => 'username',
+ ],
+ [
+ 'name' => 'Password',
+ 'type' => 'STRING',
+ 'code' => 'password',
+ ],
+ ],
+ ];
+ }
+
+ #[Test]
+ #[TestDox('all fields in SourceItemResult are annotated and match live API fields schema')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $fieldCodes = $this->getSourceFieldCodes();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ $fieldCodes,
+ SourceItemResult::class
+ );
+ */
+ }
+
+ #[Test]
+ #[TestDox('all fields in SourceItemResult have valid type casting matching API fields schema')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $fieldTypesMap = $this->getSourceFieldTypesMap();
+
+ $this->assertBitrix24AllResultItemFieldsHasValidTypeAnnotation(
+ $fieldTypesMap,
+ SourceItemResult::class
+ );
+ */
+ }
+
+ /**
+ * Returns list of field codes from biconnector.source.fields API.
+ *
+ * @return array
+ */
+ private function getSourceFieldCodes(): array
+ {
+ $raw = $this->sourceService->fields()->getFieldsDescription();
+ $fields = $raw['fields'] ?? [];
+
+ return array_column($fields, 'title');
+ }
+
+ /**
+ * Returns field type map compatible with assertBitrix24AllResultItemFieldsHasValidTypeAnnotation.
+ * Normalises biconnector-specific types to types known by the shared assertion.
+ *
+ * @return array
+ */
+ private function getSourceFieldTypesMap(): array
+ {
+ $raw = $this->sourceService->fields()->getFieldsDescription();
+ $fields = $raw['fields'] ?? [];
+
+ $result = [];
+ foreach ($fields as $field) {
+ $apiType = $field['type'];
+
+ // biconnector uses 'boolean' — map to 'char' which the shared assertion handles as bool
+ if ($apiType === 'boolean') {
+ $apiType = 'char';
+ }
+
+ $result[$field['title']] = ['type' => $apiType];
+ }
+
+ return $result;
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Source/Service/BatchTest.php b/tests/Integration/Services/Biconnector/Source/Service/BatchTest.php
new file mode 100644
index 00000000..1915c7cd
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Source/Service/BatchTest.php
@@ -0,0 +1,205 @@
+
+ *
+ * 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\Biconnector\Source\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\SourceItemResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Batch;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Source;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ private Source $sourceService;
+
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ private int $connectorId;
+
+ /**
+ * @throws InvalidArgumentException
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $builder = Fabric::getServiceBuilder(true)->getBiconnectorScope();
+ $this->sourceService = $builder->source();
+ $this->connectorService = $builder->connector();
+ $this->faker = Faker\Factory::create();
+
+ // Create a connector to use for source tests
+ $this->connectorId = $this->connectorService->add($this->makeConnectorFields(
+ 'connector-for-source-batch-' . $this->faker->uuid()
+ ))->getId();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ try {
+ $this->connectorService->delete($this->connectorId);
+ } catch (\Throwable) {
+ // Ignore cleanup errors
+ }
+ }
+
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=check',
+ 'urlTableList' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_list',
+ 'urlTableDescription' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_description',
+ 'urlData' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=data',
+ 'settings' => [
+ [
+ 'name' => 'Host',
+ 'type' => 'STRING',
+ 'code' => 'host',
+ ],
+ [
+ 'name' => 'Port',
+ 'type' => 'STRING',
+ 'code' => 'port',
+ ],
+ [
+ 'name' => 'Database',
+ 'type' => 'STRING',
+ 'code' => 'database',
+ ],
+ [
+ 'name' => 'Username',
+ 'type' => 'STRING',
+ 'code' => 'username',
+ ],
+ [
+ 'name' => 'Password',
+ 'type' => 'STRING',
+ 'code' => 'password',
+ ],
+ ],
+ ];
+ }
+
+ private function makeSourceFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'connectorId' => $this->connectorId,
+ 'settings' => [
+ 'host' => '172.18.0.2',
+ 'port' => '3306',
+ 'database' => 'customer_db',
+ 'username' => 'testuser',
+ 'password' => 'testpass123',
+ ],
+ ];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchList(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $title = 'source-' . $this->faker->uuid();
+ $id = $this->sourceService->add($this->makeSourceFields($title))->getId();
+
+ $count = 0;
+ foreach ($this->sourceService->batch->list([], [], [], 10) as $item) {
+ self::assertInstanceOf(SourceItemResult::class, $item);
+ $count++;
+ }
+
+ self::assertGreaterThanOrEqual(1, $count);
+
+ // Cleanup
+ $this->sourceService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchAdd(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $sources = [];
+ for ($i = 0; $i < 3; $i++) {
+ $sources[] = $this->makeSourceFields('source-batch-' . $this->faker->uuid());
+ }
+
+ $addedIds = [];
+ foreach ($this->sourceService->batch->add($sources) as $result) {
+ $addedIds[] = $result->getId();
+ self::assertGreaterThanOrEqual(1, $result->getId());
+ }
+
+ self::assertCount(3, $addedIds);
+
+ // Cleanup
+ foreach ($this->sourceService->batch->delete($addedIds) as $deleteResult) {
+ self::assertTrue($deleteResult->isSuccess());
+ }
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testBatchDelete(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $ids = [];
+ for ($i = 0; $i < 2; $i++) {
+ $ids[] = $this->sourceService->add(
+ $this->makeSourceFields('source-del-batch-' . $this->faker->uuid())
+ )->getId();
+ }
+
+ foreach ($this->sourceService->batch->delete($ids) as $result) {
+ self::assertTrue($result->isSuccess());
+ }
+ */
+ }
+}
diff --git a/tests/Integration/Services/Biconnector/Source/Service/SourceTest.php b/tests/Integration/Services/Biconnector/Source/Service/SourceTest.php
new file mode 100644
index 00000000..80a0e432
--- /dev/null
+++ b/tests/Integration/Services/Biconnector/Source/Service/SourceTest.php
@@ -0,0 +1,285 @@
+
+ *
+ * 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\Biconnector\Source\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Biconnector\Connector\Service\Connector;
+use Bitrix24\SDK\Services\Biconnector\Source\Result\SourceItemResult;
+use Bitrix24\SDK\Services\Biconnector\Source\Service\Source;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Faker\Generator;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Faker;
+
+#[CoversClass(Source::class)]
+class SourceTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Source $sourceService;
+
+ private Connector $connectorService;
+
+ private Generator $faker;
+
+ private int $connectorId;
+
+ /**
+ * @throws InvalidArgumentException
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function setUp(): void
+ {
+ $builder = Fabric::getServiceBuilder(true)->getBiconnectorScope();
+ $this->sourceService = $builder->source();
+ $this->connectorService = $builder->connector();
+ $this->faker = Faker\Factory::create();
+
+ // Create a connector to use for source tests
+ $this->connectorId = $this->connectorService->add($this->makeConnectorFields(
+ 'connector-for-source-' . $this->faker->uuid()
+ ))->getId();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // Clean up the connector created for tests
+ try {
+ $this->connectorService->delete($this->connectorId);
+ } catch (\Throwable) {
+ // Ignore cleanup errors
+ }
+ }
+
+ /**
+ * Returns the minimum set of required fields to create a connector.
+ */
+ private function makeConnectorFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'logo' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIiIGhlaWdodD0iMjIiIHZpZXdCb3g9IjAgMCAyMiAyMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxjaXJjbGUgY3g9IjExIiBjeT0iMTEiIHI9IjEwIiBmaWxsPSIjRkYzQjNCIiAvPgoJPHRleHQgeD0iMTEiIHk9IjEzIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNiIgZmlsbD0iI0ZGRkZGRiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPlJFU1Q8L3RleHQ+Cjwvc3ZnPg==',
+ 'urlCheck' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=check',
+ 'urlTableList' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_list',
+ 'urlTableDescription' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=table_description',
+ 'urlData' => 'https://digitmind8080.cloudpub.ru/?connection_type=mysql&action=data',
+ 'settings' => [
+ [
+ 'name' => 'Host',
+ 'type' => 'STRING',
+ 'code' => 'host',
+ ],
+ [
+ 'name' => 'Port',
+ 'type' => 'STRING',
+ 'code' => 'port',
+ ],
+ [
+ 'name' => 'Database',
+ 'type' => 'STRING',
+ 'code' => 'database',
+ ],
+ [
+ 'name' => 'Username',
+ 'type' => 'STRING',
+ 'code' => 'username',
+ ],
+ [
+ 'name' => 'Password',
+ 'type' => 'STRING',
+ 'code' => 'password',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Returns the minimum set of required fields to create a source.
+ */
+ private function makeSourceFields(string $title): array
+ {
+ return [
+ 'title' => $title,
+ 'connectorId' => $this->connectorId,
+ 'settings' => [
+ 'host' => '172.18.0.2',
+ 'port' => '3306',
+ 'database' => 'customer_db',
+ 'username' => 'testuser',
+ 'password' => 'testpass123',
+ ],
+ ];
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testAdd(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $title = 'source-' . $this->faker->uuid();
+ $id = $this->sourceService->add($this->makeSourceFields($title))->getId();
+
+ self::assertGreaterThanOrEqual(1, $id);
+
+ // Cleanup
+ $this->sourceService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testGet(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $title = 'source-' . $this->faker->uuid();
+ $id = $this->sourceService->add($this->makeSourceFields($title))->getId();
+
+ $sourceItemResult = $this->sourceService->get($id)->source();
+ self::assertInstanceOf(SourceItemResult::class, $sourceItemResult);
+ self::assertEquals($id, $sourceItemResult->id);
+ self::assertEquals($title, $sourceItemResult->title);
+
+ // Cleanup
+ $this->sourceService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testList(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $title = 'source-' . $this->faker->uuid();
+ $id = $this->sourceService->add($this->makeSourceFields($title))->getId();
+
+ $list = $this->sourceService->list()->getSources();
+ self::assertIsArray($list);
+ self::assertGreaterThanOrEqual(1, count($list));
+
+ // Cleanup
+ $this->sourceService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testUpdate(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $title = 'source-' . $this->faker->uuid();
+ $sourceFields = $this->makeSourceFields($title);
+ $id = $this->sourceService->add($sourceFields)->getId();
+
+ $newTitle = $title . '-updated';
+ self::assertTrue(
+ $this->sourceService->update($id, [
+ 'title' => $newTitle,
+ 'settings' => $sourceFields['settings'],
+ ])->isSuccess()
+ );
+
+ self::assertEquals($newTitle, $this->sourceService->get($id)->source()->title);
+
+ // Cleanup
+ $this->sourceService->delete($id);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testDelete(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $title = 'source-' . $this->faker->uuid();
+ $id = $this->sourceService->add($this->makeSourceFields($title))->getId();
+
+ self::assertTrue($this->sourceService->delete($id)->isSuccess());
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testFields(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $fields = $this->sourceService->fields()->getFieldsDescription();
+ self::assertIsArray($fields);
+ self::assertNotEmpty($fields);
+ */
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ public function testCount(): void
+ {
+ // Test body is commented out: this test requires an additional external service
+ // (a real database accessible via the Biconnector connector).
+ $this->markTestSkipped('This test requires an additional external service (a real database accessible via the Biconnector connector).');
+ /*
+ $countBefore = $this->sourceService->count();
+
+ $title = 'source-' . $this->faker->uuid();
+ $id = $this->sourceService->add($this->makeSourceFields($title))->getId();
+
+ $countAfter = $this->sourceService->count();
+ self::assertEquals($countBefore + 1, $countAfter);
+
+ // Cleanup
+ $this->sourceService->delete($id);
+ */
+ }
+}
diff --git a/tests/Unit/Services/Biconnector/BiconnectorServiceBuilderTest.php b/tests/Unit/Services/Biconnector/BiconnectorServiceBuilderTest.php
new file mode 100644
index 00000000..b0884ef0
--- /dev/null
+++ b/tests/Unit/Services/Biconnector/BiconnectorServiceBuilderTest.php
@@ -0,0 +1,49 @@
+
+ *
+ * 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\Biconnector;
+
+use Bitrix24\SDK\Services\Biconnector\BiconnectorServiceBuilder;
+use Bitrix24\SDK\Tests\Unit\Stubs\NullBatch;
+use Bitrix24\SDK\Tests\Unit\Stubs\NullBulkItemsReader;
+use Bitrix24\SDK\Tests\Unit\Stubs\NullCore;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+
+#[CoversClass(BiconnectorServiceBuilder::class)]
+class BiconnectorServiceBuilderTest extends TestCase
+{
+ private BiconnectorServiceBuilder $serviceBuilder;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->serviceBuilder = new BiconnectorServiceBuilder(
+ new NullCore(),
+ new NullBatch(),
+ new NullBulkItemsReader(),
+ new NullLogger()
+ );
+ }
+
+ public function testConnectorServiceIsCached(): void
+ {
+ $this::assertSame($this->serviceBuilder->connector(), $this->serviceBuilder->connector());
+ }
+
+ public function testSourceServiceIsCached(): void
+ {
+ $this::assertSame($this->serviceBuilder->source(), $this->serviceBuilder->source());
+ }
+}
diff --git a/tests/Unit/Services/ServiceBuilderCacheTest.php b/tests/Unit/Services/ServiceBuilderCacheTest.php
index e78b8d11..d10f84cd 100644
--- a/tests/Unit/Services/ServiceBuilderCacheTest.php
+++ b/tests/Unit/Services/ServiceBuilderCacheTest.php
@@ -86,4 +86,8 @@ public function testGetTelephonyBuilder(): void
$this::assertSame($this->serviceBuilder->getTelephonyScope(), $this->serviceBuilder->getTelephonyScope());
}
+ public function testGetBiconnectorScopeBuilder(): void
+ {
+ $this::assertSame($this->serviceBuilder->getBiconnectorScope(), $this->serviceBuilder->getBiconnectorScope());
+ }
}