Files
portal/app/tests/Feature/Notifications/NewLeadsDigestJobTest.php
T
Дмитрий 825b02cf72 fix(digest): идемпотентность дайджеста новых сделок по сделке (N-4)
N-4: SendNewLeadsDigestJob выбирал сделки received_at>now()-30min без маркера
отправки → ручной/повторный прогон (R3b велит дёргать вручную) дублировал
письмо-дайджест. Окно «непересекается» только при ровно-30-мин прогонах.

Фикс без схемы: идемпотентность по сделке через Redis SETNX
(Cache::add 'digest_sent:<id>', TTL 1 сутки) — паттерн как rate-limit
ZeroBalancePausedMail. Уже отправленная сделка в дайджест повторно не входит.

TDD: тест «повторный прогон НЕ дублирует» (RED 2 письма → GREEN 1), сюит 5/5.
R3b DIGEST-ON + owner-decisions + NEW-статус обновлены (N-4 → закрыто).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:01:10 +03:00

102 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\SendNewLeadsDigestJob;
use App\Mail\NewLeadNotification;
use App\Mail\NewLeadsDigestMail;
use App\Models\Deal;
use App\Models\Tenant;
use App\Models\User;
use App\Services\NotificationService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
uses(DatabaseTransactions::class);
beforeEach(function () {
Mail::fake();
});
function digestUser(Tenant $tenant, string $email, bool $emailOn): User
{
return User::factory()->create([
'tenant_id' => $tenant->id,
'email' => $email,
'notification_preferences' => [
'new_lead' => ['email' => $emailOn, 'inapp' => true],
],
]);
}
it('шлёт одно письмо-сводку с N сделками подписанному пользователю', function () {
$tenant = Tenant::factory()->create();
digestUser($tenant, 'digest-on@example.test', true);
DB::statement('SET app.current_tenant_id = '.$tenant->id);
Deal::factory()->for($tenant)->create(['received_at' => now()->subMinutes(5), 'is_test' => false]);
Deal::factory()->for($tenant)->create(['received_at' => now()->subMinutes(10), 'is_test' => false]);
Deal::factory()->for($tenant)->create(['received_at' => now()->subMinutes(15), 'is_test' => false]);
(new SendNewLeadsDigestJob)->handle(app(NotificationService::class));
Mail::assertSent(
NewLeadsDigestMail::class,
fn (NewLeadsDigestMail $m) => $m->hasTo('digest-on@example.test') && $m->deals->count() === 3,
);
});
it('не шлёт сводку пользователю с выключенным email', function () {
$tenant = Tenant::factory()->create();
digestUser($tenant, 'digest-off@example.test', false);
DB::statement('SET app.current_tenant_id = '.$tenant->id);
Deal::factory()->for($tenant)->create(['received_at' => now()->subMinutes(5), 'is_test' => false]);
(new SendNewLeadsDigestJob)->handle(app(NotificationService::class));
Mail::assertNotSent(
NewLeadsDigestMail::class,
fn (NewLeadsDigestMail $m) => $m->hasTo('digest-off@example.test'),
);
});
it('не шлёт сводку, если за окно нет новых сделок', function () {
$tenant = Tenant::factory()->create();
digestUser($tenant, 'digest-old@example.test', true);
DB::statement('SET app.current_tenant_id = '.$tenant->id);
Deal::factory()->for($tenant)->create(['received_at' => now()->subMinutes(45), 'is_test' => false]);
(new SendNewLeadsDigestJob)->handle(app(NotificationService::class));
Mail::assertNotSent(
NewLeadsDigestMail::class,
fn (NewLeadsDigestMail $m) => $m->hasTo('digest-old@example.test'),
);
});
it('повторный прогон в том же окне НЕ дублирует дайджест (N-4)', function () {
$tenant = Tenant::factory()->create();
digestUser($tenant, 'digest-dup@example.test', true);
DB::statement('SET app.current_tenant_id = '.$tenant->id);
Deal::factory()->for($tenant)->create(['received_at' => now()->subMinutes(5), 'is_test' => false]);
Deal::factory()->for($tenant)->create(['received_at' => now()->subMinutes(10), 'is_test' => false]);
// ранбук R3b сам велит дёргать джоб вручную → два прогона в одном окне
(new SendNewLeadsDigestJob)->handle(app(NotificationService::class));
(new SendNewLeadsDigestJob)->handle(app(NotificationService::class));
// письмо-сводка ушло РОВНО один раз — уже отправленные сделки не дублируются
Mail::assertSent(NewLeadsDigestMail::class, 1);
});
it('notifyNewLead больше не шлёт пер-лид письмо', function () {
$tenant = Tenant::factory()->create();
digestUser($tenant, 'perlead@example.test', true);
DB::statement('SET app.current_tenant_id = '.$tenant->id);
$deal = Deal::factory()->for($tenant)->create(['received_at' => now(), 'is_test' => false]);
app(NotificationService::class)->notifyNewLead($tenant, $deal);
Mail::assertNotSent(NewLeadNotification::class);
});