Skip to content

# Issue: ApplicationInstallations\UseCase\Install\Handler преждевременно переводит установку и аккаунт в active, если applicationToken === null #90

@mesilov

Description

@mesilov

version(s) affected

0.4.0

Description

Краткое описание

Сейчас ApplicationInstallations\UseCase\Install\Handler вызывает applicationInstalled($applicationToken) и для Bitrix24Account, и для ApplicationInstallation даже в том случае, когда applicationToken === null.

В результате:

  • Bitrix24Account переводится из new в active
  • ApplicationInstallation переводится из new в active
  • диспатчатся ApplicationInstallationFinishedEvent и Bitrix24AccountApplicationInstalledEvent

Это происходит раньше, чем Bitrix24 присылает отдельный webhook/event с application_token.

Ожидаемое поведение

Нужно различать два сценария:

  1. В Install\Command передан applicationToken
  • статус аккаунта становится active
  • статус установки становится active
  • токен сохраняется сразу
  1. В Install\Command передан null вместо applicationToken
  • статус аккаунта остаётся new
  • статус установки остаётся new
  • токен не сохраняется
  • переход в active должен происходить позже, когда придёт application_token через webhook / отдельный finish-flow

Фактическое поведение

Сейчас оба сценария приводят к одному результату: и аккаунт, и установка оказываются в active.

Причина:

  • Install\Handler всегда вызывает applicationInstalled($command->applicationToken)
  • applicationInstalled(null) всё равно меняет статус на active

Задействованные участки кода:

  • src/ApplicationInstallations/UseCase/Install/Handler.php
  • src/ApplicationInstallations/Entity/ApplicationInstallation.php
  • src/Bitrix24Accounts/Entity/Bitrix24Account.php

Почему это баг

UI install-flow в Bitrix24 двухшаговый:

  1. фронтенд начинает установку и отправляет auth payload без application_token
  2. позже Bitrix24 присылает webhook/event с application_token

До шага 2 доменные сущности должны оставаться в статусе new.

Текущее поведение приводит к тому, что backend считает установку завершённой до того, как пришёл application_token.

Как проверить исправление

После фикса нужно запустить reproduction-test.

Ожидаемый результат:

  • testHandleWithApplicationTokenMarksInstallationAndAccountAsActive проходит
  • testHandleWithoutApplicationTokenKeepsInstallationAndAccountInNewStatusUntilOnAppInstall проходит

How to reproduce

Минимальное воспроизведение

Приложен functional reproduction-test:

  • repro-bitrix24-php-lib-install-handler-status-test.php

Во втором кейсе тест сейчас должен падать:

  • с токеном: ожидается active
  • без токена: ожидается new

Possible Solution

Возможные направления исправления

Подойдёт один из вариантов:

  1. Изменить ApplicationInstallations\UseCase\Install\Handler, чтобы applicationInstalled() вызывался только если applicationToken !== null
  2. Ввести отдельный use case для старта установки ApplicationInstallation, по аналогии с уже существующим Bitrix24Accounts\UseCase\InstallStart
  3. Оставить переход new -> active только webhook-driven finish-логике

Additional Context

<?php

declare(strict_types=1);

namespace Bitrix24\Lib\Tests\Functional\ApplicationInstallations\UseCase\Install;

use Bitrix24\Lib\ApplicationInstallations\Infrastructure\Doctrine\ApplicationInstallationRepository;
use Bitrix24\Lib\ApplicationInstallations\UseCase\Install\Command;
use Bitrix24\Lib\ApplicationInstallations\UseCase\Install\Handler;
use Bitrix24\Lib\Bitrix24Accounts\Infrastructure\Doctrine\Bitrix24AccountRepository;
use Bitrix24\Lib\Bitrix24Accounts\ValueObjects\Domain;
use Bitrix24\Lib\Services\Flusher;
use Bitrix24\Lib\Tests\EntityManagerFactory;
use Bitrix24\Lib\Tests\Functional\ApplicationInstallations\Builders\ApplicationInstallationBuilder;
use Bitrix24\Lib\Tests\Functional\Bitrix24Accounts\Builders\Bitrix24AccountBuilder;
use Bitrix24\SDK\Application\ApplicationStatus;
use Bitrix24\SDK\Application\Contracts\ApplicationInstallations\Entity\ApplicationInstallationStatus;
use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountStatus;
use Bitrix24\SDK\Application\PortalLicenseFamily;
use Bitrix24\SDK\Core\Credentials\Scope;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Uid\Uuid;

#[CoversClass(Handler::class)]
final class InstallHandlerStatusByTokenTest extends TestCase
{
    private Handler $handler;

    private ApplicationInstallationRepository $applicationInstallationRepository;

