user()->tenant_id; $file = $request->file('file'); $storedName = Str::uuid()->toString().'.csv'; $path = $file->storeAs("imports/{$tenantId}", $storedName, 'local'); $log = ImportLog::create([ 'tenant_id' => $tenantId, 'user_id' => $request->user()->id, 'filename' => $file->getClientOriginalName(), 'file_path' => $path, 'status' => 'pending', 'entity_type' => 'leads', 'source_system' => 'crm.bp-gr.ru', 'dry_run' => $request->boolean('dry_run'), ]); ImportLeadsJob::dispatch($log->id, $tenantId); return response()->json(['data' => $this->toResource($log)], 201); } /** * GET /api/imports — история импортов тенанта (RLS отфильтрует по tenant). * * Defense-in-depth: явный where(tenant_id) поверх RLS — на dev через * `postgres` superuser RLS обходится BYPASSRLS, app-фильтр гарантирует * изоляцию (паттерн из DealController). */ public function index(Request $request): JsonResponse { $tenantId = (int) $request->user()->tenant_id; $logs = ImportLog::query() ->where('tenant_id', $tenantId) ->orderByDesc('id') ->limit(50) ->get() ->map(fn (ImportLog $log) => $this->toResource($log)); return response()->json(['data' => $logs]); } /** * GET /api/imports/{importLog} — прогресс одного импорта (для polling'а). * * Defense-in-depth: явная проверка tenant_id на принадлежность поверх RLS. */ public function show(Request $request, ImportLog $importLog): JsonResponse { $tenantId = (int) $request->user()->tenant_id; abort_if($importLog->tenant_id !== $tenantId, 403, 'Доступ к импорту другого тенанта запрещён.'); return response()->json(['data' => $this->toResource($importLog)]); } /** * GET /api/imports/unknown-statuses — незамапленные статусы (вход wizard'а §6.6). * * Defense-in-depth: явный where(tenant_id) поверх RLS. */ public function unknownStatuses(Request $request): JsonResponse { $tenantId = (int) $request->user()->tenant_id; $rows = ImportUnknownStatus::query() ->where('tenant_id', $tenantId) ->unresolved() ->orderByDesc('occurrences') ->get() ->map(fn (ImportUnknownStatus $s) => [ 'id' => $s->id, 'status_ru' => $s->status_ru, 'occurrences' => $s->occurrences, ]); return response()->json(['data' => $rows]); } /** * POST /api/imports/unknown-statuses/resolve — ручной маппинг статусов. * * Defense-in-depth: явный where(tenant_id) поверх RLS. */ public function resolveUnknownStatuses(ResolveUnknownStatusesRequest $request): JsonResponse { $tenantId = (int) $request->user()->tenant_id; $userId = (int) $request->user()->id; DB::transaction(function () use ($request, $tenantId, $userId): void { DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId); foreach ($request->validated()['mappings'] as $mapping) { ImportUnknownStatus::query() ->where('tenant_id', $tenantId) ->where('status_ru', $mapping['status_ru']) ->update([ 'mapped_to_slug' => $mapping['slug'], 'resolved_at' => now(), 'resolved_by' => $userId, ]); } }); return response()->json(['data' => ['resolved' => count($request->validated()['mappings'])]]); } /** * @return array */ private function toResource(ImportLog $log): array { return [ 'id' => $log->id, 'filename' => $log->filename, 'status' => $log->status, 'rows_total' => $log->rows_total, 'rows_added' => $log->rows_added, 'rows_updated' => $log->rows_updated, 'rows_skipped' => $log->rows_skipped, 'unknown_statuses_count' => $log->unknown_statuses_count, 'dry_run' => $log->dry_run, 'error_message' => $log->error_message, 'started_at' => $log->started_at?->toIso8601String(), 'finished_at' => $log->finished_at?->toIso8601String(), ]; } }