001d7819bf
Code-review subagent (CV.12 в Plan 1) нашёл 1 BLOCKER + 2 actionable WARNINGs:
1. **BLOCKER** — projects.supplier_b{1,2,3}_project_id были голыми BIGINT без
REFERENCES, вопреки явному комментарию «FK добавятся в Task 2». Task 2
создал supplier_projects, но FK на projects не вернул. Можно было записать
произвольный BIGINT в эти колонки.
Fix: ALTER TABLE projects ADD CONSTRAINT … FOREIGN KEY … ON DELETE SET NULL
для всех трёх + 3 partial index (WHERE NOT NULL) для FK lookup.
2. **WARNING** (Project-level B1+SMS guard) — CHECK существовал только на
supplier_projects; Project::create(['signal_type'=>'sms','supplier_b1_project_id'=>…])
проходил вопреки spec §2.2 «B1 не поддерживает СМС».
Fix: ADD CONSTRAINT chk_projects_b1_not_for_sms
CHECK (signal_type <> 'sms' OR supplier_b1_project_id IS NULL).
3. **WARNING** (resolver collision) — SupplierProjectResolver::resolveOrStub
firstOrCreate на (platform, unique_key) без signal_type → при коллизии
unique_key возвращал чужую запись с другим signal_type без ошибки.
Fix: после firstOrCreate проверяется match signal_type, иначе DomainException.
+1 тест на collision.
Schema bumped v8.16 → v8.17. Метрики: 60 таблиц / 111 индексов (+3) / 39 RLS.
Pest: 500/498 passed (+1 collision test). Larastan 0 errors. Pint clean.
Spec: §2.1, §2.2
Plan: Task 2 (закрытие code-review CV.12)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.7 KiB
PHP
76 lines
2.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\SupplierProject;
|
|
use App\Services\SupplierProjects\SupplierProjectResolver;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
test('resolveOrStub returns existing supplier_project for matching (platform, unique_key)', function () {
|
|
$existing = SupplierProject::factory()->create([
|
|
'platform' => 'B1',
|
|
'signal_type' => 'site',
|
|
'unique_key' => 'example.com',
|
|
]);
|
|
|
|
$resolver = new SupplierProjectResolver;
|
|
$resolved = $resolver->resolveOrStub('B1', 'site', 'example.com');
|
|
|
|
expect($resolved->id)->toBe($existing->id);
|
|
});
|
|
|
|
test('resolveOrStub creates pending stub when no existing project', function () {
|
|
$resolver = new SupplierProjectResolver;
|
|
$resolved = $resolver->resolveOrStub('B2', 'call', '79991234567');
|
|
|
|
expect($resolved->exists)->toBeTrue();
|
|
expect($resolved->platform)->toBe('B2');
|
|
expect($resolved->signal_type)->toBe('call');
|
|
expect($resolved->unique_key)->toBe('79991234567');
|
|
expect($resolved->sync_status)->toBe('pending');
|
|
expect($resolved->current_limit)->toBe(0);
|
|
expect($resolved->current_workdays)->toBe([1, 2, 3, 4, 5, 6, 7]);
|
|
});
|
|
|
|
test('resolveOrStub returns same row on second call (no duplicates)', function () {
|
|
$resolver = new SupplierProjectResolver;
|
|
$first = $resolver->resolveOrStub('B3', 'sms', 'TINKOFF');
|
|
$second = $resolver->resolveOrStub('B3', 'sms', 'TINKOFF');
|
|
|
|
expect($first->id)->toBe($second->id);
|
|
expect(SupplierProject::where('unique_key', 'TINKOFF')->count())->toBe(1);
|
|
});
|
|
|
|
test('resolveOrStub throws DomainException for B1+sms (forbidden combo)', function () {
|
|
$resolver = new SupplierProjectResolver;
|
|
expect(fn () => $resolver->resolveOrStub('B1', 'sms', 'TINKOFF'))
|
|
->toThrow(DomainException::class);
|
|
});
|
|
|
|
test('resolveOrStub throws InvalidArgumentException for invalid platform', function () {
|
|
$resolver = new SupplierProjectResolver;
|
|
expect(fn () => $resolver->resolveOrStub('B9', 'site', 'a.com'))
|
|
->toThrow(InvalidArgumentException::class);
|
|
});
|
|
|
|
test('resolveOrStub throws InvalidArgumentException for invalid signal_type', function () {
|
|
$resolver = new SupplierProjectResolver;
|
|
expect(fn () => $resolver->resolveOrStub('B1', 'unknown_type', 'a.com'))
|
|
->toThrow(InvalidArgumentException::class);
|
|
});
|
|
|
|
test('resolveOrStub throws DomainException when unique_key collides across signal_types', function () {
|
|
SupplierProject::factory()->create([
|
|
'platform' => 'B2',
|
|
'signal_type' => 'sms',
|
|
'unique_key' => 'TINKOFF',
|
|
]);
|
|
|
|
$resolver = new SupplierProjectResolver;
|
|
|
|
expect(fn () => $resolver->resolveOrStub('B2', 'site', 'TINKOFF'))
|
|
->toThrow(DomainException::class);
|
|
});
|