Files
portal/app/app/Http/Controllers/Api/AdminSuppliersController.php
T
Дмитрий 0f820c4569 feat(admin): Plan 4 Task 10 — AdminSuppliersController + AdminSupplierPricesView (B1/B2/B3 cost editor)
Backend AdminSuppliersController:
- GET /api/admin/suppliers — все 3 поставщика (B1/B2/B3).
- PATCH /api/admin/suppliers/{id} — обновляет cost_rub / quality_score / is_active.
- Validation: cost_rub >= 0, quality_score 0..9.99.
- Audit trail saas_admin_audit_log (stub admin via system-supplier@liderra.local).
- 4 Pest integration tests.

Frontend AdminSupplierPricesView (Vue 3 + Vuetify 3):
- v-data-table 3 строки с inline-editing cost_rub/quality_score/is_active.
- Forest-palette + JetBrains Mono tnum.
- 3 Vitest tests + Histoire story.

Router /admin/supplier-prices route.

Drive-by fix: SupplierProjectFactory.definition() default signal_type
ограничен ['site','call'] — иначе при ->create(['platform' => 'B1']) с
оригинальным random 'sms' нарушается CHECK chk_supplier_projects_b1_not_for_sms
(flaky parallel-pest race condition). Тесты, которым нужен 'sms', продолжают
явно передавать signal_type вместе с B2/B3.

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

105 lines
4.0 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);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\SaasAdminAuditLog;
use App\Models\Supplier;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* SaaS-admin CRUD для suppliers (B1/B2/B3 закупочные цены).
*
* Spec: docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md §6.2.
* Без auth-middleware на MVP (паритет с AdminPricingTiersController; gated на Б-1).
* Audit trail в saas_admin_audit_log (action='suppliers.update').
*
* Reuses pattern from AdminPricingTiersController::resolveAdminUserId(), но с
* системным стабом 'system-supplier@liderra.local' (отдельный actor для
* фильтрации audit-event'ов по этой подсистеме).
*/
final class AdminSuppliersController extends Controller
{
/** GET /api/admin/suppliers */
public function index(): JsonResponse
{
return response()->json([
'data' => Supplier::query()->orderBy('sort_order')->get(),
]);
}
/** PATCH /api/admin/suppliers/{id} */
public function update(Request $request, int $id): JsonResponse
{
$request->validate([
'cost_rub' => ['sometimes', 'numeric', 'min:0'],
'quality_score' => ['sometimes', 'numeric', 'between:0,9.99'],
'is_active' => ['sometimes', 'boolean'],
]);
$supplier = Supplier::findOrFail($id);
/** @var array<string, mixed> $changes */
$changes = $request->only(['cost_rub', 'quality_score', 'is_active']);
DB::transaction(function () use ($supplier, $changes, $request): void {
/** @var array<string, mixed> $before */
$before = $supplier->only(array_keys($changes));
$supplier->update($changes);
SaasAdminAuditLog::create([
'admin_user_id' => $this->resolveAdminUserId($request),
'action' => 'suppliers.update',
'target_type' => 'suppliers',
'target_id' => $supplier->id,
'payload_before' => $before,
'payload_after' => $changes,
'reason' => 'Supplier cost/quality update via admin UI.',
'ip_address' => $request->ip() ?? '127.0.0.1',
'user_agent' => $request->userAgent(),
]);
});
return response()->json(['data' => $supplier->fresh()]);
}
/**
* Резолвит admin_user_id: из request input (на MVP) либо создаёт
* системный стаб-аккаунт `system-supplier@liderra.local`, чтобы соблюсти
* NOT NULL constraint + FK на saas_admin_users.
*
* Идентичная логика — AdminPricingTiersController::resolveAdminUserId();
* отличается только email системного стаба (для фильтрации audit-event'ов).
*/
private function resolveAdminUserId(Request $request): int
{
$requested = $request->input('admin_user_id');
if (is_int($requested) || (is_string($requested) && ctype_digit($requested))) {
$existing = DB::table('saas_admin_users')->where('id', (int) $requested)->value('id');
if ($existing !== null) {
return (int) $existing;
}
}
$existingId = DB::table('saas_admin_users')
->where('email', 'system-supplier@liderra.local')
->value('id');
if ($existingId !== null) {
return (int) $existingId;
}
return (int) DB::table('saas_admin_users')->insertGetId([
'email' => 'system-supplier@liderra.local',
'full_name' => 'System Supplier Bot',
'password_hash' => '$2y$04$system-stub-not-loginable',
'role' => 'super_admin',
'is_active' => false,
'sso_provider' => 'local',
'is_break_glass' => false,
]);
}
}