2eaa78f95b
Продуктовый код (фиксы, не baseline): TenantRequisites+SupplierLead — явные @property (ide-helper:models пропускал модели); DealsController V1 — лишний ?-> на non-null received_at; ScrubPii — guard instanceof Monolog. Тест-код: ImitationTestCase @param int; findByInn return-type. Baseline перегенерён — в нём ТОЛЬКО ложноположительные (Pest TestCall + защитный ?-> на nullable first() в debug-строках ScenarioBC), 0 продуктовых подавлений (проверено диффом). composer stan: 0. NB: столбцы lead-region (dadata_qc/phone_operator/region_source/resolved_subject_code) есть в БД, но отсутствуют в db/schema.sql — отдельный дрейф схемы. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
3.2 KiB
PHP
96 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Api\V1;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Deal;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Публичный read-API сделок тенанта (G6). Аутентификация — middleware ApiKeyAuth
|
|
* (tenant_id в request->attributes['api_tenant_id']). Только сделки (deals), не
|
|
* supplier_leads.
|
|
*/
|
|
class DealsController extends Controller
|
|
{
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$tenantId = (int) $request->attributes->get('api_tenant_id');
|
|
|
|
$limit = max(1, min(500, (int) $request->query('limit', '100')));
|
|
|
|
$since = trim((string) $request->query('since', ''));
|
|
$sinceDt = null;
|
|
if ($since !== '') {
|
|
try {
|
|
$sinceDt = Carbon::parse($since);
|
|
} catch (\Throwable) {
|
|
return response()->json(['message' => 'Невалидный since.'], 422);
|
|
}
|
|
}
|
|
|
|
$cursorRaw = (string) $request->query('cursor', '');
|
|
$cursor = null;
|
|
if ($cursorRaw !== '') {
|
|
$decoded = base64_decode($cursorRaw, true);
|
|
$parsed = $decoded === false ? null : json_decode($decoded, true);
|
|
if (! is_array($parsed) || ! isset($parsed['r'], $parsed['i'])) {
|
|
return response()->json(['message' => 'Невалидный cursor.'], 422);
|
|
}
|
|
$cursor = ['r' => (string) $parsed['r'], 'i' => (int) $parsed['i']];
|
|
}
|
|
|
|
[$rows, $next] = DB::transaction(function () use ($tenantId, $limit, $sinceDt, $cursor) {
|
|
DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);
|
|
|
|
$query = Deal::query()
|
|
->where('tenant_id', $tenantId)
|
|
->with('project:id,name');
|
|
|
|
if ($sinceDt !== null) {
|
|
$query->where('received_at', '>=', $sinceDt);
|
|
}
|
|
if ($cursor !== null) {
|
|
$query->whereRaw('(received_at, id) < (?, ?)', [$cursor['r'], $cursor['i']]);
|
|
}
|
|
|
|
$rows = $query->orderByDesc('received_at')->orderByDesc('id')
|
|
->limit($limit + 1)->get();
|
|
|
|
$hasNext = $rows->count() > $limit;
|
|
if ($hasNext) {
|
|
$rows = $rows->slice(0, $limit)->values();
|
|
}
|
|
|
|
$next = null;
|
|
if ($hasNext && $rows->isNotEmpty()) {
|
|
$last = $rows->last();
|
|
$next = base64_encode((string) json_encode([
|
|
'r' => $last->received_at->toIso8601String(),
|
|
'i' => $last->id,
|
|
]));
|
|
}
|
|
|
|
return [$rows, $next];
|
|
});
|
|
|
|
return response()->json([
|
|
'data' => $rows->map(fn (Deal $d) => [
|
|
'id' => $d->id,
|
|
'received_at' => $d->received_at->toIso8601String(),
|
|
'phone' => $d->phone,
|
|
'contact_name' => $d->contact_name,
|
|
'city' => $d->city,
|
|
'status' => $d->status,
|
|
'project' => $d->project?->name,
|
|
])->all(),
|
|
'next_cursor' => $next,
|
|
]);
|
|
}
|
|
}
|