0f820c4569
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>
105 lines
4.0 KiB
PHP
105 lines
4.0 KiB
PHP
<?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,
|
||
]);
|
||
}
|
||
}
|