    private Bitrix24AccountRepository $bitrix24AccountRepository;

    #[\Override]
    protected function setUp(): void
    {
        $entityManager = EntityManagerFactory::get();
        $eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch());

        $this->applicationInstallationRepository = new ApplicationInstallationRepository($entityManager);
        $this->bitrix24AccountRepository = new Bitrix24AccountRepository($entityManager);

        $this->handler = new Handler(
            $this->bitrix24AccountRepository,
            $this->applicationInstallationRepository,
            new Flusher($entityManager, $eventDispatcher),
            new NullLogger(),
        );
    }

    #[Test]
    public function testHandleWithApplicationTokenMarksInstallationAndAccountAsActive(): void
    {
        $bitrix24AccountBuilder = (new Bitrix24AccountBuilder())
            ->withApplicationScope(new Scope(['crm']))
            ->build();

        $applicationInstallationBuilder = (new ApplicationInstallationBuilder())
            ->withApplicationStatus(new ApplicationStatus('F'))
            ->withPortalLicenseFamily(PortalLicenseFamily::free)
            ->build();

        $applicationToken = Uuid::v7()->toRfc4122();

        $this->handler->handle(
            new Command(
                $bitrix24AccountBuilder->getMemberId(),
                new Domain($bitrix24AccountBuilder->getDomainUrl()),
                $bitrix24AccountBuilder->getAuthToken(),
                $bitrix24AccountBuilder->getApplicationVersion(),
                $bitrix24AccountBuilder->getApplicationScope(),
                $bitrix24AccountBuilder->getBitrix24UserId(),
                $bitrix24AccountBuilder->isBitrix24UserAdmin(),
                $applicationInstallationBuilder->getApplicationStatus(),
                $applicationInstallationBuilder->getPortalLicenseFamily(),
                $applicationToken,
                $applicationInstallationBuilder->getPortalUsersCount(),
                $applicationInstallationBuilder->getContactPersonId(),
                $applicationInstallationBuilder->getBitrix24PartnerContactPersonId(),
                $applicationInstallationBuilder->getBitrix24PartnerId(),
                $applicationInstallationBuilder->getExternalId(),
                $applicationInstallationBuilder->getComment(),
            ),
        );

        $installation = $this->applicationInstallationRepository->findByApplicationToken($applicationToken);
        self::assertNotNull($installation);
        self::assertSame(ApplicationInstallationStatus::active, $installation->getStatus());

        $accounts = $this->bitrix24AccountRepository->findByApplicationToken($applicationToken);
        self::assertCount(1, $accounts);
        self::assertSame(Bitrix24AccountStatus::active, $accounts[0]->getStatus());
    }

    #[Test]
    public function testHandleWithoutApplicationTokenKeepsInstallationAndAccountInNewStatusUntilOnAppInstall(): void
    {
        $bitrix24AccountBuilder = (new Bitrix24AccountBuilder())
            ->withApplicationScope(new Scope(['crm']))
            ->build();

        $applicationInstallationBuilder = (new ApplicationInstallationBuilder())
            ->withApplicationStatus(new ApplicationStatus('F'))
            ->withPortalLicenseFamily(PortalLicenseFamily::free)
            ->build();

        $memberId = $bitrix24AccountBuilder->getMemberId();

        $this->handler->handle(
            new Command(
                $memberId,
                new Domain($bitrix24AccountBuilder->getDomainUrl()),
                $bitrix24AccountBuilder->getAuthToken(),
                $bitrix24AccountBuilder->getApplicationVersion(),
                $bitrix24AccountBuilder->getApplicationScope(),
                $bitrix24AccountBuilder->getBitrix24UserId(),
                $bitrix24AccountBuilder->isBitrix24UserAdmin(),
                $applicationInstallationBuilder->getApplicationStatus(),
                $applicationInstallationBuilder->getPortalLicenseFamily(),
                null,
                $applicationInstallationBuilder->getPortalUsersCount(),
                $applicationInstallationBuilder->getContactPersonId(),
                $applicationInstallationBuilder->getBitrix24PartnerContactPersonId(),
                $applicationInstallationBuilder->getBitrix24PartnerId(),
                $applicationInstallationBuilder->getExternalId(),
                $applicationInstallationBuilder->getComment(),
            ),
        );

        $installation = $this->applicationInstallationRepository->findByBitrix24AccountMemberId($memberId);
        self::assertNotNull($installation);
        self::assertSame(ApplicationInstallationStatus::new, $installation->getStatus());

        $accounts = $this->bitrix24AccountRepository->findByMemberId($memberId);
        self::assertCount(1, $accounts);
        self::assertSame(Bitrix24AccountStatus::new, $accounts[0]->getStatus());
    }
}

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions