Files
portal/app/tests/Feature/Models/ProjectExtensionsTest.php
T
Дмитрий 99afcbc25c feat(models): extend Project with signal_type, sms_senders, supplier_b1/b2/b3 relations + scopes
- app/Models/Project.php — добавлены fillable+casts для supplier integration:
  signal_type, signal_identifier, sms_senders (jsonb array), sms_keyword,
  delivered_in_month, supplier_b{1,2,3}_project_id.
  + supplierB1/B2/B3() BelongsTo relations на SupplierProject (sharing-model).
  + scopeActiveOnDay($iso) — bitmask проверка по delivery_days_mask
    (bit 0 = Mon, bit 6 = Sun; ISO=1 → 1<<0 = 1; ISO=7 → 1<<6 = 64).
  + scopeForSignal($type, $identifier) — фильтр по сигналу (для роутинга в Plan 2).
- database/factories/ProjectFactory.php — defaults null/0 для новых полей
  (CHECK constraints не нарушаются: signal_type IS NULL → остальные опциональны).
  + state-методы asSiteSignal($domain), asCallSignal($phone), asSmsSignal($senders, $keyword).
- tests/Feature/Models/ProjectExtensionsTest.php — 6 тестов: signal_type fillable,
  sms_senders array cast + sms_keyword, SMS без keyword, supplierB1/B2/B3 relations,
  scopeActiveOnDay (bitmask Mon/Sat), scopeForSignal (3 сигнала + edge-case).

Pest: 469 / 467 passed / 2 skipped (461 + 6 новых = 467, с retry на transient
PG connection issues — на параллельных тестах с testing_rls_user GRANT тяжёл).
Larastan: 0 errors. Pint passed.

Spec: §2.1
Plan: Task 10

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:59:53 +03:00

107 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
test('Project has signal_type, signal_identifier in fillable', function () {
$tenant = Tenant::factory()->create();
DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
$p = Project::factory()->for($tenant)->asSiteSignal('example.com')->create();
expect($p->signal_type)->toBe('site');
expect($p->signal_identifier)->toBe('example.com');
});
test('Project casts sms_senders as array + sms_keyword stored', function () {
$tenant = Tenant::factory()->create();
DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
$p = Project::factory()
->for($tenant)
->asSmsSignal(['TINKOFF', 'SBERBANK'], 'ипотека')
->create();
expect($p->fresh()->sms_senders)->toBe(['TINKOFF', 'SBERBANK']);
expect($p->fresh()->sms_keyword)->toBe('ипотека');
});
test('Project SMS without keyword stores null keyword', function () {
$tenant = Tenant::factory()->create();
DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
$p = Project::factory()
->for($tenant)
->asSmsSignal(['TINKOFF'], null)
->create();
expect($p->fresh()->sms_keyword)->toBeNull();
expect($p->fresh()->sms_senders)->toBe(['TINKOFF']);
});
test('Project has supplierB1/B2/B3 relations', function () {
$tenant = Tenant::factory()->create();
DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
$sp = SupplierProject::factory()->create(['platform' => 'B1']);
$p = Project::factory()->for($tenant)->create(['supplier_b1_project_id' => $sp->id]);
expect($p->supplierB1)->toBeInstanceOf(SupplierProject::class);
expect($p->supplierB1->id)->toBe($sp->id);
expect($p->supplierB2)->toBeNull();
expect($p->supplierB3)->toBeNull();
});
test('Project scopeActiveOnDay returns only projects with today bit set in delivery_days_mask', function () {
$tenant = Tenant::factory()->create();
DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
// Все 7 дней (битмаска 0b1111111 = 127)
Project::factory()->for($tenant)->create([
'is_active' => true,
'delivery_days_mask' => 0b1111111,
]);
// Только Сб+Вс (биты 5,6 = 0b1100000 = 96)
Project::factory()->for($tenant)->create([
'is_active' => true,
'delivery_days_mask' => 0b1100000,
]);
// Неактивный — не попадает независимо от маски
Project::factory()->for($tenant)->create([
'is_active' => false,
'delivery_days_mask' => 0b1111111,
]);
$todayDow = (int) now()->dayOfWeekIso;
// Проект на все дни должен пройти на любой день
$count = Project::activeOnDay($todayDow)->count();
expect($count)->toBeGreaterThanOrEqual(1);
// Понедельник (ISO=1, bit 0): проходит только проект на все дни
expect(Project::activeOnDay(1)->count())->toBe(1);
// Суббота (ISO=6, bit 5): проходят оба активных
expect(Project::activeOnDay(6)->count())->toBe(2);
});
test('Project scopeForSignal filters by (signal_type, signal_identifier)', function () {
$tenant = Tenant::factory()->create();
DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
Project::factory()->for($tenant)->asSiteSignal('a.com')->create();
Project::factory()->for($tenant)->asSiteSignal('b.com')->create();
Project::factory()->for($tenant)->asCallSignal('79991234567')->create();
expect(Project::forSignal('site', 'a.com')->count())->toBe(1);
expect(Project::forSignal('site', 'b.com')->count())->toBe(1);
expect(Project::forSignal('call', '79991234567')->count())->toBe(1);
expect(Project::forSignal('site', 'nonexistent.com')->count())->toBe(0);
});