Files
portal/app/tests/Feature/Supplier/AutoPauseFlowTest.php
T
2026-05-23 20:44:53 +03:00

179 lines
7.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Jobs\RouteSupplierLeadJob;
use App\Mail\ZeroBalancePausedMail;
use App\Models\Project;
use App\Models\Supplier;
use App\Models\SupplierLead;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Billing\LedgerService;
use App\Services\LeadDistributor;
use App\Services\LeadRouter;
use App\Services\NotificationService;
use App\Services\RegionTagResolver;
use App\Services\SupplierProjects\SupplierProjectResolver;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
beforeEach(function () {
Mail::fake();
Cache::store('redis')->flush();
$this->seed(PricingTierSeeder::class);
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
});
function makeFlowWithBalance(array $balance): array
{
$supplier = Supplier::where('code', 'b1')->first();
$supplierProject = SupplierProject::factory()->create([
'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'example.com',
]);
$tenant = Tenant::factory()->create($balance);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site', 'signal_identifier' => 'example.com',
'supplier_b1_project_id' => $supplierProject->id,
'is_active' => true, 'daily_limit_target' => 10,
'effective_daily_limit_today' => 10, 'delivered_today' => 0,
'delivery_days_mask' => 127, 'region_mask' => 255,
]);
linkProjectToSupplier($project, $supplierProject);
$lead = SupplierLead::factory()->create([
'vid' => random_int(100_000_000, 999_999_999),
'phone' => '79991234567',
'raw_payload' => ['project' => 'B1_example.com', 'phone' => '79991234567', 'time' => time()],
'supplier_project_id' => $supplierProject->id,
'received_at' => now(),
]);
return compact('tenant', 'project', 'lead');
}
function runJob(int $leadId): void
{
(new RouteSupplierLeadJob($leadId))->handle(
app(LeadRouter::class),
app(SupplierProjectResolver::class),
app(NotificationService::class),
app(LedgerService::class),
app(LeadDistributor::class),
app(RegionTagResolver::class),
);
}
it('pauses project (is_active=false) when both balances empty', function () {
$ctx = makeFlowWithBalance(['balance_leads' => 0, 'balance_rub' => '100.00']);
runJob($ctx['lead']->id);
expect($ctx['project']->fresh()->is_active)->toBeFalse();
});
it('sends ZeroBalancePausedMail на email tenant'.chr(8217).'а', function () {
$ctx = makeFlowWithBalance(['balance_leads' => 0, 'balance_rub' => '100.00']);
runJob($ctx['lead']->id);
Mail::assertSent(ZeroBalancePausedMail::class, function ($mail) use ($ctx) {
return $mail->hasTo($ctx['tenant']->contact_email);
});
});
it('respects rate-limit 1 hour per tenant: 2 consecutive calls → 1 email only', function () {
$ctx1 = makeFlowWithBalance(['balance_leads' => 0, 'balance_rub' => '100.00']);
runJob($ctx1['lead']->id);
// Создаём второй lead для того же tenant'а
$ctx2lead = SupplierLead::factory()->create([
'vid' => random_int(100_000_000, 999_999_999),
'phone' => '79991234567',
'raw_payload' => ['project' => 'B1_example.com', 'phone' => '79991234567', 'time' => time()],
'supplier_project_id' => $ctx1['lead']->supplier_project_id,
'received_at' => now(),
]);
runJob($ctx2lead->id);
Mail::assertSent(ZeroBalancePausedMail::class, 1);
});
it('sends 2nd email after 65 minutes (rate-limit expired)', function () {
$ctx = makeFlowWithBalance(['balance_leads' => 0, 'balance_rub' => '100.00']);
runJob($ctx['lead']->id);
Mail::assertSent(ZeroBalancePausedMail::class, 1);
Carbon::setTestNow(now()->addMinutes(65));
Cache::store('redis')->flush(); // имитируем TTL expiry
// Реактивируем проект (real-world: admin/cron вернул is_active=true, но клиент так и не пополнил баланс).
// matchEligibleProjects() в LeadRouter фильтрует by is_active=true, иначе 2-й lead не дойдёт до handleInsufficientBalance.
// NB: Eloquent $project->update(['is_active' => true]) — no-op (in-memory attr остался true с момента ::create()),
// поэтому используем прямой UPDATE через pgsql_supplier (как делает handleInsufficientBalance при pause).
DB::connection('pgsql_supplier')
->update('UPDATE projects SET is_active = true WHERE id = ?', [$ctx['project']->id]);
$lead2 = SupplierLead::factory()->create([
'vid' => random_int(100_000_000, 999_999_999),
'phone' => '79991234567',
'raw_payload' => ['project' => 'B1_example.com', 'phone' => '79991234567', 'time' => time()],
'supplier_project_id' => $ctx['lead']->supplier_project_id,
'received_at' => now(),
]);
runJob($lead2->id);
Mail::assertSent(ZeroBalancePausedMail::class, 2);
});
it('sharing-flow isolation: tenant A on zero paused, tenant B with balance receives deal', function () {
$supplier = Supplier::where('code', 'b1')->first();
$supplierProject = SupplierProject::factory()->create([
'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'example.com',
]);
// tenantA: balance_rub > 0 (проходит WHERE EXISTS-фильтр LeadRouter), но < tier_price (500 ₽).
// Поэтому projectA попадает в matched, LedgerService падает с InsufficientBalanceException → auto-pause.
$tenantA = Tenant::factory()->create(['balance_leads' => 0, 'balance_rub' => '100.00']);
$tenantB = Tenant::factory()->create(['balance_rub' => '100000.00']);
$projectA = Project::factory()->create([
'tenant_id' => $tenantA->id, 'signal_type' => 'site', 'signal_identifier' => 'example.com',
'supplier_b1_project_id' => $supplierProject->id, 'is_active' => true,
'daily_limit_target' => 10, 'effective_daily_limit_today' => 10,
'delivered_today' => 0, 'delivery_days_mask' => 127, 'region_mask' => 255,
]);
linkProjectToSupplier($projectA, $supplierProject);
$projectB = Project::factory()->create([
'tenant_id' => $tenantB->id, 'signal_type' => 'site', 'signal_identifier' => 'example.com',
'supplier_b1_project_id' => $supplierProject->id, 'is_active' => true,
'daily_limit_target' => 10, 'effective_daily_limit_today' => 10,
'delivered_today' => 0, 'delivery_days_mask' => 127, 'region_mask' => 255,
]);
linkProjectToSupplier($projectB, $supplierProject);
$lead = SupplierLead::factory()->create([
'vid' => random_int(100_000_000, 999_999_999),
'phone' => '79991234567',
'raw_payload' => ['project' => 'B1_example.com', 'phone' => '79991234567', 'time' => time()],
'supplier_project_id' => $supplierProject->id,
'received_at' => now(),
]);
runJob($lead->id);
expect($projectA->fresh()->is_active)->toBeFalse();
expect($projectB->fresh()->is_active)->toBeTrue();
expect((string) $tenantB->fresh()->balance_rub)->toBe('99500.00');
});