-
Notifications
You must be signed in to change notification settings - Fork 3
Feature/110 active accounts read model #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
camaxtly
merged 3 commits into
mesilov:feature/110-active-accounts-read-model
from
KarlsonComplete:feature/110-active-accounts-read-model
Aug 23, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
d92bd11
Introduce `Bitrix24AccountReadModel` for active accounts retrieval wi…
KarlsonComplete 8cc8d37
Refactor test setup in `Bitrix24AccountReadModelTest` to simplify bui…
KarlsonComplete 45c99b1
Enhance `Bitrix24AccountReadModelTest` to validate active accounts re…
KarlsonComplete File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # #110 — Read model для списка активных аккаунтов Bitrix24 | ||
|
|
||
| ## Summary | ||
|
|
||
| Нужен read-only способ получить все активные аккаунты Bitrix24 | ||
| (`status = Bitrix24AccountStatus::active`) с пагинацией и стабильной сортировкой. | ||
|
|
||
| Решение согласовано в issue #110: reviewer (camaxtly) явно сказал — интерфейс | ||
| не нужен, достаточно маленькой конкретной реализации (read model). Сигнатуру | ||
| метода оставили из предложения KarlsonComplete. | ||
|
|
||
| ## Scope | ||
|
|
||
| В рамках задачи: | ||
|
|
||
| - новый конкретный класс read model в `src/Bitrix24Accounts/Infrastructure/Doctrine/` | ||
| - функциональный тест на него | ||
|
|
||
| Вне задачи: | ||
|
|
||
| - интерфейс для read model (отвергнуто reviewer) | ||
| - CLI-команда в этой библиотеке (ответственность consumer-приложения) | ||
| - изменения CHANGELOG.md (по запросу пользователя) | ||
| - открытие PR (пользователь открывает самостоятельно) | ||
|
|
||
| ## Target contract | ||
|
|
||
| Метод (точная сигнатура из предложения KarlsonComplete, без интерфейса): | ||
|
|
||
| ```php | ||
| public function findAllActive(string $sort = 'desc', int $page = 1, int $limit = 50): PaginationInterface | ||
| ``` | ||
|
|
||
| Поведение: | ||
|
|
||
| - `$sort` ∈ `{'asc','desc'}` — направление сортировки по полю `createdAt`. | ||
| При невалидном значении бросаем | ||
| `Bitrix24\SDK\Core\Exceptions\InvalidArgumentException` | ||
| (тот же тип исключения, что используется в bounded context). | ||
| - Фильтр: `a.status = Bitrix24AccountStatus::active` | ||
| - Возврат: `PaginationInterface<Bitrix24Account>` через Knp paginator | ||
| (`knplabs/knp-paginator-bundle ^6` уже в `composer.json`). | ||
| - Образец wiring/стиля: `src/Journal/Infrastructure/Doctrine/DoctrineDbalJournalItemRepository.php`. | ||
|
|
||
| ## Implementation changes | ||
|
|
||
| 1. `src/Bitrix24Accounts/Infrastructure/Doctrine/Bitrix24AccountReadModel.php` | ||
|
|
||
| - `final` класс, без интерфейса | ||
| - конструктор: `EntityManagerInterface` + `PaginatorInterface` | ||
| - метод `findAllActive(...)` см. выше | ||
| - `QueryBuilder` по alias `a`: `where a.status = :status` (= `active->name`), | ||
| `orderBy a.createdAt` в направлении `$sort` | ||
| - пагинация: `$this->paginator->paginate($qb, $page, $limit)` | ||
| (без sort-опций Knp — сортировка зафиксирована в QueryBuilder) | ||
|
|
||
| 2. `tests/Functional/Bitrix24Accounts/Infrastructure/Doctrine/Bitrix24AccountReadModelTest.php` | ||
|
|
||
| - `setUp` по образцу `tests/Functional/Journal/Services/HandlerTest.php` | ||
| (`Paginator` с `TraceableEventDispatcher` + stub `ArgumentAccessInterface`), | ||
| изоляция через transactional `setUp`/`tearDown` (`beginTransaction` / `rollback`) | ||
| - наполнение через `Bitrix24AccountBuilder` + `Bitrix24AccountRepository::save()` + `Flusher::flush()` | ||
|
|
||
| ## Test cases and scenarios | ||
|
|
||
| 1. Функциональные: | ||
| - `findAllActive()` возвращает только active-аккаунты (есть new/blocked/deleted — они отсеиваются) | ||
| - метаданные пагинации (`getTotalItemCount`, размер текущей страницы, `limit`) | ||
| - направление сортировки: `desc` (default) = новые первыми, `asc` = старые первыми | ||
| (для детерминизма между созданием аккаунтов делаем `usleep`, чтобы миллисекундные | ||
| `createdAt` различались при precision=3) | ||
| - невалидный `$sort` бросает `InvalidArgumentException` | ||
|
|
||
| ## Assumptions and defaults | ||
|
|
||
| - Базовая ветка: `dev` (локальная) | ||
| - Ветка работы: `feature/110-active-accounts-read-model` | ||
| - `CHANGELOG.md` не редактируем | ||
| - PR открывает пользователь самостоятельно | ||
| - Функциональные тесты требуют запущенный Docker + Postgres (`make test-functional`) | ||
| - Контроль качества только через `Makefile`: `make lint-all`, `make test-unit`, `make test-functional` |
56 changes: 56 additions & 0 deletions
56
src/Bitrix24Accounts/Infrastructure/Doctrine/Bitrix24AccountReadModel.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Bitrix24\Lib\Bitrix24Accounts\Infrastructure\Doctrine; | ||
|
|
||
| use Bitrix24\Lib\Bitrix24Accounts\Entity\Bitrix24Account; | ||
| use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountStatus; | ||
| use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; | ||
| use Doctrine\ORM\EntityManagerInterface; | ||
| use Knp\Component\Pager\Pagination\PaginationInterface; | ||
| use Knp\Component\Pager\PaginatorInterface; | ||
|
|
||
| /** | ||
| * Read-only access to Bitrix24 accounts for listing / reporting use cases. | ||
| */ | ||
| final readonly class Bitrix24AccountReadModel | ||
| { | ||
| public function __construct( | ||
| private EntityManagerInterface $entityManager, | ||
| private PaginatorInterface $paginator | ||
| ) {} | ||
|
|
||
| /** | ||
| * Returns all active Bitrix24 accounts with pagination and stable ordering by createdAt. | ||
| * | ||
| * @param string $sort sort direction by account createdAt, 'asc' or 'desc' | ||
| * @param int $page 1-based page number | ||
| * @param int $limit number of items per page | ||
| * | ||
| * @return PaginationInterface<Bitrix24Account> | ||
| * | ||
| * @throws InvalidArgumentException if $sort is neither "asc" nor "desc" | ||
| */ | ||
| public function findAllActive(string $sort = 'desc', int $page = 1, int $limit = 50): PaginationInterface | ||
| { | ||
| if ('asc' !== $sort && 'desc' !== $sort) { | ||
| throw new InvalidArgumentException( | ||
| sprintf('sort direction must be "asc" or "desc", got "%s"', $sort) | ||
| ); | ||
| } | ||
|
|
||
| $queryBuilder = $this->entityManager->getRepository(Bitrix24Account::class) | ||
| ->createQueryBuilder('a') | ||
| ->where('a.status = :status') | ||
| ->setParameter('status', Bitrix24AccountStatus::active->name) | ||
| ->orderBy('a.createdAt', $sort) | ||
| ; | ||
|
|
||
| return $this->paginator->paginate( | ||
| $queryBuilder, | ||
| $page, | ||
| $limit | ||
| ); | ||
| } | ||
| } |
105 changes: 105 additions & 0 deletions
105
tests/Functional/Bitrix24Accounts/Infrastructure/Doctrine/Bitrix24AccountReadModelTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Bitrix24\Lib\Tests\Functional\Bitrix24Accounts\Infrastructure\Doctrine; | ||
|
|
||
| use Bitrix24\Lib\Bitrix24Accounts\Infrastructure\Doctrine\Bitrix24AccountReadModel; | ||
| use Bitrix24\Lib\Bitrix24Accounts\Infrastructure\Doctrine\Bitrix24AccountRepository; | ||
| use Bitrix24\Lib\Services\Flusher; | ||
| use Bitrix24\Lib\Tests\EntityManagerFactory; | ||
| use Bitrix24\Lib\Tests\Functional\Bitrix24Accounts\Builders\Bitrix24AccountBuilder; | ||
| use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountStatus; | ||
| use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException; | ||
| use Knp\Component\Pager\ArgumentAccess\ArgumentAccessInterface; | ||
| use Knp\Component\Pager\Event\Subscriber\Paginate\PaginationSubscriber; | ||
| use Knp\Component\Pager\Paginator; | ||
| use PHPUnit\Framework\Attributes\CoversClass; | ||
| use PHPUnit\Framework\Attributes\Test; | ||
| use PHPUnit\Framework\TestCase; | ||
| use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; | ||
| use Symfony\Component\EventDispatcher\EventDispatcher; | ||
| use Symfony\Component\Stopwatch\Stopwatch; | ||
| use Symfony\Component\Uid\Uuid; | ||
|
|
||
| /** | ||
| * @internal | ||
| */ | ||
| #[CoversClass(Bitrix24AccountReadModel::class)] | ||
| class Bitrix24AccountReadModelTest extends TestCase | ||
| { | ||
| private Bitrix24AccountReadModel $readModel; | ||
|
|
||
| private Bitrix24AccountRepository $repository; | ||
|
|
||
| private Flusher $flusher; | ||
|
|
||
| #[\Override] | ||
| protected function setUp(): void | ||
| { | ||
| $entityManager = EntityManagerFactory::get(); | ||
| $traceableEventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); | ||
| $traceableEventDispatcher->addSubscriber(new PaginationSubscriber()); | ||
|
|
||
| $this->readModel = new Bitrix24AccountReadModel( | ||
| $entityManager, | ||
| new Paginator($traceableEventDispatcher, $this->createStub(ArgumentAccessInterface::class)) | ||
| ); | ||
| $this->repository = new Bitrix24AccountRepository($entityManager); | ||
| $this->flusher = new Flusher($entityManager, $traceableEventDispatcher); | ||
| } | ||
|
|
||
| #[Test] | ||
| public function testFindAllActiveReturnsOnlyActiveAccounts(): void | ||
| { | ||
| $activeMemberIds = [ | ||
| Uuid::v4()->toRfc4122(), | ||
| Uuid::v4()->toRfc4122(), | ||
| Uuid::v4()->toRfc4122(), | ||
| ]; | ||
|
|
||
| foreach ($activeMemberIds as $activeMemberId) { | ||
| $activeAccount = new Bitrix24AccountBuilder() | ||
| ->withMemberId($activeMemberId) | ||
| ->withStatus(Bitrix24AccountStatus::new) | ||
| ->withInstalled() | ||
| ->build(); | ||
|
|
||
| $this->repository->save($activeAccount); | ||
|
|
||
| $this->flusher->flush($activeAccount); | ||
| } | ||
|
|
||
| $new = new Bitrix24AccountBuilder()->build(); | ||
| $blocked = new Bitrix24AccountBuilder()->withStatus(Bitrix24AccountStatus::new)->withInstalled()->build(); | ||
| $blocked->markAsBlocked(null); | ||
|
|
||
| $deleted = new Bitrix24AccountBuilder()->withStatus(Bitrix24AccountStatus::new)->withInstalled()->build(); | ||
| $deleted->applicationUninstalled(null); | ||
|
|
||
| $this->repository->save($new); | ||
| $this->repository->save($blocked); | ||
| $this->repository->save($deleted); | ||
|
|
||
| $this->flusher->flush($new, $blocked, $deleted); | ||
|
|
||
| $foundActiveAccounts = 0; | ||
| $pagination = $this->readModel->findAllActive('desc', 1, 1000); | ||
| foreach ($pagination as $account) { | ||
| self::assertSame(Bitrix24AccountStatus::active, $account->getStatus()); | ||
|
|
||
| if (in_array($account->getMemberId(), $activeMemberIds, true)) { | ||
| $foundActiveAccounts++; | ||
| } | ||
| } | ||
|
|
||
| self::assertSame(3, $foundActiveAccounts, 'findAllActive must return exactly all created active accounts'); | ||
| } | ||
|
|
||
| #[Test] | ||
| public function testFindAllActiveThrowsOnInvalidSortDirection(): void | ||
| { | ||
| $this->expectException(InvalidArgumentException::class); | ||
| $this->readModel->findAllActive('random'); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.