5211048bce
Задача 2.0 (разведка) — РЕШЕНА без входа в кабинет: ответ уже был в находках Фазы 2. Билинг МТС за «показы своей базе» — НАКОПИТЕЛЬНЫЙ (резерв → списание по факту показов → возврат остатка, экран /payment). Следствие: списание «по факту» (была задача 2.4) на успехе отправки честно сделать нельзя — показов ещё нет; перенесено в Этап 3 (опрос завершения). Этап 2 закрыт составом 2.1 (отмена) + 2.3 (возврат брони) + 2.2. - 2.2 PHP: RobotResult несёт `actualCostRub` (nullable string) — заготовка, чтобы позже (Этап 3) прочитать фактическую стоимость из кабинета и списать её с кошелька клиента. Проброшено в fromRobotJson (робот начнёт класть поле позже; нет поля → null). Мёртвых фабрик launched()/draftReady() не добавлял. - 2.2 Node: чистая утилита `parseCost(text)` в cabinet.js — «Стоимость кампании от 201,6 ₽» → «201.60» (запятая→точка, разделители тысяч включая неразрывный пробел код 160, дробь до 2 знаков без округления, нет числа → null). Вынесена отдельной покрытой функцией; к DOM-потоку НЕ подключена (селектор строки стоимости подтвердим живьём в Этапе 3). - Разведка билинга и решение по 2.4 зафиксированы в FLOW-FINDINGS.md; план Этапа 2 обновлён (2.0 решён, 2.4 → Этап 3, Этап 2 закрыт). TDD, робот замокан, тесты на liderra_testing. Приёмка: Node 40/40 (npm test), Pest RobotResultTest 5/5 + RobotRunnerTest (потребитель) зелёный, регрессия ClientTg 120/120, phpstan RobotResult.php 0, pint чисто, deptrac 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
2.2 KiB
PHP
55 lines
2.2 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Services\ClientTg;
|
||
|
||
/**
|
||
* Итог запуска Node-робота (bin/run.js). Зеркалит контракт runner.js:
|
||
* успех → {ok:true, matched, launched?, campaignId?, actualCostRub?}
|
||
* отказ → {ok:false, step, reason}
|
||
* А также «обёрточные» отказы (таймаут/непарсабельный вывод) — тоже ok:false с
|
||
* причиной, чтобы воркер не падал.
|
||
*
|
||
* actualCostRub — фактическая стоимость кампании из кабинета, строка вида
|
||
* «201.60» (или отсутствует → null; робот начнёт класть это поле позже).
|
||
*/
|
||
final class RobotResult
|
||
{
|
||
public function __construct(
|
||
public readonly bool $ok,
|
||
public readonly int $matched = 0,
|
||
public readonly bool $launched = false,
|
||
public readonly ?string $campaignId = null,
|
||
public readonly ?string $step = null,
|
||
public readonly ?string $reason = null,
|
||
public readonly ?string $actualCostRub = null,
|
||
) {}
|
||
|
||
/**
|
||
* Разбирает JSON робота. Отсутствие/ложь поля ok трактуется как отказ.
|
||
*
|
||
* @param array<string, mixed> $json
|
||
*/
|
||
public static function fromRobotJson(array $json): self
|
||
{
|
||
$ok = ($json['ok'] ?? false) === true;
|
||
|
||
return new self(
|
||
ok: $ok,
|
||
matched: (int) ($json['matched'] ?? 0),
|
||
launched: (bool) ($json['launched'] ?? false),
|
||
campaignId: isset($json['campaignId']) ? (string) $json['campaignId'] : null,
|
||
step: isset($json['step']) ? (string) $json['step'] : null,
|
||
reason: isset($json['reason']) ? (string) $json['reason'] : null,
|
||
actualCostRub: isset($json['actualCostRub']) ? (string) $json['actualCostRub'] : null,
|
||
);
|
||
}
|
||
|
||
/** Обёрточный отказ (таймаут, непарсабельный вывод, сбой процесса). */
|
||
public static function failed(string $reason, ?string $step = null): self
|
||
{
|
||
return new self(ok: false, step: $step, reason: $reason);
|
||
}
|
||
}
|