diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index e5c58cf21..60088a446 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -12,6 +12,8 @@
->in(__DIR__ . '/src/Services/CRM/Quote/')
->in(__DIR__ . '/src/Services/CRM/Lead/')
->in(__DIR__ . '/src/Services/CRM/Currency/')
+ ->in(__DIR__ . '/src/Services/CRM/Type/')
+ ->in(__DIR__ . '/src/Services/CRM/Userfieldconfig/')
->in(__DIR__ . '/src/Services/Catalog/')
->in(__DIR__ . '/src/Services/CRM/Requisites/')
->in(__DIR__ . '/src/Services/CRM/Status/')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30197e9c4..a71e05b19 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
## Unreleased
+### Added
+
+- Added service `Services\CRM\Userfieldconfig` with support methods,
+ see [userfieldconfig.* methods](https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/index.html) ([#605](https://github.com/bitrix24/b24phpsdk/issues/605)):
+ - `add` adds a new custom field for a CRM smart-process type, the new invoice or the sign document entity, with batch calls support
+ - `update` updates the settings of an existing custom field, with batch calls support
+ - `get` returns the settings of a custom field by its identifier
+ - `list` returns a list of custom field settings matching the filter
+ - `delete` deletes a custom field, with batch calls support
+ - `getTypes` returns the set of custom field types available for the specified module
+- Added support for events
+ see [userfieldconfig events](https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/events/index.html) ([#605](https://github.com/bitrix24/b24phpsdk/issues/605)):
+ - `onCrmTypeUserFieldAdd`
+ - `onCrmTypeUserFieldUpdate`
+ - `onCrmTypeUserFieldDelete`
+ - `onCrmTypeUserFieldSetEnumValues`
+
## 1.11.0 - 2026.08.31
### Added
diff --git a/Makefile b/Makefile
index 47ec23721..d2310a576 100644
--- a/Makefile
+++ b/Makefile
@@ -373,6 +373,14 @@ test-integration-sale-property-relation:
.PHONY: test-integration-scope-crm
test-integration-scope-crm:
docker-compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_crm
+
+.PHONY: test-integration-crm-type
+test-integration-crm-type:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_crm_type
+
+.PHONY: test-integration-crm-userfieldconfig
+test-integration-crm-userfieldconfig:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_crm_userfieldconfig
.PHONY: integration_tests_scope_crm_address
integration_tests_scope_crm_address:
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 62605da91..5af1b78a5 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -28,6 +28,8 @@ parameters:
- tests/Integration/Services/CRM/Quote/Service/QuoteProductRowsTest.php
- tests/Integration/Services/CRM/Lead/Service/LeadUserfieldTest.php
- tests/Integration/Services/CRM/Currency
+ - tests/Integration/Services/CRM/Type
+ - tests/Integration/Services/CRM/Userfieldconfig
- tests/Integration/Services/CRM/Requisites
- tests/Integration/Services/Task
- tests/Integration/Services/Sale
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 91ba4642d..67ce5e6fc 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -118,6 +118,12 @@
./tests/Integration/Services/CRM/Currency/
+
+ ./tests/Integration/Services/CRM/Type/
+
+
+ ./tests/Integration/Services/CRM/Userfieldconfig/
+
./tests/Integration/Services/CRM/Deal/Service/DealRecurringTest.php
diff --git a/rector.php b/rector.php
index 867d06ff5..47dc21253 100644
--- a/rector.php
+++ b/rector.php
@@ -54,6 +54,10 @@
__DIR__ . '/tests/Integration/Services/CRM/Quote/Service',
__DIR__ . '/src/Services/CRM/Currency',
__DIR__ . '/tests/Integration/Services/CRM/Currency',
+ __DIR__ . '/src/Services/CRM/Type',
+ __DIR__ . '/tests/Integration/Services/CRM/Type',
+ __DIR__ . '/src/Services/CRM/Userfieldconfig',
+ __DIR__ . '/tests/Integration/Services/CRM/Userfieldconfig',
__DIR__ . '/src/Services/CRM/Requisites',
__DIR__ . '/tests/Integration/Services/CRM/Requisites',
__DIR__ . '/src/Services/CRM/Timeline',
diff --git a/src/Services/CRM/CRMServiceBuilder.php b/src/Services/CRM/CRMServiceBuilder.php
index 429e0f4fb..3e8c502ce 100644
--- a/src/Services/CRM/CRMServiceBuilder.php
+++ b/src/Services/CRM/CRMServiceBuilder.php
@@ -719,4 +719,21 @@ public function type(): Type\Service\Type
return $this->serviceCache[__METHOD__];
}
+
+ public function userfieldConfig(): Userfieldconfig\Service\Userfieldconfig
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $batch = new Userfieldconfig\Batch(
+ $this->core,
+ $this->log
+ );
+ $this->serviceCache[__METHOD__] = new Userfieldconfig\Service\Userfieldconfig(
+ new Userfieldconfig\Service\Batch($batch, $this->log),
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
}
diff --git a/src/Services/CRM/Type/Events/CrmTypeEventsFactory.php b/src/Services/CRM/Type/Events/CrmTypeEventsFactory.php
new file mode 100644
index 000000000..836007daa
--- /dev/null
+++ b/src/Services/CRM/Type/Events/CrmTypeEventsFactory.php
@@ -0,0 +1,57 @@
+
+ *
+ * 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\CRM\Type\Events;
+
+use Bitrix24\SDK\Core\Contracts\Events\EventInterface;
+use Bitrix24\SDK\Core\Contracts\Events\EventsFabricInterface;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldAdd\OnCrmTypeUserFieldAdd;
+use Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldDelete\OnCrmTypeUserFieldDelete;
+use Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldSetEnumValues\OnCrmTypeUserFieldSetEnumValues;
+use Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldUpdate\OnCrmTypeUserFieldUpdate;
+use Symfony\Component\HttpFoundation\Request;
+
+readonly class CrmTypeEventsFactory implements EventsFabricInterface
+{
+ public function isSupport(string $eventCode): bool
+ {
+ return in_array(strtoupper($eventCode), [
+ OnCrmTypeUserFieldAdd::CODE,
+ OnCrmTypeUserFieldUpdate::CODE,
+ OnCrmTypeUserFieldDelete::CODE,
+ OnCrmTypeUserFieldSetEnumValues::CODE,
+ ], true);
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ public function create(Request $eventRequest): EventInterface
+ {
+ $eventPayload = $eventRequest->request->all();
+ if (!array_key_exists('event', $eventPayload)) {
+ throw new InvalidArgumentException('«event» key not found in event payload');
+ }
+
+ return match ($eventPayload['event']) {
+ OnCrmTypeUserFieldAdd::CODE => new OnCrmTypeUserFieldAdd($eventRequest),
+ OnCrmTypeUserFieldUpdate::CODE => new OnCrmTypeUserFieldUpdate($eventRequest),
+ OnCrmTypeUserFieldDelete::CODE => new OnCrmTypeUserFieldDelete($eventRequest),
+ OnCrmTypeUserFieldSetEnumValues::CODE => new OnCrmTypeUserFieldSetEnumValues($eventRequest),
+ default => throw new InvalidArgumentException(
+ sprintf('Unexpected event code «%s»', $eventPayload['event'])
+ ),
+ };
+ }
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldAdd/OnCrmTypeUserFieldAdd.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldAdd/OnCrmTypeUserFieldAdd.php
new file mode 100644
index 000000000..b6b7a6e3d
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldAdd/OnCrmTypeUserFieldAdd.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldAdd;
+
+use Bitrix24\SDK\Application\Requests\Events\AbstractEventRequest;
+
+class OnCrmTypeUserFieldAdd extends AbstractEventRequest
+{
+ public const CODE = 'ONCRMTYPEUSERFIELDADD';
+
+ public function getPayload(): OnCrmTypeUserFieldAddPayload
+ {
+ return new OnCrmTypeUserFieldAddPayload($this->eventPayload['data']);
+ }
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldAdd/OnCrmTypeUserFieldAddPayload.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldAdd/OnCrmTypeUserFieldAddPayload.php
new file mode 100644
index 000000000..627399d98
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldAdd/OnCrmTypeUserFieldAddPayload.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldAdd;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read positive-int $id
+ * @property-read non-empty-string $entityId
+ * @property-read non-empty-string $fieldName
+ */
+class OnCrmTypeUserFieldAddPayload extends AbstractItem
+{
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldDelete/OnCrmTypeUserFieldDelete.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldDelete/OnCrmTypeUserFieldDelete.php
new file mode 100644
index 000000000..49b1a0118
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldDelete/OnCrmTypeUserFieldDelete.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldDelete;
+
+use Bitrix24\SDK\Application\Requests\Events\AbstractEventRequest;
+
+class OnCrmTypeUserFieldDelete extends AbstractEventRequest
+{
+ public const CODE = 'ONCRMTYPEUSERFIELDDELETE';
+
+ public function getPayload(): OnCrmTypeUserFieldDeletePayload
+ {
+ return new OnCrmTypeUserFieldDeletePayload($this->eventPayload['data']);
+ }
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldDelete/OnCrmTypeUserFieldDeletePayload.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldDelete/OnCrmTypeUserFieldDeletePayload.php
new file mode 100644
index 000000000..a066b68d9
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldDelete/OnCrmTypeUserFieldDeletePayload.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldDelete;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read positive-int $id
+ * @property-read non-empty-string $entityId
+ * @property-read non-empty-string $fieldName
+ */
+class OnCrmTypeUserFieldDeletePayload extends AbstractItem
+{
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldSetEnumValues/OnCrmTypeUserFieldSetEnumValues.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldSetEnumValues/OnCrmTypeUserFieldSetEnumValues.php
new file mode 100644
index 000000000..fe3cf65d9
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldSetEnumValues/OnCrmTypeUserFieldSetEnumValues.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldSetEnumValues;
+
+use Bitrix24\SDK\Application\Requests\Events\AbstractEventRequest;
+
+class OnCrmTypeUserFieldSetEnumValues extends AbstractEventRequest
+{
+ public const CODE = 'ONCRMTYPEUSERFIELDSETENUMVALUES';
+
+ public function getPayload(): OnCrmTypeUserFieldSetEnumValuesPayload
+ {
+ return new OnCrmTypeUserFieldSetEnumValuesPayload($this->eventPayload['data']);
+ }
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldSetEnumValues/OnCrmTypeUserFieldSetEnumValuesPayload.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldSetEnumValues/OnCrmTypeUserFieldSetEnumValuesPayload.php
new file mode 100644
index 000000000..f5f9b7039
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldSetEnumValues/OnCrmTypeUserFieldSetEnumValuesPayload.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldSetEnumValues;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read positive-int $id
+ * @property-read non-empty-string $entityId
+ * @property-read non-empty-string $fieldName
+ */
+class OnCrmTypeUserFieldSetEnumValuesPayload extends AbstractItem
+{
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldUpdate/OnCrmTypeUserFieldUpdate.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldUpdate/OnCrmTypeUserFieldUpdate.php
new file mode 100644
index 000000000..a5a4ae08d
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldUpdate/OnCrmTypeUserFieldUpdate.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldUpdate;
+
+use Bitrix24\SDK\Application\Requests\Events\AbstractEventRequest;
+
+class OnCrmTypeUserFieldUpdate extends AbstractEventRequest
+{
+ public const CODE = 'ONCRMTYPEUSERFIELDUPDATE';
+
+ public function getPayload(): OnCrmTypeUserFieldUpdatePayload
+ {
+ return new OnCrmTypeUserFieldUpdatePayload($this->eventPayload['data']);
+ }
+}
diff --git a/src/Services/CRM/Type/Events/OnCrmTypeUserFieldUpdate/OnCrmTypeUserFieldUpdatePayload.php b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldUpdate/OnCrmTypeUserFieldUpdatePayload.php
new file mode 100644
index 000000000..423a47c71
--- /dev/null
+++ b/src/Services/CRM/Type/Events/OnCrmTypeUserFieldUpdate/OnCrmTypeUserFieldUpdatePayload.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Type\Events\OnCrmTypeUserFieldUpdate;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read positive-int $id
+ * @property-read non-empty-string $entityId
+ * @property-read non-empty-string $fieldName
+ */
+class OnCrmTypeUserFieldUpdatePayload extends AbstractItem
+{
+}
diff --git a/src/Services/CRM/Type/Result/AddedTypeItemResult.php b/src/Services/CRM/Type/Result/AddedTypeItemResult.php
index 7f54edefa..6f7b07482 100644
--- a/src/Services/CRM/Type/Result/AddedTypeItemResult.php
+++ b/src/Services/CRM/Type/Result/AddedTypeItemResult.php
@@ -31,4 +31,4 @@ public function getId(): int
{
return (int)$this->getCoreResponse()->getResponseData()->getResult()['type']['id'];
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/CRM/Type/Result/DeletedItemResult.php b/src/Services/CRM/Type/Result/DeletedItemResult.php
index 944fc0d88..8f8e0aea0 100644
--- a/src/Services/CRM/Type/Result/DeletedItemResult.php
+++ b/src/Services/CRM/Type/Result/DeletedItemResult.php
@@ -23,4 +23,4 @@ public function isSuccess(): bool
{
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/CRM/Type/Result/UpdatedTypeItemResult.php b/src/Services/CRM/Type/Result/UpdatedTypeItemResult.php
index 939c802fd..a729ee0e1 100644
--- a/src/Services/CRM/Type/Result/UpdatedTypeItemResult.php
+++ b/src/Services/CRM/Type/Result/UpdatedTypeItemResult.php
@@ -26,4 +26,4 @@ public function isSuccess(): bool
{
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/Services/CRM/Type/Service/Type.php b/src/Services/CRM/Type/Service/Type.php
index 66a27de3f..6af3e0580 100644
--- a/src/Services/CRM/Type/Service/Type.php
+++ b/src/Services/CRM/Type/Service/Type.php
@@ -35,7 +35,6 @@ class Type extends AbstractService
*
* @link https://apidocs.bitrix24.com/api-reference/crm/universal/user-defined-object-types/crm-type-fields.html
*
- * @return FieldsResult
* @throws BaseException
* @throws TransportException
*/
@@ -54,10 +53,6 @@ public function fields(): FieldsResult
*
* @link https://apidocs.bitrix24.com/api-reference/crm/universal/user-defined-object-types/crm-type-add.html
*
- * @param string $title
- * @param int|null $entityTypeId
- * @param array $parameters
- * @return AddedTypeItemResult
* @throws BaseException
* @throws TransportException
*/
@@ -104,7 +99,6 @@ public function update(int $id, array $fields): UpdatedTypeItemResult
*
* @link https://apidocs.bitrix24.com/api-reference/crm/universal/user-defined-object-types/crm-type-get.html
*
- * @return TypeResult
* @throws BaseException
* @throws TransportException
*/
@@ -123,8 +117,6 @@ public function get(int $id): TypeResult
*
* @link https://apidocs.bitrix24.com/api-reference/crm/universal/user-defined-object-types/crm-type-get-by-entity-type-id.html
*
- * @param int $entityTypeId
- * @return TypeResult
* @throws BaseException
* @throws TransportException
*/
@@ -143,10 +135,6 @@ public function getByEntityTypeId(int $entityTypeId): TypeResult
*
* @link https://apidocs.bitrix24.com/api-reference/crm/universal/user-defined-object-types/crm-type-get-by-entity-type-id.html
*
- * @param array $order
- * @param array $filter
- * @param int $start
- * @return TypesResult
* @throws BaseException
* @throws TransportException
*/
@@ -169,8 +157,6 @@ public function list(array $order = [], array $filter = [], int $start = 0): Typ
*
* @link https://apidocs.bitrix24.com/api-reference/crm/universal/user-defined-object-types/crm-type-delete.html
*
- * @param int $entityTypeId
- * @return DeletedItemResult
* @throws BaseException
* @throws TransportException
*/
diff --git a/src/Services/CRM/Userfieldconfig/Batch.php b/src/Services/CRM/Userfieldconfig/Batch.php
new file mode 100644
index 000000000..0f957ad59
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Batch.php
@@ -0,0 +1,138 @@
+
+ *
+ * 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\CRM\Userfieldconfig;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+use Generator;
+
+/**
+ * userfieldconfig.update and userfieldconfig.delete require a lowercase `id` key and a
+ * top-level `moduleId` key that the base \Bitrix24\SDK\Core\Batch does not build
+ * (it defaults to an uppercase `ID` key for deletes and has no concept of `moduleId`),
+ * so this scope needs its own batch command builder.
+ */
+class Batch extends \Bitrix24\SDK\Core\Batch
+{
+ /**
+ * Update entity items with batch call
+ *
+ * @param array> $entityItems keyed by userfieldconfig id
+ *
+ * @return Generator|ResponseData[]
+ * @throws BaseException
+ */
+ public function updateUserfieldConfigItems(string $apiMethod, string $moduleId, array $entityItems): Generator
+ {
+ $this->logger->debug(
+ 'updateUserfieldConfigItems.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'moduleId' => $moduleId,
+ 'entityItems' => $entityItems,
+ ]
+ );
+
+ try {
+ $this->clearCommands();
+ foreach ($entityItems as $entityItemId => $field) {
+ if (!is_int($entityItemId)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'invalid type «%s» of userfieldconfig id «%s», id must be integer type',
+ gettype($entityItemId),
+ $entityItemId
+ )
+ );
+ }
+
+ $this->registerCommand($apiMethod, [
+ 'moduleId' => $moduleId,
+ 'id' => $entityItemId,
+ 'field' => $field,
+ ]);
+ }
+
+ foreach ($this->getTraversable(true) as $cnt => $updatedItemResult) {
+ yield $cnt => $updatedItemResult;
+ }
+ } catch (InvalidArgumentException $exception) {
+ $errorMessage = sprintf('batch update userfieldconfig items: %s', $exception->getMessage());
+ $this->logger->error($errorMessage, ['trace' => $exception->getTrace()]);
+ throw $exception;
+ } catch (\Throwable $exception) {
+ $errorMessage = sprintf('batch update userfieldconfig items: %s', $exception->getMessage());
+ $this->logger->error($errorMessage, ['trace' => $exception->getTrace()]);
+ throw new BaseException($errorMessage, $exception->getCode(), $exception);
+ }
+
+ $this->logger->debug('updateUserfieldConfigItems.finish');
+ }
+
+ /**
+ * Delete entity items with batch call
+ *
+ * @param array $entityItemIds
+ *
+ * @return Generator|ResponseData[]
+ * @throws BaseException
+ */
+ public function deleteUserfieldConfigItems(string $apiMethod, string $moduleId, array $entityItemIds): Generator
+ {
+ $this->logger->debug(
+ 'deleteUserfieldConfigItems.start',
+ [
+ 'apiMethod' => $apiMethod,
+ 'moduleId' => $moduleId,
+ 'entityItemIds' => $entityItemIds,
+ ]
+ );
+
+ try {
+ $this->clearCommands();
+ foreach ($entityItemIds as $cnt => $entityItemId) {
+ if (!is_int($entityItemId)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'invalid type «%s» of userfieldconfig id «%s» at position %s, id must be integer type',
+ gettype($entityItemId),
+ $entityItemId,
+ $cnt
+ )
+ );
+ }
+
+ $this->registerCommand($apiMethod, [
+ 'moduleId' => $moduleId,
+ 'id' => $entityItemId,
+ ]);
+ }
+
+ foreach ($this->getTraversable(true) as $cnt => $deletedItemResult) {
+ yield $cnt => $deletedItemResult;
+ }
+ } catch (InvalidArgumentException $exception) {
+ $errorMessage = sprintf('batch delete userfieldconfig items: %s', $exception->getMessage());
+ $this->logger->error($errorMessage, ['trace' => $exception->getTrace()]);
+ throw $exception;
+ } catch (\Throwable $exception) {
+ $errorMessage = sprintf('batch delete userfieldconfig items: %s', $exception->getMessage());
+ $this->logger->error($errorMessage, ['trace' => $exception->getTrace()]);
+ throw new BaseException($errorMessage, $exception->getCode(), $exception);
+ }
+
+ $this->logger->debug('deleteUserfieldConfigItems.finish');
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/AddedUserfieldConfigItemBatchResult.php b/src/Services/CRM/Userfieldconfig/Result/AddedUserfieldConfigItemBatchResult.php
new file mode 100644
index 000000000..66c600c79
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/AddedUserfieldConfigItemBatchResult.php
@@ -0,0 +1,40 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Contracts\AddedItemIdResultInterface;
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+
+class AddedUserfieldConfigItemBatchResult implements AddedItemIdResultInterface
+{
+ public function __construct(private readonly ResponseData $responseData)
+ {
+ }
+
+ public function getResponseData(): ResponseData
+ {
+ return $this->responseData;
+ }
+
+ public function field(): UserfieldConfigItemResult
+ {
+ return new UserfieldConfigItemResult($this->responseData->getResult()['field']);
+ }
+
+ #[\Override]
+ public function getId(): int
+ {
+ return (int)$this->field()->id;
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/AddedUserfieldConfigItemResult.php b/src/Services/CRM/Userfieldconfig/Result/AddedUserfieldConfigItemResult.php
new file mode 100644
index 000000000..9e8a6d378
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/AddedUserfieldConfigItemResult.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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class AddedUserfieldConfigItemResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function field(): UserfieldConfigItemResult
+ {
+ return new UserfieldConfigItemResult($this->getCoreResponse()->getResponseData()->getResult()['field']);
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/DeletedUserfieldConfigItemBatchResult.php b/src/Services/CRM/Userfieldconfig/Result/DeletedUserfieldConfigItemBatchResult.php
new file mode 100644
index 000000000..51e0e1bb6
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/DeletedUserfieldConfigItemBatchResult.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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Contracts\DeletedItemResultInterface;
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+
+class DeletedUserfieldConfigItemBatchResult implements DeletedItemResultInterface
+{
+ public function __construct(private readonly ResponseData $responseData)
+ {
+ }
+
+ public function getResponseData(): ResponseData
+ {
+ return $this->responseData;
+ }
+
+ /**
+ * userfieldconfig.delete returns a null result on success, unlike the boolean
+ * `true` returned by most other *.delete methods.
+ */
+ #[\Override]
+ public function isSuccess(): bool
+ {
+ return $this->responseData->getResult()[0] === null;
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/DeletedUserfieldConfigItemResult.php b/src/Services/CRM/Userfieldconfig/Result/DeletedUserfieldConfigItemResult.php
new file mode 100644
index 000000000..67d1a6de8
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/DeletedUserfieldConfigItemResult.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class DeletedUserfieldConfigItemResult extends AbstractResult
+{
+ /**
+ * userfieldconfig.delete returns a null result on success, unlike the boolean
+ * `true` returned by most other *.delete methods.
+ *
+ * @throws BaseException
+ */
+ public function isSuccess(): bool
+ {
+ return $this->getCoreResponse()->getResponseData()->getResult()[0] === null;
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UpdatedUserfieldConfigItemBatchResult.php b/src/Services/CRM/Userfieldconfig/Result/UpdatedUserfieldConfigItemBatchResult.php
new file mode 100644
index 000000000..067a76124
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UpdatedUserfieldConfigItemBatchResult.php
@@ -0,0 +1,37 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Response\DTO\ResponseData;
+
+class UpdatedUserfieldConfigItemBatchResult
+{
+ public function __construct(private readonly ResponseData $responseData)
+ {
+ }
+
+ public function getResponseData(): ResponseData
+ {
+ return $this->responseData;
+ }
+
+ /**
+ * userfieldconfig.update returns the full updated field descriptor under the
+ * `field` key, same shape as userfieldconfig.add.
+ */
+ public function field(): UserfieldConfigItemResult
+ {
+ return new UserfieldConfigItemResult($this->responseData->getResult()['field']);
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UpdatedUserfieldConfigItemResult.php b/src/Services/CRM/Userfieldconfig/Result/UpdatedUserfieldConfigItemResult.php
new file mode 100644
index 000000000..66fba28f1
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UpdatedUserfieldConfigItemResult.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class UpdatedUserfieldConfigItemResult extends AbstractResult
+{
+ /**
+ * userfieldconfig.update returns the full updated field descriptor under the
+ * `field` key, same shape as userfieldconfig.add.
+ *
+ * @throws BaseException
+ */
+ public function field(): UserfieldConfigItemResult
+ {
+ return new UserfieldConfigItemResult($this->getCoreResponse()->getResponseData()->getResult()['field']);
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigEnumItemResult.php b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigEnumItemResult.php
new file mode 100644
index 000000000..eb54881e9
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigEnumItemResult.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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read positive-int $id
+ * @property-read positive-int $userFieldId
+ * @property-read string $value
+ * @property-read bool $def
+ * @property-read int $sort
+ * @property-read string|null $xmlId
+ */
+class UserfieldConfigEnumItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigItemResult.php b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigItemResult.php
new file mode 100644
index 000000000..c118010ae
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigItemResult.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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read positive-int $id
+ * @property-read non-empty-string $entityId
+ * @property-read non-empty-string $fieldName
+ * @property-read non-empty-string $userTypeId
+ * @property-read string|null $xmlId
+ * @property-read int $sort
+ * @property-read bool $multiple
+ * @property-read bool $mandatory
+ * @property-read string $showFilter
+ * @property-read bool $showInList
+ * @property-read bool $editInList
+ * @property-read bool $isSearchable
+ * @property-read array $settings
+ * @property-read array $languageId
+ * @property-read array $editFormLabel
+ * @property-read array $listColumnLabel
+ * @property-read array $listFilterLabel
+ * @property-read array $errorMessage
+ * @property-read array $helpMessage
+ * @property-read array|null $enum
+ */
+class UserfieldConfigItemResult extends AbstractItem
+{
+ /**
+ * @param int|string $offset
+ *
+ * @return mixed
+ */
+ #[\Override]
+ public function __get($offset)
+ {
+ return match ($offset) {
+ 'multiple', 'mandatory', 'showInList', 'editInList', 'isSearchable' => $this->data[$offset] === 'Y',
+ default => parent::__get($offset),
+ };
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigResult.php b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigResult.php
new file mode 100644
index 000000000..f15b12c23
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigResult.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the MIT-LICENSE.txt
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace Bitrix24\SDK\Services\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class UserfieldConfigResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function field(): ?UserfieldConfigItemResult
+ {
+ $field = $this->getCoreResponse()->getResponseData()->getResult()['field'];
+
+ return $field === null ? null : new UserfieldConfigItemResult($field);
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigTypeItemResult.php b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigTypeItemResult.php
new file mode 100644
index 000000000..5af86bbf2
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigTypeItemResult.php
@@ -0,0 +1,24 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * @property-read non-empty-string $userTypeId
+ * @property-read string $description
+ */
+class UserfieldConfigTypeItemResult extends AbstractItem
+{
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigTypesResult.php b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigTypesResult.php
new file mode 100644
index 000000000..a59c01211
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigTypesResult.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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class UserfieldConfigTypesResult extends AbstractResult
+{
+ /**
+ * @return array keyed by user field type code
+ * @throws BaseException
+ */
+ public function getTypes(): array
+ {
+ $items = [];
+ foreach ($this->getCoreResponse()->getResponseData()->getResult()['types'] as $typeCode => $item) {
+ $items[$typeCode] = new UserfieldConfigTypeItemResult($item);
+ }
+
+ return $items;
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigsResult.php b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigsResult.php
new file mode 100644
index 000000000..cd49040b2
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Result/UserfieldConfigsResult.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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+class UserfieldConfigsResult extends AbstractResult
+{
+ /**
+ * @return UserfieldConfigItemResult[]
+ * @throws BaseException
+ */
+ public function getUserfieldConfigs(): array
+ {
+ $items = [];
+ foreach ($this->getCoreResponse()->getResponseData()->getResult()['fields'] as $item) {
+ $items[] = new UserfieldConfigItemResult($item);
+ }
+
+ return $items;
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Service/Batch.php b/src/Services/CRM/Userfieldconfig/Service/Batch.php
new file mode 100644
index 000000000..df6e04a6b
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Service/Batch.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\Services\CRM\Userfieldconfig\Service;
+
+use Bitrix24\SDK\Attributes\ApiBatchMethodMetadata;
+use Bitrix24\SDK\Attributes\ApiBatchServiceMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\AddedUserfieldConfigItemBatchResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\DeletedUserfieldConfigItemBatchResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\UpdatedUserfieldConfigItemBatchResult;
+use Generator;
+use Psr\Log\LoggerInterface;
+
+#[ApiBatchServiceMetadata(new Scope(['crm', 'userfieldconfig']))]
+class Batch
+{
+ public function __construct(protected Userfieldconfig\Batch $batch, protected LoggerInterface $log)
+ {
+ }
+
+ /**
+ * Batch adding custom fields
+ *
+ * @param non-empty-string $moduleId
+ * @param array> $fields
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'userfieldconfig.add',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-add.html',
+ 'Batch adding custom fields'
+ )]
+ public function add(string $moduleId, array $fields): Generator
+ {
+ $items = [];
+ foreach ($fields as $field) {
+ $items[] = [
+ 'moduleId' => $moduleId,
+ 'field' => $field,
+ ];
+ }
+
+ foreach ($this->batch->addEntityItems('userfieldconfig.add', $items) as $key => $item) {
+ yield $key => new AddedUserfieldConfigItemBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch updating custom fields
+ *
+ * @param non-empty-string $moduleId
+ * @param array> $fields keyed by userfieldconfig id
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'userfieldconfig.update',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-update.html',
+ 'Batch updating custom fields'
+ )]
+ public function update(string $moduleId, array $fields): Generator
+ {
+ foreach ($this->batch->updateUserfieldConfigItems('userfieldconfig.update', $moduleId, $fields) as $key => $item) {
+ yield $key => new UpdatedUserfieldConfigItemBatchResult($item);
+ }
+ }
+
+ /**
+ * Batch deleting custom fields
+ *
+ * @param non-empty-string $moduleId
+ * @param positive-int[] $ids
+ *
+ * @return Generator
+ * @throws BaseException
+ */
+ #[ApiBatchMethodMetadata(
+ 'userfieldconfig.delete',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-delete.html',
+ 'Batch deleting custom fields'
+ )]
+ public function delete(string $moduleId, array $ids): Generator
+ {
+ foreach ($this->batch->deleteUserfieldConfigItems('userfieldconfig.delete', $moduleId, $ids) as $key => $item) {
+ yield $key => new DeletedUserfieldConfigItemBatchResult($item);
+ }
+ }
+}
diff --git a/src/Services/CRM/Userfieldconfig/Service/Userfieldconfig.php b/src/Services/CRM/Userfieldconfig/Service/Userfieldconfig.php
new file mode 100644
index 000000000..c34287bcc
--- /dev/null
+++ b/src/Services/CRM/Userfieldconfig/Service/Userfieldconfig.php
@@ -0,0 +1,196 @@
+
+ *
+ * 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\CRM\Userfieldconfig\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\Services\AbstractService;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\AddedUserfieldConfigItemResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\DeletedUserfieldConfigItemResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\UpdatedUserfieldConfigItemResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\UserfieldConfigResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\UserfieldConfigsResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\UserfieldConfigTypesResult;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['crm', 'userfieldconfig']))]
+class Userfieldconfig extends AbstractService
+{
+ public function __construct(public Batch $batch, CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Adds a new custom field for a CRM smart-process type, the new invoice or the sign document entity.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-add.html
+ *
+ * @param non-empty-string $moduleId
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'userfieldconfig.add',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-add.html',
+ 'Adds a new custom field for a CRM smart-process type, the new invoice or the sign document entity.'
+ )]
+ public function add(string $moduleId, array $field): AddedUserfieldConfigItemResult
+ {
+ return new AddedUserfieldConfigItemResult(
+ $this->core->call('userfieldconfig.add', [
+ 'moduleId' => $moduleId,
+ 'field' => $field,
+ ])
+ );
+ }
+
+ /**
+ * Updates the settings of an existing custom field.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-update.html
+ *
+ * @param non-empty-string $moduleId
+ * @param positive-int $id
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'userfieldconfig.update',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-update.html',
+ 'Updates the settings of an existing custom field.'
+ )]
+ public function update(string $moduleId, int $id, array $field): UpdatedUserfieldConfigItemResult
+ {
+ return new UpdatedUserfieldConfigItemResult(
+ $this->core->call('userfieldconfig.update', [
+ 'moduleId' => $moduleId,
+ 'id' => $id,
+ 'field' => $field,
+ ])
+ );
+ }
+
+ /**
+ * Returns the settings of a custom field by its identifier.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-get.html
+ *
+ * @param non-empty-string $moduleId
+ * @param positive-int $id
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'userfieldconfig.get',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-get.html',
+ 'Returns the settings of a custom field by its identifier.'
+ )]
+ public function get(string $moduleId, int $id): UserfieldConfigResult
+ {
+ return new UserfieldConfigResult(
+ $this->core->call('userfieldconfig.get', [
+ 'moduleId' => $moduleId,
+ 'id' => $id,
+ ])
+ );
+ }
+
+ /**
+ * Returns a list of custom field settings matching the filter.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-list.html
+ *
+ * @param non-empty-string $moduleId
+ * @param array $select defaults to `['*']` because the API returns only a minimal,
+ * largely useless field set (e.g. `{"1":"1","settings":null}`) when select is empty
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'userfieldconfig.list',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-list.html',
+ 'Returns a list of custom field settings matching the filter.'
+ )]
+ public function list(string $moduleId, array $select = ['*'], array $order = [], array $filter = [], int $start = 0): UserfieldConfigsResult
+ {
+ return new UserfieldConfigsResult(
+ $this->core->call('userfieldconfig.list', [
+ 'moduleId' => $moduleId,
+ 'select' => $select,
+ 'order' => $order,
+ 'filter' => $filter,
+ 'start' => $start,
+ ])
+ );
+ }
+
+ /**
+ * Deletes a custom field.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-delete.html
+ *
+ * @param non-empty-string $moduleId
+ * @param positive-int $id
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'userfieldconfig.delete',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-delete.html',
+ 'Deletes a custom field.'
+ )]
+ public function delete(string $moduleId, int $id): DeletedUserfieldConfigItemResult
+ {
+ return new DeletedUserfieldConfigItemResult(
+ $this->core->call('userfieldconfig.delete', [
+ 'moduleId' => $moduleId,
+ 'id' => $id,
+ ])
+ );
+ }
+
+ /**
+ * Returns the set of custom field types available for the specified module.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-get-types.html
+ *
+ * @param non-empty-string $moduleId
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'userfieldconfig.getTypes',
+ 'https://apidocs.bitrix24.com/api-reference/crm/universal/userfieldconfig/userfieldconfig-get-types.html',
+ 'Returns the set of custom field types available for the specified module.'
+ )]
+ public function getTypes(string $moduleId): UserfieldConfigTypesResult
+ {
+ return new UserfieldConfigTypesResult(
+ $this->core->call('userfieldconfig.getTypes', [
+ 'moduleId' => $moduleId,
+ ])
+ );
+ }
+}
diff --git a/src/Services/RemoteEventsFactory.php b/src/Services/RemoteEventsFactory.php
index 94ed4ddd1..bf6edc155 100644
--- a/src/Services/RemoteEventsFactory.php
+++ b/src/Services/RemoteEventsFactory.php
@@ -25,6 +25,7 @@
use Bitrix24\SDK\Services\CRM\Company\Events\CrmCompanyEventsFactory;
use Bitrix24\SDK\Services\CRM\Contact\Events\CrmContactEventsFactory;
use Bitrix24\SDK\Services\CRM\Documentgenerator\Document\Events\CrmDocumentGeneratorDocumentEventsFactory;
+use Bitrix24\SDK\Services\CRM\Type\Events\CrmTypeEventsFactory;
use Bitrix24\SDK\Services\SonetGroup\Events\SonetGroupEventsFactory;
use Bitrix24\SDK\Services\Telephony\Events\TelephonyEventsFactory;
use Bitrix24\SDK\Services\IMOpenLines\Connector\Events\ImConnectorEventsFactory;
@@ -227,6 +228,7 @@ public static function init(LoggerInterface $logger): self
new CalendarEventsFactory(),
new CrmCompanyEventsFactory(),
new CrmContactEventsFactory(),
+ new CrmTypeEventsFactory(),
new CrmDocumentGeneratorDocumentEventsFactory(),
new SonetGroupEventsFactory(),
new SaleEventsFactory(),
diff --git a/tests/Integration/Services/CRM/Type/Service/TypeTest.php b/tests/Integration/Services/CRM/Type/Service/TypeTest.php
index 4a444367a..97356747f 100644
--- a/tests/Integration/Services/CRM/Type/Service/TypeTest.php
+++ b/tests/Integration/Services/CRM/Type/Service/TypeTest.php
@@ -38,63 +38,63 @@ class TypeTest extends TestCase
public function testAdd(): void
{
$title = sprintf('%s test SPA type', time());
- $result = $this->typeService->add($title);
- $this->assertEquals($title, $result->type()->title);
- $this->assertTrue($this->typeService->delete($result->getId())->isSuccess());
+ $addedTypeItemResult = $this->typeService->add($title);
+ $this->assertEquals($title, $addedTypeItemResult->type()->title);
+ $this->assertTrue($this->typeService->delete($addedTypeItemResult->getId())->isSuccess());
}
public function testUpdate(): void
{
$title = sprintf('%s test SPA type', time());
- $result = $this->typeService->add($title);
- $this->assertEquals($title, $result->type()->title);
+ $addedTypeItemResult = $this->typeService->add($title);
+ $this->assertEquals($title, $addedTypeItemResult->type()->title);
$title = sprintf('%s updated SPA type', time());
- $updatedResult = $this->typeService->update($result->getId(), ['title' => $title]);
- $this->assertEquals($title, $updatedResult->type()->title);
+ $updatedTypeItemResult = $this->typeService->update($addedTypeItemResult->getId(), ['title' => $title]);
+ $this->assertEquals($title, $updatedTypeItemResult->type()->title);
- $this->assertTrue($this->typeService->delete($result->getId())->isSuccess());
+ $this->assertTrue($this->typeService->delete($addedTypeItemResult->getId())->isSuccess());
}
public function testGet(): void
{
$title = sprintf('%s test SPA type', time());
- $addResult = $this->typeService->add($title);
+ $addedTypeItemResult = $this->typeService->add($title);
- $result = $this->typeService->get($addResult->getId());
- $this->assertEquals($title, $addResult->type()->title);
- $this->assertEquals($result->type()->id, $addResult->type()->id);
- $this->assertTrue($this->typeService->delete($addResult->getId())->isSuccess());
+ $typeResult = $this->typeService->get($addedTypeItemResult->getId());
+ $this->assertEquals($title, $addedTypeItemResult->type()->title);
+ $this->assertEquals($typeResult->type()->id, $addedTypeItemResult->type()->id);
+ $this->assertTrue($this->typeService->delete($addedTypeItemResult->getId())->isSuccess());
}
public function testList(): void
{
$title = sprintf('%s test SPA type', time());
- $addResult = $this->typeService->add($title);
- $result = $this->typeService->list([], ['id' => $addResult->getId()])->getTypes()[0];
- $this->assertEquals($title, $addResult->type()->title);
- $this->assertEquals($result->id, $addResult->type()->id);
- $this->assertTrue($this->typeService->delete($addResult->getId())->isSuccess());
+ $addedTypeItemResult = $this->typeService->add($title);
+ $result = $this->typeService->list([], ['id' => $addedTypeItemResult->getId()])->getTypes()[0];
+ $this->assertEquals($title, $addedTypeItemResult->type()->title);
+ $this->assertEquals($result->id, $addedTypeItemResult->type()->id);
+ $this->assertTrue($this->typeService->delete($addedTypeItemResult->getId())->isSuccess());
}
public function testGetByEntityTypeId(): void
{
$title = sprintf('%s test SPA type', time());
- $result = $this->typeService->add($title);
- $this->assertEquals($title, $result->type()->title);
+ $addedTypeItemResult = $this->typeService->add($title);
+ $this->assertEquals($title, $addedTypeItemResult->type()->title);
- $resultTypeId = $this->typeService->getByEntityTypeId($result->type()->entityTypeId);
- $this->assertEquals($title, $resultTypeId->type()->title);
- $this->assertEquals($result->type()->id, $resultTypeId->type()->id);
- $this->assertTrue($this->typeService->delete($result->getId())->isSuccess());
+ $typeResult = $this->typeService->getByEntityTypeId($addedTypeItemResult->type()->entityTypeId);
+ $this->assertEquals($title, $typeResult->type()->title);
+ $this->assertEquals($addedTypeItemResult->type()->id, $typeResult->type()->id);
+ $this->assertTrue($this->typeService->delete($addedTypeItemResult->getId())->isSuccess());
}
public function testDelete(): void
{
$title = sprintf('%s test SPA type', time());
- $result = $this->typeService->add($title);
- $this->assertEquals($title, $result->type()->title);
- $this->assertTrue($this->typeService->delete($result->getId())->isSuccess());
+ $addedTypeItemResult = $this->typeService->add($title);
+ $this->assertEquals($title, $addedTypeItemResult->type()->title);
+ $this->assertTrue($this->typeService->delete($addedTypeItemResult->getId())->isSuccess());
}
protected function setUp(): void
diff --git a/tests/Integration/Services/CRM/Userfieldconfig/Result/UserfieldConfigItemResultTest.php b/tests/Integration/Services/CRM/Userfieldconfig/Result/UserfieldConfigItemResultTest.php
new file mode 100644
index 000000000..b506d2f35
--- /dev/null
+++ b/tests/Integration/Services/CRM/Userfieldconfig/Result/UserfieldConfigItemResultTest.php
@@ -0,0 +1,97 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Result;
+
+use Bitrix24\SDK\Services\CRM\Type\Service\Type;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Result\UserfieldConfigItemResult;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Service\Userfieldconfig;
+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(UserfieldConfigItemResult::class)]
+class UserfieldConfigItemResultTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Userfieldconfig $userfieldConfigService;
+
+ private Type $typeService;
+
+ private int $scratchTypeId;
+
+ private int $scratchFieldId;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->userfieldConfigService = Fabric::getServiceBuilder()->getCRMScope()->userfieldConfig();
+ $this->typeService = Fabric::getServiceBuilder()->getCRMScope()->type();
+
+ // entityId for userfieldconfig is CRM_{id} built from the SPA type's own `id`,
+ // not from `entityTypeId` (see docs for onCrmTypeUserFieldAdd, section "ENTITY_ID")
+ $addedTypeItemResult = $this->typeService->add(sprintf('%s userfieldconfig annotations test SPA type', time()));
+ $this->scratchTypeId = $addedTypeItemResult->getId();
+ $entityId = sprintf('CRM_%d', $this->scratchTypeId);
+
+ // uses userTypeId "enumeration" with a non-empty `enum` list so that the raw API
+ // response includes the `enum` key, matching the class annotation below
+ $addedUserfieldConfigItemResult = $this->userfieldConfigService->add('crm', [
+ 'entityId' => $entityId,
+ 'fieldName' => sprintf('UF_%s_%d', $entityId, time()),
+ 'userTypeId' => 'enumeration',
+ 'enum' => [
+ ['value' => 'A', 'def' => 'N', 'sort' => 100],
+ ['value' => 'B', 'def' => 'Y', 'sort' => 200],
+ ],
+ ]);
+ $this->scratchFieldId = (int)$addedUserfieldConfigItemResult->field()->id;
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // deleting the SPA type also removes its custom fields
+ $this->typeService->delete($this->scratchTypeId);
+ }
+
+ #[Test]
+ #[TestDox('all fields in UserfieldConfigItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllFieldsAreAnnotated(): void
+ {
+ $rawItem = $this->userfieldConfigService->get('crm', $this->scratchFieldId)
+ ->getCoreResponse()
+ ->getResponseData()
+ ->getResult()['field'];
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawItem),
+ UserfieldConfigItemResult::class
+ );
+ }
+
+ #[Test]
+ #[TestDox('all fields in UserfieldConfigItemResult have valid type casting in magic getters')]
+ public function testAllFieldsHasValidTypeCastingInMagicGetters(): void
+ {
+ $item = $this->userfieldConfigService->get('crm', $this->scratchFieldId)->field();
+ $this->assertBitrix24ResultItemFieldsTypeCastMatchAnnotations(
+ $item,
+ UserfieldConfigItemResult::class
+ );
+ }
+}
diff --git a/tests/Integration/Services/CRM/Userfieldconfig/Service/BatchTest.php b/tests/Integration/Services/CRM/Userfieldconfig/Service/BatchTest.php
new file mode 100644
index 000000000..51bf2a6c0
--- /dev/null
+++ b/tests/Integration/Services/CRM/Userfieldconfig/Service/BatchTest.php
@@ -0,0 +1,148 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Service;
+
+use Bitrix24\SDK\Services\CRM\Type\Service\Type;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Service\Batch;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Service\Userfieldconfig;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Batch::class)]
+class BatchTest extends TestCase
+{
+ public const FIELD_LETTERS = ['A', 'B', 'C'];
+
+ private Userfieldconfig $userfieldConfigService;
+
+ private Type $typeService;
+
+ private int $scratchTypeId;
+
+ private string $entityId;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->userfieldConfigService = Fabric::getServiceBuilder()->getCRMScope()->userfieldConfig();
+ $this->typeService = Fabric::getServiceBuilder()->getCRMScope()->type();
+
+ // entityId for userfieldconfig is CRM_{id} built from the SPA type's own `id`,
+ // not from `entityTypeId` (see docs for onCrmTypeUserFieldAdd, section "ENTITY_ID")
+ $addedTypeItemResult = $this->typeService->add(sprintf('%s userfieldconfig batch test SPA type', time()));
+ $this->scratchTypeId = $addedTypeItemResult->getId();
+ $this->entityId = sprintf('CRM_%d', $this->scratchTypeId);
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // deleting the SPA type also removes its custom fields
+ $this->typeService->delete($this->scratchTypeId);
+ }
+
+ public function testBatchAdd(): void
+ {
+ $fieldNames = $this->buildFieldNames();
+ $items = [];
+ foreach ($fieldNames as $fieldName) {
+ $items[] = [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ];
+ }
+
+ $cnt = 0;
+ $addedFieldNames = [];
+ foreach ($this->userfieldConfigService->batch->add('crm', $items) as $result) {
+ $addedFieldNames[] = $result->field()->fieldName;
+ $cnt++;
+ }
+
+ $this->assertEquals(count($items), $cnt);
+ $this->assertEqualsCanonicalizing($fieldNames, $addedFieldNames);
+ }
+
+ public function testBatchUpdate(): void
+ {
+ $fieldNames = $this->buildFieldNames();
+ $itemsToAdd = [];
+ foreach ($fieldNames as $fieldName) {
+ $itemsToAdd[] = [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ];
+ }
+
+ $ids = [];
+ foreach ($this->userfieldConfigService->batch->add('crm', $itemsToAdd) as $result) {
+ $ids[] = $result->getId();
+ }
+
+ $itemsToUpdate = [];
+ foreach ($ids as $id) {
+ $itemsToUpdate[$id] = ['mandatory' => 'Y'];
+ }
+
+ $cnt = 0;
+ foreach ($this->userfieldConfigService->batch->update('crm', $itemsToUpdate) as $updateResult) {
+ $this->assertTrue($updateResult->field()->mandatory);
+ $cnt++;
+ }
+
+ $this->assertEquals(count($ids), $cnt);
+ }
+
+ public function testBatchDelete(): void
+ {
+ $fieldNames = $this->buildFieldNames();
+ $itemsToAdd = [];
+ foreach ($fieldNames as $fieldName) {
+ $itemsToAdd[] = [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ];
+ }
+
+ $ids = [];
+ foreach ($this->userfieldConfigService->batch->add('crm', $itemsToAdd) as $result) {
+ $ids[] = $result->getId();
+ }
+
+ $cnt = 0;
+ foreach ($this->userfieldConfigService->batch->delete('crm', $ids) as $deleteResult) {
+ $this->assertTrue($deleteResult->isSuccess());
+ $cnt++;
+ }
+
+ $this->assertEquals(count($ids), $cnt);
+ }
+
+ /**
+ * @return non-empty-string[]
+ */
+ private function buildFieldNames(): array
+ {
+ $fieldNames = [];
+ foreach (self::FIELD_LETTERS as $letter) {
+ $fieldNames[] = sprintf('UF_%s_%d%s', $this->entityId, time(), $letter);
+ }
+
+ return $fieldNames;
+ }
+}
diff --git a/tests/Integration/Services/CRM/Userfieldconfig/Service/UserfieldconfigTest.php b/tests/Integration/Services/CRM/Userfieldconfig/Service/UserfieldconfigTest.php
new file mode 100644
index 000000000..b7237ebab
--- /dev/null
+++ b/tests/Integration/Services/CRM/Userfieldconfig/Service/UserfieldconfigTest.php
@@ -0,0 +1,129 @@
+
+ *
+ * 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\CRM\Userfieldconfig\Service;
+
+use Bitrix24\SDK\Services\CRM\Type\Service\Type;
+use Bitrix24\SDK\Services\CRM\Userfieldconfig\Service\Userfieldconfig;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Userfieldconfig::class)]
+class UserfieldconfigTest extends TestCase
+{
+ private Userfieldconfig $userfieldConfigService;
+
+ private Type $typeService;
+
+ private int $scratchTypeId;
+
+ private string $entityId;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->userfieldConfigService = Fabric::getServiceBuilder()->getCRMScope()->userfieldConfig();
+ $this->typeService = Fabric::getServiceBuilder()->getCRMScope()->type();
+
+ // entityId for userfieldconfig is CRM_{id} built from the SPA type's own `id`,
+ // not from `entityTypeId` (see docs for onCrmTypeUserFieldAdd, section "ENTITY_ID")
+ $addedTypeItemResult = $this->typeService->add(sprintf('%s userfieldconfig test SPA type', time()));
+ $this->scratchTypeId = $addedTypeItemResult->getId();
+ $this->entityId = sprintf('CRM_%d', $this->scratchTypeId);
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // deleting the SPA type also removes its custom fields
+ $this->typeService->delete($this->scratchTypeId);
+ }
+
+ public function testAdd(): void
+ {
+ $fieldName = sprintf('UF_%s_%d', $this->entityId, time());
+ $addedUserfieldConfigItemResult = $this->userfieldConfigService->add('crm', [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ]);
+
+ $this->assertEquals($fieldName, $addedUserfieldConfigItemResult->field()->fieldName);
+ $this->assertEquals($this->entityId, $addedUserfieldConfigItemResult->field()->entityId);
+ $this->assertEquals('string', $addedUserfieldConfigItemResult->field()->userTypeId);
+ }
+
+ public function testUpdate(): void
+ {
+ $fieldName = sprintf('UF_%s_%d', $this->entityId, time());
+ $addedUserfieldConfigItemResult = $this->userfieldConfigService->add('crm', [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ]);
+
+ $updatedUserfieldConfigItemResult = $this->userfieldConfigService->update('crm', (int)$addedUserfieldConfigItemResult->field()->id, ['mandatory' => 'Y']);
+ $this->assertTrue($updatedUserfieldConfigItemResult->field()->mandatory);
+ $this->assertEquals($addedUserfieldConfigItemResult->field()->id, $updatedUserfieldConfigItemResult->field()->id);
+ }
+
+ public function testGet(): void
+ {
+ $fieldName = sprintf('UF_%s_%d', $this->entityId, time());
+ $addedUserfieldConfigItemResult = $this->userfieldConfigService->add('crm', [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ]);
+
+ $userfieldConfigResult = $this->userfieldConfigService->get('crm', (int)$addedUserfieldConfigItemResult->field()->id);
+ $this->assertEquals($fieldName, $userfieldConfigResult->field()?->fieldName);
+ $this->assertEquals($addedUserfieldConfigItemResult->field()->id, $userfieldConfigResult->field()?->id);
+ }
+
+ public function testList(): void
+ {
+ $fieldName = sprintf('UF_%s_%d', $this->entityId, time());
+ $addedUserfieldConfigItemResult = $this->userfieldConfigService->add('crm', [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ]);
+
+ $items = $this->userfieldConfigService->list('crm', ['*'], [], ['entityId' => $this->entityId])->getUserfieldConfigs();
+ $this->assertNotEmpty($items);
+ $this->assertEquals($fieldName, $items[0]->fieldName);
+ $this->assertEquals($addedUserfieldConfigItemResult->field()->id, $items[0]->id);
+ }
+
+ public function testDelete(): void
+ {
+ $fieldName = sprintf('UF_%s_%d', $this->entityId, time());
+ $addedUserfieldConfigItemResult = $this->userfieldConfigService->add('crm', [
+ 'entityId' => $this->entityId,
+ 'fieldName' => $fieldName,
+ 'userTypeId' => 'string',
+ ]);
+
+ $this->assertTrue($this->userfieldConfigService->delete('crm', (int)$addedUserfieldConfigItemResult->field()->id)->isSuccess());
+ }
+
+ public function testGetTypes(): void
+ {
+ $types = $this->userfieldConfigService->getTypes('crm')->getTypes();
+ $this->assertArrayHasKey('string', $types);
+ $this->assertEquals('string', $types['string']->userTypeId);
+ $this->assertNotEmpty($types['string']->description);
+ }
+}
diff --git a/tests/Unit/Services/CRM/Type/Events/CrmTypeEventsFactoryTest.php b/tests/Unit/Services/CRM/Type/Events/CrmTypeEventsFactoryTest.php
new file mode 100644
index 000000000..43680db7c
--- /dev/null
+++ b/tests/Unit/Services/CRM/Type/Events/CrmTypeEventsFactoryTest.php
@@ -0,0 +1,151 @@
+factory = new CrmTypeEventsFactory();
+ }
+
+ #[Test]
+ #[TestDox('isSupport() recognizes all onCrmType user field event codes')]
+ #[DataProvider('supportedEventCodeProvider')]
+ public function testIsSupport(string $eventCode, string $expectedClass): void
+ {
+ self::assertNotEmpty($expectedClass);
+ self::assertTrue($this->factory->isSupport($eventCode));
+ self::assertTrue($this->factory->isSupport(strtolower($eventCode)));
+ }
+
+ #[Test]
+ #[TestDox('isSupport() rejects unrelated event codes')]
+ public function testIsSupportRejectsUnknownCode(): void
+ {
+ self::assertFalse($this->factory->isSupport('ONCRMCOMPANYUSERFIELDADD'));
+ }
+
+ #[Test]
+ #[TestDox('create() builds OnCrmTypeUserFieldAdd and exposes payload fields')]
+ public function testCreateBuildsOnCrmTypeUserFieldAdd(): void
+ {
+ $event = $this->factory->create($this->createRequest(OnCrmTypeUserFieldAdd::CODE));
+
+ self::assertInstanceOf(OnCrmTypeUserFieldAdd::class, $event);
+ self::assertSame(OnCrmTypeUserFieldAdd::CODE, $event->getEventCode());
+
+ $onCrmTypeUserFieldAddPayload = $event->getPayload();
+ self::assertSame('6977', $onCrmTypeUserFieldAddPayload->id);
+ self::assertSame('CRM_13', $onCrmTypeUserFieldAddPayload->entityId);
+ self::assertSame('UF_CRM_13_1742999523', $onCrmTypeUserFieldAddPayload->fieldName);
+ }
+
+ #[Test]
+ #[TestDox('create() builds OnCrmTypeUserFieldUpdate and exposes payload fields')]
+ public function testCreateBuildsOnCrmTypeUserFieldUpdate(): void
+ {
+ $event = $this->factory->create($this->createRequest(OnCrmTypeUserFieldUpdate::CODE));
+
+ self::assertInstanceOf(OnCrmTypeUserFieldUpdate::class, $event);
+ self::assertSame(OnCrmTypeUserFieldUpdate::CODE, $event->getEventCode());
+
+ $onCrmTypeUserFieldUpdatePayload = $event->getPayload();
+ self::assertSame('6977', $onCrmTypeUserFieldUpdatePayload->id);
+ self::assertSame('CRM_13', $onCrmTypeUserFieldUpdatePayload->entityId);
+ self::assertSame('UF_CRM_13_1742999523', $onCrmTypeUserFieldUpdatePayload->fieldName);
+ }
+
+ #[Test]
+ #[TestDox('create() builds OnCrmTypeUserFieldDelete and exposes payload fields')]
+ public function testCreateBuildsOnCrmTypeUserFieldDelete(): void
+ {
+ $event = $this->factory->create($this->createRequest(OnCrmTypeUserFieldDelete::CODE));
+
+ self::assertInstanceOf(OnCrmTypeUserFieldDelete::class, $event);
+ self::assertSame(OnCrmTypeUserFieldDelete::CODE, $event->getEventCode());
+
+ $onCrmTypeUserFieldDeletePayload = $event->getPayload();
+ self::assertSame('6977', $onCrmTypeUserFieldDeletePayload->id);
+ self::assertSame('CRM_13', $onCrmTypeUserFieldDeletePayload->entityId);
+ self::assertSame('UF_CRM_13_1742999523', $onCrmTypeUserFieldDeletePayload->fieldName);
+ }
+
+ #[Test]
+ #[TestDox('create() builds OnCrmTypeUserFieldSetEnumValues and exposes payload fields')]
+ public function testCreateBuildsOnCrmTypeUserFieldSetEnumValues(): void
+ {
+ $event = $this->factory->create($this->createRequest(OnCrmTypeUserFieldSetEnumValues::CODE));
+
+ self::assertInstanceOf(OnCrmTypeUserFieldSetEnumValues::class, $event);
+ self::assertSame(OnCrmTypeUserFieldSetEnumValues::CODE, $event->getEventCode());
+
+ $onCrmTypeUserFieldSetEnumValuesPayload = $event->getPayload();
+ self::assertSame('6977', $onCrmTypeUserFieldSetEnumValuesPayload->id);
+ self::assertSame('CRM_13', $onCrmTypeUserFieldSetEnumValuesPayload->entityId);
+ self::assertSame('UF_CRM_13_1742999523', $onCrmTypeUserFieldSetEnumValuesPayload->fieldName);
+ }
+
+ #[Test]
+ #[TestDox('create() throws for an event code it does not support')]
+ public function testCreateThrowsForUnsupportedEvent(): void
+ {
+ $request = $this->createRequest('ONCRMCOMPANYUSERFIELDADD');
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->factory->create($request);
+ }
+
+ public static function supportedEventCodeProvider(): Generator
+ {
+ yield 'add' => [OnCrmTypeUserFieldAdd::CODE, OnCrmTypeUserFieldAdd::class];
+ yield 'update' => [OnCrmTypeUserFieldUpdate::CODE, OnCrmTypeUserFieldUpdate::class];
+ yield 'delete' => [OnCrmTypeUserFieldDelete::CODE, OnCrmTypeUserFieldDelete::class];
+ yield 'setEnumValues' => [OnCrmTypeUserFieldSetEnumValues::CODE, OnCrmTypeUserFieldSetEnumValues::class];
+ }
+
+ private function createRequest(string $eventCode): Request
+ {
+ $payload = [
+ 'event' => $eventCode,
+ 'event_handler_id' => '713',
+ 'data' => [
+ 'id' => '6977',
+ 'entityId' => 'CRM_13',
+ 'fieldName' => 'UF_CRM_13_1742999523',
+ ],
+ 'ts' => '1742999523',
+ 'auth' => [
+ 'application_token' => 'test_app_token',
+ ],
+ ];
+
+ $rawRequest = http_build_query($payload);
+ parse_str($rawRequest, $requestContent);
+
+ $request = new Request([], $requestContent, [], [], [], [], $rawRequest);
+ $request->setMethod('POST');
+
+ return $request;
+ }
+}
diff --git a/tests/Unit/Services/CRM/Userfieldconfig/Service/UserfieldconfigTest.php b/tests/Unit/Services/CRM/Userfieldconfig/Service/UserfieldconfigTest.php
new file mode 100644
index 000000000..ddf1856bd
--- /dev/null
+++ b/tests/Unit/Services/CRM/Userfieldconfig/Service/UserfieldconfigTest.php
@@ -0,0 +1,130 @@
+mockCore('userfieldconfig.add', [
+ 'moduleId' => 'crm',
+ 'field' => ['entityId' => 'CRM_7', 'fieldName' => 'UF_CRM_7_TEST', 'userTypeId' => 'string'],
+ ]);
+
+ self::assertInstanceOf(
+ AddedUserfieldConfigItemResult::class,
+ $this->makeService($core)->add('crm', [
+ 'entityId' => 'CRM_7',
+ 'fieldName' => 'UF_CRM_7_TEST',
+ 'userTypeId' => 'string',
+ ])
+ );
+ }
+
+ public function testUpdateBuildsParameters(): void
+ {
+ $core = $this->mockCore('userfieldconfig.update', [
+ 'moduleId' => 'crm',
+ 'id' => 7095,
+ 'field' => ['mandatory' => 'Y'],
+ ]);
+
+ self::assertInstanceOf(
+ UpdatedUserfieldConfigItemResult::class,
+ $this->makeService($core)->update('crm', 7095, ['mandatory' => 'Y'])
+ );
+ }
+
+ public function testGetBuildsParameters(): void
+ {
+ $core = $this->mockCore('userfieldconfig.get', [
+ 'moduleId' => 'crm',
+ 'id' => 7095,
+ ]);
+
+ self::assertInstanceOf(UserfieldConfigResult::class, $this->makeService($core)->get('crm', 7095));
+ }
+
+ public function testListBuildsParameters(): void
+ {
+ $core = $this->mockCore('userfieldconfig.list', [
+ 'moduleId' => 'crm',
+ 'select' => ['*'],
+ 'order' => ['id' => 'DESC'],
+ 'filter' => ['multiple' => 'Y'],
+ 'start' => 0,
+ ]);
+
+ self::assertInstanceOf(
+ UserfieldConfigsResult::class,
+ $this->makeService($core)->list('crm', ['*'], ['id' => 'DESC'], ['multiple' => 'Y'])
+ );
+ }
+
+ public function testListDefaultsToWildcardSelect(): void
+ {
+ $core = $this->mockCore('userfieldconfig.list', [
+ 'moduleId' => 'crm',
+ 'select' => ['*'],
+ 'order' => [],
+ 'filter' => [],
+ 'start' => 0,
+ ]);
+
+ self::assertInstanceOf(UserfieldConfigsResult::class, $this->makeService($core)->list('crm'));
+ }
+
+ public function testDeleteBuildsParameters(): void
+ {
+ $core = $this->mockCore('userfieldconfig.delete', [
+ 'moduleId' => 'crm',
+ 'id' => 7095,
+ ]);
+
+ self::assertInstanceOf(DeletedUserfieldConfigItemResult::class, $this->makeService($core)->delete('crm', 7095));
+ }
+
+ public function testGetTypesBuildsParameters(): void
+ {
+ $core = $this->mockCore('userfieldconfig.getTypes', [
+ 'moduleId' => 'crm',
+ ]);
+
+ self::assertInstanceOf(UserfieldConfigTypesResult::class, $this->makeService($core)->getTypes('crm'));
+ }
+
+ private function makeService(CoreInterface $core): Userfieldconfig
+ {
+ return new Userfieldconfig(new Batch(new UserfieldconfigBatch($core, new NullLogger()), new NullLogger()), $core, new NullLogger());
+ }
+
+ private function mockCore(string $method, array $parameters): CoreInterface
+ {
+ $response = $this->createStub(Response::class);
+ $core = $this->createMock(CoreInterface::class);
+ $core->expects($this->once())
+ ->method('call')
+ ->with($method, $parameters)
+ ->willReturn($response);
+
+ return $core;
+ }
+}