phase2(supplier-resolver): вынос дублированной логики lookup в App\Services\SupplierResolver
Закрыт TODO (a) из v1.49: общая логика выбора активного supplier'а через
project_suppliers m2m была дублирована между ProcessWebhookJob (webhook-flow)
и DealController (manual-create) — 11 одинаковых строк query-builder'а на
2 файла. Теперь — единственный источник истины + DI через app() (тот же
паттерн, что у DuplicateDetector в v1.23).
App\Services\SupplierResolver:
- resolveForProject(Project): ?int — точная копия прежней query
(project_suppliers JOIN suppliers, is_active+is_active, ORDER BY
sort_order, id).
- costRubSnapshot(int $supplierId): string — вынесенный snapshot цены
для записи в supplier_lead_costs.
ProcessWebhookJob и DealController:
- Удалены private resolveSupplierId() (по 14 строк).
- Удалены локальные DB::table('suppliers')->value('cost_rub').
- Используют app(SupplierResolver::class) внутри handle()/store().
Pest +8 в tests/Feature/Services/SupplierResolverTest.php:
- null без связей / единственный активный / пропуск inactive supplier /
пропуск inactive m2m / ORDER BY sort_order / null если все inactive /
изоляция по project_id / costRubSnapshot формат '137.50'.
PHPStan baseline регенерирован.
Регресс:
- Pint + PHPStan passed (baseline регенерирован).
- Pest 174/174 за 21.46 сек (+8 от 166, 708 assertions).
- Vitest 247/247 за 17.53 сек (нетронут — backend-only refactor).
Реестр v1.58→v1.59 / CLAUDE.md v1.49→v1.50.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ use App\Models\Project;
|
||||
use App\Models\SupplierLeadCost;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\SupplierResolver;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
@@ -101,17 +102,14 @@ class DealController extends Controller
|
||||
// баланс (Ю-2: реселлерская модель работает только при закупке у
|
||||
// supplier'а), но cost-аналитика всё равно нужна — owner проекта
|
||||
// мог самостоятельно купить лид и ввести руками.
|
||||
$supplierId = $this->resolveSupplierId($project);
|
||||
$resolver = app(SupplierResolver::class);
|
||||
$supplierId = $resolver->resolveForProject($project);
|
||||
if ($supplierId !== null) {
|
||||
$costRub = (string) DB::table('suppliers')
|
||||
->where('id', $supplierId)
|
||||
->value('cost_rub');
|
||||
|
||||
SupplierLeadCost::create([
|
||||
'deal_id' => $deal->id,
|
||||
'received_at' => $deal->received_at,
|
||||
'supplier_id' => $supplierId,
|
||||
'cost_rub' => $costRub,
|
||||
'cost_rub' => $resolver->costRubSnapshot($supplierId),
|
||||
'supplier_lead_id' => null, // manual: нет внешнего id
|
||||
'created_at' => now(),
|
||||
]);
|
||||
@@ -219,25 +217,4 @@ class DealController extends Controller
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск активного supplier_id для проекта через project_suppliers m2m.
|
||||
* Дублирует логику ProcessWebhookJob::resolveSupplierId — на MVP
|
||||
* приемлемо; рефактор в `App\Services\SupplierResolver` — отдельный коммит
|
||||
* (тогда Job и Controller будут шарить логику + system_settings fallback).
|
||||
*/
|
||||
private function resolveSupplierId(Project $project): ?int
|
||||
{
|
||||
$row = DB::table('project_suppliers')
|
||||
->join('suppliers', 'suppliers.id', '=', 'project_suppliers.supplier_id')
|
||||
->where('project_suppliers.project_id', $project->id)
|
||||
->where('project_suppliers.is_active', true)
|
||||
->where('suppliers.is_active', true)
|
||||
->orderBy('suppliers.sort_order')
|
||||
->orderBy('suppliers.id')
|
||||
->select('suppliers.id')
|
||||
->first();
|
||||
|
||||
return $row !== null ? (int) $row->id : null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user