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');