Files
portal/app/tests/Feature/Supplier/AutoPauseFlowTest.php
T

174 lines
7.1 KiB
PHP
Raw Normal View History

<?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\DuplicateDetector;
use App\Services\LeadRouter;
use App\Services\NotificationService;
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,
]);
$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(DuplicateDetector::class),
app(NotificationService::class),
app(LedgerService::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_leads' => 5, 'balance_rub' => '0.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,
]);
$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,
]);
$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((int) $tenantB->fresh()->balance_leads)->toBe(4);
});