124 lines
5.0 KiB
PHP
124 lines
5.0 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Jobs\ClientTg\AccumulateTelegramLeadJob;
|
||
|
|
use App\Models\ClientTg\AutoRule;
|
||
|
|
use App\Models\ClientTg\Campaign;
|
||
|
|
use App\Models\Deal;
|
||
|
|
use App\Models\Tenant;
|
||
|
|
use App\Services\ClientTg\TelegramAutoAccumulator;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Illuminate\Support\Facades\Queue;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Защитный observer авто-режима Telegram (план §Сессия 4, задача 4.2).
|
||
|
|
*
|
||
|
|
* 🔴🔴🔴 Observer живёт ВНУТРИ боевого потока приёма лидов (RouteSupplierLeadJob,
|
||
|
|
* DealController, вебхуки) — он НИКОГДА не должен уронить создание сделки. Всё тело —
|
||
|
|
* try/catch(Throwable)+Log. Дёшев: не ходит в auto_rule (решение «слать/копить» — за
|
||
|
|
* джобом/накопителем); freshness-guard отсекает массовый импорт. Зеркало DealSmsObserver.
|
||
|
|
*
|
||
|
|
* Приёма-джоб (AccumulateTelegramLeadJob) уводит работу с роли приёмщика лида
|
||
|
|
* (crm_supplier_worker, без GRANT на auto_rule) на очередь (crm_app_user) — иначе
|
||
|
|
* на проде auto_rule под supplier-ролью дал бы «тихий ноль».
|
||
|
|
*/
|
||
|
|
uses(RefreshDatabase::class);
|
||
|
|
|
||
|
|
function makeTgObserverRule(int $tenantId): AutoRule
|
||
|
|
{
|
||
|
|
return AutoRule::create([
|
||
|
|
'tenant_id' => $tenantId,
|
||
|
|
'enabled' => true,
|
||
|
|
'ad_text' => 'Заходите в наш канал',
|
||
|
|
'ad_link' => 'https://t.me/example_channel',
|
||
|
|
'ord_category' => 'Размещение рекламы',
|
||
|
|
'budget_cap_rub' => '2500.00',
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(function () {
|
||
|
|
$this->tenant = Tenant::factory()->create();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('свежий лид → ставит приёма-джоб накопителя', function () {
|
||
|
|
Queue::fake();
|
||
|
|
|
||
|
|
$deal = Deal::factory()->create([
|
||
|
|
'tenant_id' => $this->tenant->id,
|
||
|
|
'phone' => '79990000001',
|
||
|
|
'received_at' => now(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
Queue::assertPushed(AccumulateTelegramLeadJob::class, function ($job) {
|
||
|
|
return true; // сам факт постановки; свойства джоба приватные
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('старый/импортный лид → накопитель не трогаем (freshness-guard)', function () {
|
||
|
|
Queue::fake();
|
||
|
|
|
||
|
|
Deal::factory()->create([
|
||
|
|
'tenant_id' => $this->tenant->id,
|
||
|
|
'phone' => '79990000002',
|
||
|
|
'received_at' => now()->subDays(5),
|
||
|
|
]);
|
||
|
|
|
||
|
|
Queue::assertNotPushed(AccumulateTelegramLeadJob::class);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('приём лида не падает, даже если постановка джоба бросит', function () {
|
||
|
|
// Подменяем джоб на такой, чей dispatch бросает — observer обязан проглотить.
|
||
|
|
// Проверяем, что создание сделки всё равно проходит (сделка в БД).
|
||
|
|
Queue::fake();
|
||
|
|
|
||
|
|
$deal = Deal::factory()->create([
|
||
|
|
'tenant_id' => $this->tenant->id,
|
||
|
|
'phone' => '79990000003',
|
||
|
|
'received_at' => now(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
expect(Deal::find($deal->id))->not->toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('приёма-джоб кормит накопитель телефоном лида', function () {
|
||
|
|
Queue::fake(); // накопитель внутри джоба поставит RunTelegramCampaignJob, но не при пороге 367
|
||
|
|
makeTgObserverRule($this->tenant->id);
|
||
|
|
|
||
|
|
$deal = Deal::factory()->create([
|
||
|
|
'tenant_id' => $this->tenant->id,
|
||
|
|
'phone' => '79990000004',
|
||
|
|
'received_at' => now(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Прогоняем джоб «руками» с реальным накопителем.
|
||
|
|
app()->call([new AccumulateTelegramLeadJob($deal->id, $this->tenant->id), 'handle']);
|
||
|
|
|
||
|
|
// Накопитель открыл авто-черновик и положил в него телефон лида.
|
||
|
|
$draft = Campaign::where('tenant_id', $this->tenant->id)
|
||
|
|
->whereNull('created_by')
|
||
|
|
->where('status', Campaign::STATUS_DRAFT)
|
||
|
|
->first();
|
||
|
|
expect($draft)->not->toBeNull();
|
||
|
|
expect($draft->phones()->where('phone', '79990000004')->exists())->toBeTrue();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('приёма-джоб — best-effort: сбой накопителя не пробрасывается', function () {
|
||
|
|
$deal = Deal::factory()->create([
|
||
|
|
'tenant_id' => $this->tenant->id,
|
||
|
|
'phone' => '79990000005',
|
||
|
|
'received_at' => now(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Накопитель, который всегда бросает — джоб обязан проглотить (не уронить воркер).
|
||
|
|
$boom = Mockery::mock(TelegramAutoAccumulator::class);
|
||
|
|
$boom->shouldReceive('accumulate')->andThrow(new RuntimeException('boom'));
|
||
|
|
app()->instance(TelegramAutoAccumulator::class, $boom);
|
||
|
|
|
||
|
|
$job = new AccumulateTelegramLeadJob($deal->id, $this->tenant->id);
|
||
|
|
// Не должно бросить.
|
||
|
|
app()->call([$job, 'handle']);
|
||
|
|
|
||
|
|
expect(true)->toBeTrue();
|
||
|
|
});
|