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); + } +}