From bb2e0daad8b8169bd3af73a4dfd60d22ae3e6e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 19 Jun 2026 10:13:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(G6):=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=20+=20=D1=80=D0=BE=D1=83=D1=82?= =?UTF-8?q?=20GET=20/api/v1/deals=20(=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20read-API=20=D1=81=D0=B4=D0=B5=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Controllers/Api/V1/DealsController.php | 95 +++++++++++++++++++ app/routes/web.php | 5 + 2 files changed, 100 insertions(+) create mode 100644 app/app/Http/Controllers/Api/V1/DealsController.php diff --git a/app/app/Http/Controllers/Api/V1/DealsController.php b/app/app/Http/Controllers/Api/V1/DealsController.php new file mode 100644 index 00000000..fecc5dda --- /dev/null +++ b/app/app/Http/Controllers/Api/V1/DealsController.php @@ -0,0 +1,95 @@ +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, + ]); + } +} diff --git a/app/routes/web.php b/app/routes/web.php index b911321c..5b4ee38b 100644 --- a/app/routes/web.php +++ b/app/routes/web.php @@ -220,6 +220,11 @@ Route::middleware(['auth:sanctum', 'tenant'])->prefix('/api/api-keys')->group(fu Route::post('/regenerate', 'App\Http\Controllers\Api\ApiKeyController@regenerate'); }); +// G6: публичный read-API сделок по API-ключу (middleware apikey — без sanctum/tenant). +Route::middleware('apikey')->prefix('/api/v1')->group(function () { + Route::get('/deals', 'App\Http\Controllers\Api\V1\DealsController@index')->name('api.v1.deals.index'); +}); + // Настройки исходящего webhook'а тенанта (audit D4/D5/J5). Route::middleware(['auth:sanctum', 'tenant'])->group(function () { Route::get('/api/tenants/me/webhook-settings', 'App\Http\Controllers\Api\WebhookSettingsController@show');