diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f141291..3e06feb3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@
### Added
+- Added `Services\Timeman` service with support for workday tracking methods,
+ see [timeman.* methods](https://apidocs.bitrix24.com/api-reference/timeman/index.html):
+ - `open` — starts a new workday or continues after pause/close
+ - `pause` — pauses the current workday
+ - `close` — closes the current workday
+ - `status` — gets current workday status
+ - `settings` — gets user's work time settings
+ ([#484](https://github.com/bitrix24/b24phpsdk/issues/484))
- 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..4bed68b1 100644
--- a/Makefile
+++ b/Makefile
@@ -445,6 +445,10 @@ 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-timeman
+test-integration-scope-timeman:
+ docker compose run --rm php-cli vendor/bin/phpunit --testsuite integration_tests_scope_timeman
+
# work dev environment
.PHONY: php-dev-server-up
php-dev-server-up:
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index bb074ed2..2e4576d5 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -36,6 +36,7 @@ parameters:
- tests/Integration/Services/CRM/Documentgenerator/Numerator
- tests/Integration/Services/CRM/Documentgenerator/Document
- tests/Integration/Services/CRM/Documentgenerator/Template
+ - tests/Integration/Services/Timeman
excludePaths:
- tests/Integration/Services/CRM/Requisites/Service/RequisiteUserfieldUseCaseTest.php
- tests/Integration/Services/CRM/Status
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index e9c12ee3..aac5bbca 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -232,6 +232,9 @@
./tests/Integration/Services/SonetGroup/
+
+ ./tests/Integration/Services/Timeman/
+
diff --git a/rector.php b/rector.php
index c35b39eb..5d19d5a9 100644
--- a/rector.php
+++ b/rector.php
@@ -77,6 +77,8 @@
__DIR__ . '/src/Services/CRM/Documentgenerator/Template',
__DIR__ . '/tests/Integration/Services/CRM/Documentgenerator/Template',
__DIR__ . '/tests/Unit/',
+ __DIR__ . '/src/Services/Timeman',
+ __DIR__ . '/tests/Integration/Services/Timeman',
])
->withCache(cacheDirectory: __DIR__ . '/var/.cache/rector')
->withSets(
diff --git a/src/Services/ServiceBuilder.php b/src/Services/ServiceBuilder.php
index e76b5e06..c4d01a1b 100644
--- a/src/Services/ServiceBuilder.php
+++ b/src/Services/ServiceBuilder.php
@@ -36,6 +36,7 @@
use Bitrix24\SDK\Services\Calendar\CalendarServiceBuilder;
use Bitrix24\SDK\Services\Paysystem\PaysystemServiceBuilder;
use Bitrix24\SDK\Services\SonetGroup\SonetGroupServiceBuilder;
+use Bitrix24\SDK\Services\Timeman\TimemanServiceBuilder;
use Psr\Log\LoggerInterface;
class ServiceBuilder extends AbstractServiceBuilder
@@ -334,5 +335,18 @@ public function getSonetGroupScope(): SonetGroupServiceBuilder
return $this->serviceCache[__METHOD__];
}
-
+
+ public function getTimemanScope(): TimemanServiceBuilder
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new TimemanServiceBuilder(
+ $this->core,
+ $this->batch,
+ $this->bulkItemsReader,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
}
diff --git a/src/Services/Timeman/Result/TimemanSettingsItemResult.php b/src/Services/Timeman/Result/TimemanSettingsItemResult.php
new file mode 100644
index 00000000..3499bc62
--- /dev/null
+++ b/src/Services/Timeman/Result/TimemanSettingsItemResult.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\Timeman\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+
+/**
+ * Represents a single settings item returned by timeman.settings method.
+ *
+ * @property-read bool $UF_TIMEMAN
+ * @property-read bool $UF_TM_FREE
+ * @property-read string $UF_TM_MAX_START
+ * @property-read string $UF_TM_MIN_FINISH
+ * @property-read string $UF_TM_MIN_DURATION
+ * @property-read string $UF_TM_ALLOWED_DELTA
+ * @property-read bool|null $ADMIN
+ */
+class TimemanSettingsItemResult extends AbstractItem
+{
+}
+
diff --git a/src/Services/Timeman/Result/TimemanSettingsResult.php b/src/Services/Timeman/Result/TimemanSettingsResult.php
new file mode 100644
index 00000000..01fd323a
--- /dev/null
+++ b/src/Services/Timeman/Result/TimemanSettingsResult.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\Timeman\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result wrapping the settings object returned by timeman.settings.
+ */
+class TimemanSettingsResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function getSettings(): TimemanSettingsItemResult
+ {
+ return new TimemanSettingsItemResult($this->getCoreResponse()->getResponseData()->getResult());
+ }
+}
+
diff --git a/src/Services/Timeman/Result/WorkdayItemResult.php b/src/Services/Timeman/Result/WorkdayItemResult.php
new file mode 100644
index 00000000..a4444243
--- /dev/null
+++ b/src/Services/Timeman/Result/WorkdayItemResult.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\Timeman\Result;
+
+use Bitrix24\SDK\Core\Result\AbstractItem;
+use Carbon\CarbonImmutable;
+
+/**
+ * Represents a single workday item returned by timeman.open, timeman.pause, timeman.close, timeman.status methods.
+ *
+ * @property-read string $STATUS
+ * @property-read CarbonImmutable|null $TIME_START
+ * @property-read CarbonImmutable|null $TIME_FINISH
+ * @property-read string $DURATION
+ * @property-read string $TIME_LEAKS
+ * @property-read bool $ACTIVE
+ * @property-read string $IP_OPEN
+ * @property-read string|null $IP_CLOSE
+ * @property-read float $LAT_OPEN
+ * @property-read float $LON_OPEN
+ * @property-read float $LAT_CLOSE
+ * @property-read float $LON_CLOSE
+ * @property-read int $TZ_OFFSET
+ */
+class WorkdayItemResult extends AbstractItem
+{
+ public function __get($offset)
+ {
+ return match ($offset) {
+ 'TIME_START', 'TIME_FINISH' => isset($this->data[$offset])
+ ? CarbonImmutable::createFromFormat(DATE_ATOM, $this->data[$offset])
+ : null,
+ 'TZ_OFFSET' => isset($this->data[$offset]) ? (int)$this->data[$offset] : null,
+ 'LAT_OPEN', 'LON_OPEN', 'LAT_CLOSE', 'LON_CLOSE' => isset($this->data[$offset]) ? (float)$this->data[$offset] : null,
+ default => $this->data[$offset] ?? null,
+ };
+ }
+}
diff --git a/src/Services/Timeman/Result/WorkdayResult.php b/src/Services/Timeman/Result/WorkdayResult.php
new file mode 100644
index 00000000..25e2e221
--- /dev/null
+++ b/src/Services/Timeman/Result/WorkdayResult.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\Timeman\Result;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Result\AbstractResult;
+
+/**
+ * Result wrapping a single workday object returned by timeman.open, timeman.pause, timeman.close, timeman.status.
+ */
+class WorkdayResult extends AbstractResult
+{
+ /**
+ * @throws BaseException
+ */
+ public function getWorkday(): WorkdayItemResult
+ {
+ return new WorkdayItemResult($this->getCoreResponse()->getResponseData()->getResult());
+ }
+}
+
diff --git a/src/Services/Timeman/Service/Timeman.php b/src/Services/Timeman/Service/Timeman.php
new file mode 100644
index 00000000..264b2f69
--- /dev/null
+++ b/src/Services/Timeman/Service/Timeman.php
@@ -0,0 +1,193 @@
+
+ *
+ * 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\Timeman\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\Timeman\Result\TimemanSettingsResult;
+use Bitrix24\SDK\Services\Timeman\Result\WorkdayResult;
+use Carbon\CarbonImmutable;
+use Psr\Log\LoggerInterface;
+
+#[ApiServiceMetadata(new Scope(['timeman']))]
+class Timeman extends AbstractService
+{
+ public function __construct(CoreInterface $core, LoggerInterface $logger)
+ {
+ parent::__construct($core, $logger);
+ }
+
+ /**
+ * Opens a new workday or continues a workday after a pause or completion.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-open.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'timeman.open',
+ 'https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-open.html',
+ 'Opens a new workday or continues a workday after a pause or completion.'
+ )]
+ public function open(
+ ?int $userId = null,
+ ?CarbonImmutable $time = null,
+ ?string $report = null,
+ ?float $lat = null,
+ ?float $lon = null
+ ): WorkdayResult {
+ $params = [];
+ if ($userId !== null) {
+ $params['USER_ID'] = $userId;
+ }
+
+ if ($time instanceof CarbonImmutable) {
+ $params['TIME'] = $time->format(CarbonImmutable::ATOM);
+ }
+
+ if ($report !== null) {
+ $params['REPORT'] = $report;
+ }
+
+ if ($lat !== null) {
+ $params['LAT'] = $lat;
+ }
+
+ if ($lon !== null) {
+ $params['LON'] = $lon;
+ }
+
+ return new WorkdayResult($this->core->call('timeman.open', $params));
+ }
+
+ /**
+ * Pauses the current workday.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-pause.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'timeman.pause',
+ 'https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-pause.html',
+ 'Pauses the current workday.'
+ )]
+ public function pause(?int $userId = null): WorkdayResult
+ {
+ $params = [];
+ if ($userId !== null) {
+ $params['USER_ID'] = $userId;
+ }
+
+ return new WorkdayResult($this->core->call('timeman.pause', $params));
+ }
+
+ /**
+ * Closes the current workday.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-close.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'timeman.close',
+ 'https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-close.html',
+ 'Closes the current workday.'
+ )]
+ public function close(
+ ?int $userId = null,
+ ?CarbonImmutable $time = null,
+ ?string $report = null,
+ ?float $lat = null,
+ ?float $lon = null
+ ): WorkdayResult {
+ $params = [];
+ if ($userId !== null) {
+ $params['USER_ID'] = $userId;
+ }
+
+ if ($time instanceof CarbonImmutable) {
+ $params['TIME'] = $time->format(CarbonImmutable::ATOM);
+ }
+
+ if ($report !== null) {
+ $params['REPORT'] = $report;
+ }
+
+ if ($lat !== null) {
+ $params['LAT'] = $lat;
+ }
+
+ if ($lon !== null) {
+ $params['LON'] = $lon;
+ }
+
+ return new WorkdayResult($this->core->call('timeman.close', $params));
+ }
+
+ /**
+ * Gets information about the current workday of the user.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-status.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'timeman.status',
+ 'https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-status.html',
+ 'Gets information about the current workday of the user.'
+ )]
+ public function status(?int $userId = null): WorkdayResult
+ {
+ $params = [];
+ if ($userId !== null) {
+ $params['USER_ID'] = $userId;
+ }
+
+ return new WorkdayResult($this->core->call('timeman.status', $params));
+ }
+
+ /**
+ * Gets the work time settings of the user.
+ *
+ * @link https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-settings.html
+ *
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[ApiEndpointMetadata(
+ 'timeman.settings',
+ 'https://apidocs.bitrix24.com/api-reference/timeman/base/timeman-settings.html',
+ 'Gets the work time settings of the user.'
+ )]
+ public function settings(?int $userId = null): TimemanSettingsResult
+ {
+ $params = [];
+ if ($userId !== null) {
+ $params['USER_ID'] = $userId;
+ }
+
+ return new TimemanSettingsResult($this->core->call('timeman.settings', $params));
+ }
+}
+
diff --git a/src/Services/Timeman/TimemanServiceBuilder.php b/src/Services/Timeman/TimemanServiceBuilder.php
new file mode 100644
index 00000000..b7b2b1fd
--- /dev/null
+++ b/src/Services/Timeman/TimemanServiceBuilder.php
@@ -0,0 +1,36 @@
+
+ *
+ * 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\Timeman;
+
+use Bitrix24\SDK\Attributes\ApiServiceBuilderMetadata;
+use Bitrix24\SDK\Core\Credentials\Scope;
+use Bitrix24\SDK\Services\AbstractServiceBuilder;
+use Bitrix24\SDK\Services\Timeman\Service\Timeman;
+
+#[ApiServiceBuilderMetadata(new Scope(['timeman']))]
+class TimemanServiceBuilder extends AbstractServiceBuilder
+{
+ public function timeman(): Timeman
+ {
+ if (!isset($this->serviceCache[__METHOD__])) {
+ $this->serviceCache[__METHOD__] = new Timeman(
+ $this->core,
+ $this->log
+ );
+ }
+
+ return $this->serviceCache[__METHOD__];
+ }
+}
+
diff --git a/tests/Integration/Services/Timeman/Result/TimemanSettingsItemResultAnnotationsTest.php b/tests/Integration/Services/Timeman/Result/TimemanSettingsItemResultAnnotationsTest.php
new file mode 100644
index 00000000..2522432c
--- /dev/null
+++ b/tests/Integration/Services/Timeman/Result/TimemanSettingsItemResultAnnotationsTest.php
@@ -0,0 +1,100 @@
+
+ *
+ * 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\Timeman\Result;
+
+use Bitrix24\SDK\Services\Timeman\Result\TimemanSettingsItemResult;
+use Bitrix24\SDK\Services\Timeman\Service\Timeman;
+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;
+use Typhoon\Reflection\TyphoonReflector;
+use function Typhoon\Type\stringify;
+
+#[CoversClass(TimemanSettingsItemResult::class)]
+class TimemanSettingsItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Timeman $timemanService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->timemanService = Fabric::getServiceBuilder()->getTimemanScope()->timeman();
+ }
+
+ #[Test]
+ #[TestDox('all fields in TimemanSettingsItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->timemanService->settings()->getCoreResponse()
+ ->getResponseData()->getResult();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawResult),
+ TimemanSettingsItemResult::class
+ );
+ }
+
+ #[Test]
+ #[TestDox('all fields in TimemanSettingsItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $timemanSettingsItemResult = $this->timemanService->settings()->getSettings();
+
+ $collection = TyphoonReflector::build()
+ ->reflectClass(TimemanSettingsItemResult::class)
+ ->properties();
+
+ foreach ($collection as $meta) {
+ if (!$meta->isAnnotated()) {
+ continue;
+ }
+
+ if ($meta->isNative()) {
+ continue;
+ }
+
+ $propName = $meta->id->name;
+ $typeStr = stringify($meta->type());
+ $value = $timemanSettingsItemResult->$propName;
+
+ // null is always valid for nullable types
+ if (str_contains($typeStr, 'null') && $value === null) {
+ continue;
+ }
+
+ $message = sprintf(
+ 'field «%s» in «%s» annotated as «%s» but actual PHP type is «%s»',
+ $propName,
+ TimemanSettingsItemResult::class,
+ $typeStr,
+ get_debug_type($value)
+ );
+
+ match (true) {
+ str_contains($typeStr, 'array') => $this->assertIsArray($value, $message),
+ str_contains($typeStr, 'bool') => $this->assertIsBool($value, $message),
+ str_contains($typeStr, 'int') => $this->assertIsInt($value, $message),
+ str_contains($typeStr, 'float') => $this->assertIsFloat($value, $message),
+ str_contains($typeStr, 'string') => $this->assertIsString($value, $message),
+ default => $this->assertInstanceOf($typeStr, $value, $message),
+ };
+ }
+ }
+}
+
diff --git a/tests/Integration/Services/Timeman/Result/WorkdayItemResultAnnotationsTest.php b/tests/Integration/Services/Timeman/Result/WorkdayItemResultAnnotationsTest.php
new file mode 100644
index 00000000..058110fe
--- /dev/null
+++ b/tests/Integration/Services/Timeman/Result/WorkdayItemResultAnnotationsTest.php
@@ -0,0 +1,113 @@
+
+ *
+ * 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\Timeman\Result;
+
+use Bitrix24\SDK\Services\Timeman\Result\WorkdayItemResult;
+use Bitrix24\SDK\Services\Timeman\Service\Timeman;
+use Bitrix24\SDK\Tests\CustomAssertions\CustomBitrix24Assertions;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use Carbon\CarbonImmutable;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Test;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+use Typhoon\Reflection\TyphoonReflector;
+use function Typhoon\Type\stringify;
+
+#[CoversClass(WorkdayItemResult::class)]
+class WorkdayItemResultAnnotationsTest extends TestCase
+{
+ use CustomBitrix24Assertions;
+
+ private Timeman $timemanService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->timemanService = Fabric::getServiceBuilder()->getTimemanScope()->timeman();
+ // Ensure a workday is open so the API returns all fields (not just STATUS)
+ $this->timemanService->open();
+ }
+
+ #[\Override]
+ protected function tearDown(): void
+ {
+ // Close the workday opened in setUp to clean up
+ $this->timemanService->close();
+ }
+
+ #[Test]
+ #[TestDox('all fields in WorkdayItemResult are annotated in phpdoc and match with raw api response')]
+ public function testAllSystemFieldsAnnotated(): void
+ {
+ $rawResult = $this->timemanService->status()->getCoreResponse()
+ ->getResponseData()->getResult();
+
+ $this->assertBitrix24AllResultItemFieldsAnnotated(
+ array_keys($rawResult),
+ WorkdayItemResult::class
+ );
+ }
+
+ #[Test]
+ #[TestDox('all fields in WorkdayItemResult have valid type casting in magic getters')]
+ public function testAllSystemFieldsHasValidTypeAnnotation(): void
+ {
+ $workdayItemResult = $this->timemanService->status()->getWorkday();
+
+ // We do the type-cast check inline rather than via assertBitrix24ResultItemFieldsTypeCastMatchAnnotations
+ // because that method passes the raw union-type string (e.g. "Carbon\CarbonImmutable|null")
+ // to assertInstanceOf(), which PHP rejects as an invalid class name.
+ $collection = TyphoonReflector::build()
+ ->reflectClass(WorkdayItemResult::class)
+ ->properties();
+
+ foreach ($collection as $meta) {
+ if (!$meta->isAnnotated()) {
+ continue;
+ }
+
+ if ($meta->isNative()) {
+ continue;
+ }
+
+ $propName = $meta->id->name;
+ $typeStr = stringify($meta->type());
+ $value = $workdayItemResult->$propName;
+
+ // null is always valid for nullable types
+ if (str_contains($typeStr, 'null') && $value === null) {
+ continue;
+ }
+
+ $message = sprintf(
+ 'field «%s» in «%s» annotated as «%s» but actual PHP type is «%s»',
+ $propName,
+ WorkdayItemResult::class,
+ $typeStr,
+ get_debug_type($value)
+ );
+
+ match (true) {
+ str_contains($typeStr, CarbonImmutable::class) => $this->assertInstanceOf(CarbonImmutable::class, $value, $message),
+ str_contains($typeStr, 'array') => $this->assertIsArray($value, $message),
+ str_contains($typeStr, 'bool') => $this->assertIsBool($value, $message),
+ str_contains($typeStr, 'int') => $this->assertIsInt($value, $message),
+ str_contains($typeStr, 'float') => $this->assertIsFloat($value, $message),
+ str_contains($typeStr, 'string') => $this->assertIsString($value, $message),
+ default => $this->assertInstanceOf($typeStr, $value, $message),
+ };
+ }
+ }
+}
diff --git a/tests/Integration/Services/Timeman/Service/TimemanTest.php b/tests/Integration/Services/Timeman/Service/TimemanTest.php
new file mode 100644
index 00000000..5c44e894
--- /dev/null
+++ b/tests/Integration/Services/Timeman/Service/TimemanTest.php
@@ -0,0 +1,122 @@
+
+ *
+ * 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\Timeman\Service;
+
+use Bitrix24\SDK\Core\Exceptions\BaseException;
+use Bitrix24\SDK\Core\Exceptions\TransportException;
+use Bitrix24\SDK\Services\Timeman\Service\Timeman;
+use Bitrix24\SDK\Tests\Integration\Fabric;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+
+#[CoversClass(Timeman::class)]
+class TimemanTest extends TestCase
+{
+ private Timeman $timemanService;
+
+ #[\Override]
+ protected function setUp(): void
+ {
+ $this->timemanService = Fabric::getServiceBuilder()->getTimemanScope()->timeman();
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('timeman.status returns current workday information')]
+ public function testStatus(): void
+ {
+ $workdayResult = $this->timemanService->status();
+ $workdayItemResult = $workdayResult->getWorkday();
+
+ $this->assertContains(
+ $workdayItemResult->STATUS,
+ ['OPENED', 'CLOSED', 'PAUSED', 'EXPIRED'],
+ 'STATUS must be one of OPENED, CLOSED, PAUSED, EXPIRED'
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('timeman.settings returns user work time settings')]
+ public function testSettings(): void
+ {
+ $timemanSettingsResult = $this->timemanService->settings();
+ $timemanSettingsItemResult = $timemanSettingsResult->getSettings();
+
+ $this->assertIsBool($timemanSettingsItemResult->UF_TIMEMAN);
+ $this->assertIsBool($timemanSettingsItemResult->UF_TM_FREE);
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('timeman.open opens the workday for the current user')]
+ public function testOpen(): void
+ {
+ $workdayResult = $this->timemanService->open();
+ $workdayItemResult = $workdayResult->getWorkday();
+
+ $this->assertContains(
+ $workdayItemResult->STATUS,
+ ['OPENED', 'CLOSED', 'PAUSED', 'EXPIRED'],
+ 'STATUS must be one of OPENED, CLOSED, PAUSED, EXPIRED'
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('timeman.pause pauses the current workday')]
+ public function testPause(): void
+ {
+ // Ensure workday is open before pausing
+ $this->timemanService->open();
+
+ $workdayResult = $this->timemanService->pause();
+ $workdayItemResult = $workdayResult->getWorkday();
+
+ $this->assertContains(
+ $workdayItemResult->STATUS,
+ ['OPENED', 'CLOSED', 'PAUSED', 'EXPIRED'],
+ 'STATUS must be one of OPENED, CLOSED, PAUSED, EXPIRED'
+ );
+ }
+
+ /**
+ * @throws BaseException
+ * @throws TransportException
+ */
+ #[TestDox('timeman.close closes the current workday')]
+ public function testClose(): void
+ {
+ // Ensure workday is open before closing
+ $this->timemanService->open();
+
+ $workdayResult = $this->timemanService->close();
+ $workdayItemResult = $workdayResult->getWorkday();
+
+ $this->assertContains(
+ $workdayItemResult->STATUS,
+ ['OPENED', 'CLOSED', 'PAUSED', 'EXPIRED'],
+ 'STATUS must be one of OPENED, CLOSED, PAUSED, EXPIRED'
+ );
+ }
+}
diff --git a/tests/Unit/Services/Timeman/Service/TimemanTest.php b/tests/Unit/Services/Timeman/Service/TimemanTest.php
new file mode 100644
index 00000000..5390c3dd
--- /dev/null
+++ b/tests/Unit/Services/Timeman/Service/TimemanTest.php
@@ -0,0 +1,230 @@
+
+ *
+ * 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\Timeman\Service;
+
+use Bitrix24\SDK\Core\Contracts\CoreInterface;
+use Bitrix24\SDK\Core\Response\Response;
+use Bitrix24\SDK\Services\Timeman\Result\TimemanSettingsResult;
+use Bitrix24\SDK\Services\Timeman\Result\WorkdayResult;
+use Bitrix24\SDK\Services\Timeman\Service\Timeman;
+use Carbon\CarbonImmutable;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\TestDox;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\NullLogger;
+
+#[CoversClass(Timeman::class)]
+class TimemanTest extends TestCase
+{
+ #[TestDox('Test Timeman service can be instantiated')]
+ public function testCanBeInstantiated(): void
+ {
+ $core = $this->createStub(CoreInterface::class);
+ $timeman = new Timeman($core, new NullLogger());
+
+ $this->assertInstanceOf(Timeman::class, $timeman);
+ }
+
+ #[TestDox('Test Timeman::open calls timeman.open without parameters')]
+ public function testOpenCallsCorrectMethodWithNoParams(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.open', [])
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->open();
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::open builds correct parameters')]
+ public function testOpenBuildsCorrectParameters(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+ $time = CarbonImmutable::create(2025, 3, 27, 8, 0, 1, 'UTC');
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with(
+ 'timeman.open',
+ [
+ 'USER_ID' => 503,
+ 'TIME' => $time->format(CarbonImmutable::ATOM),
+ 'REPORT' => 'Test report',
+ 'LAT' => 53.548841,
+ 'LON' => 9.987274,
+ ]
+ )
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->open(
+ userId: 503,
+ time: $time,
+ report: 'Test report',
+ lat: 53.548841,
+ lon: 9.987274
+ );
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::pause calls timeman.pause without parameters')]
+ public function testPauseCallsCorrectMethodWithNoParams(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.pause', [])
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->pause();
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::pause builds correct parameters')]
+ public function testPauseBuildsCorrectParameters(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.pause', ['USER_ID' => 503])
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->pause(userId: 503);
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::close calls timeman.close without parameters')]
+ public function testCloseCallsCorrectMethodWithNoParams(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.close', [])
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->close();
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::close builds correct parameters')]
+ public function testCloseBuildsCorrectParameters(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+ $time = CarbonImmutable::create(2025, 3, 27, 17, 0, 0, 'UTC');
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with(
+ 'timeman.close',
+ [
+ 'USER_ID' => 503,
+ 'TIME' => $time->format(CarbonImmutable::ATOM),
+ 'REPORT' => 'End of day',
+ 'LAT' => 53.548841,
+ 'LON' => 9.987274,
+ ]
+ )
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->close(
+ userId: 503,
+ time: $time,
+ report: 'End of day',
+ lat: 53.548841,
+ lon: 9.987274
+ );
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::status calls timeman.status without parameters')]
+ public function testStatusCallsCorrectMethodWithNoParams(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.status', [])
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->status();
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::status builds correct parameters')]
+ public function testStatusBuildsCorrectParameters(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.status', ['USER_ID' => 503])
+ ->willReturn($response);
+
+ $workdayResult = (new Timeman($core, new NullLogger()))->status(userId: 503);
+
+ $this->assertInstanceOf(WorkdayResult::class, $workdayResult);
+ }
+
+ #[TestDox('Test Timeman::settings calls timeman.settings without parameters')]
+ public function testSettingsCallsCorrectMethodWithNoParams(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.settings', [])
+ ->willReturn($response);
+
+ $timemanSettingsResult = (new Timeman($core, new NullLogger()))->settings();
+
+ $this->assertInstanceOf(TimemanSettingsResult::class, $timemanSettingsResult);
+ }
+
+ #[TestDox('Test Timeman::settings builds correct parameters')]
+ public function testSettingsBuildsCorrectParameters(): void
+ {
+ $core = $this->createMock(CoreInterface::class);
+ $response = $this->createStub(Response::class);
+
+ $core->expects($this->once())
+ ->method('call')
+ ->with('timeman.settings', ['USER_ID' => 503])
+ ->willReturn($response);
+
+ $timemanSettingsResult = (new Timeman($core, new NullLogger()))->settings(userId: 503);
+
+ $this->assertInstanceOf(TimemanSettingsResult::class, $timemanSettingsResult);
+ }
+}