From 791bc1bfae1b0e17f1cdcac5dc2b0b846b409f8f 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, 22 May 2026 16:09:37 +0300 Subject: [PATCH] =?UTF-8?q?feat(pd):=20pd=5Fprocessing=5Flog=20'created'?= =?UTF-8?q?=20on=20historical=20import=20(152-=D0=A4=D0=97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Import/HistoricalImportService.php | 19 ++- app/tests/Feature/Pd/DealImportPdLogTest.php | 121 ++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 app/tests/Feature/Pd/DealImportPdLogTest.php diff --git a/app/app/Services/Import/HistoricalImportService.php b/app/app/Services/Import/HistoricalImportService.php index 72d7ad5d..eca6158a 100644 --- a/app/app/Services/Import/HistoricalImportService.php +++ b/app/app/Services/Import/HistoricalImportService.php @@ -10,6 +10,7 @@ use App\Models\ImportUnknownStatus; use App\Models\Project; use App\Models\Reminder; use App\Services\MonthlyPartitionManager; +use App\Services\Pd\PdAuditLogger; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Throwable; @@ -26,6 +27,7 @@ final class HistoricalImportService public function __construct( private readonly MonthlyPartitionManager $partitions, private readonly StatusRuToSlugMapper $statusMapper, + private readonly PdAuditLogger $pdLog, ) {} /** @@ -68,7 +70,7 @@ final class HistoricalImportService } try { - $wasCreated = $this->upsertRow($tenantId, $userId, $row, $slug); + $wasCreated = $this->upsertRow($tenantId, $userId, $row, $slug, $log->id); $wasCreated ? $added++ : $updated++; } catch (Throwable $e) { $skipped++; @@ -132,9 +134,9 @@ final class HistoricalImportService * Идемпотентный upsert одной строки в собственной транзакции. * Возвращает true — создана новая сделка, false — обновлена существующая. */ - private function upsertRow(int $tenantId, int $userId, ParsedLeadRow $row, string $slug): bool + private function upsertRow(int $tenantId, int $userId, ParsedLeadRow $row, string $slug, int $importLogId): bool { - return DB::transaction(function () use ($tenantId, $userId, $row, $slug): bool { + return DB::transaction(function () use ($tenantId, $userId, $row, $slug, $importLogId): bool { DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId); $project = Project::firstOrCreate( @@ -188,6 +190,17 @@ final class HistoricalImportService $this->syncReminder($tenantId, $userId, $deal, $row); + $this->pdLog->record( + action: 'created', + subjectType: 'lead', + subjectId: $deal->id, + purpose: 'lead_create_import_'.$importLogId, + tenantId: $tenantId, + actorTenantUserId: $userId, + actorAdminUserId: null, + ip: null, + ); + return true; }); } diff --git a/app/tests/Feature/Pd/DealImportPdLogTest.php b/app/tests/Feature/Pd/DealImportPdLogTest.php new file mode 100644 index 00000000..1339e56f --- /dev/null +++ b/app/tests/Feature/Pd/DealImportPdLogTest.php @@ -0,0 +1,121 @@ +create(); + $user = User::factory()->for($tenant)->create(); + DB::statement('SET app.current_tenant_id = '.$tenant->id); + + $log = ImportLog::create([ + 'tenant_id' => $tenant->id, + 'user_id' => $user->id, + 'filename' => 'leads.csv', + 'file_path' => 'imports/x.csv', + 'dry_run' => false, + ]); + + $header = "\xEF\xBB\xBF".'id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя'; + $rows = array_merge( + (new CsvLeadsParser)->parse($header."\n".'9901,Окна,окна,79161000001,2023/07/10 10:00:00,,,Новые,')->rows, + (new CsvLeadsParser)->parse($header."\n".'9902,Окна,окна,79161000002,2023/07/10 10:00:00,,,Новые,')->rows, + ); + + app(HistoricalImportService::class)->import($tenant->id, $user->id, $log, $rows); + + $pd = DB::table('pd_processing_log') + ->where('action', 'created') + ->where('purpose', 'lead_create_import_'.$log->id) + ->get(); + + expect($pd)->toHaveCount(2); + + foreach ($pd as $r) { + expect($r->subject_type)->toBe('lead') + ->and((int) $r->actor_tenant_user_id)->toBe($user->id) + ->and($r->actor_admin_user_id)->toBeNull() + ->and($r->subject_id)->not->toBeNull() + ->and((int) $r->tenant_id)->toBe($tenant->id); + } +}); + +it('does NOT write pd_processing_log on dry_run import', function () { + $tenant = Tenant::factory()->create(); + $user = User::factory()->for($tenant)->create(); + DB::statement('SET app.current_tenant_id = '.$tenant->id); + + $log = ImportLog::create([ + 'tenant_id' => $tenant->id, + 'user_id' => $user->id, + 'filename' => 'leads.csv', + 'file_path' => 'imports/x.csv', + 'dry_run' => true, + ]); + + $header = "\xEF\xBB\xBF".'id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя'; + $rows = (new CsvLeadsParser)->parse($header."\n".'9903,Окна,окна,79161000003,2023/07/10 10:00:00,,,Новые,')->rows; + + app(HistoricalImportService::class)->import($tenant->id, $user->id, $log, $rows); + + $count = DB::table('pd_processing_log') + ->where('purpose', 'lead_create_import_'.$log->id) + ->count(); + + expect($count)->toBe(0); +}); + +it('does NOT write pd_processing_log on import UPDATE (idempotent re-import)', function () { + $tenant = Tenant::factory()->create(); + $user = User::factory()->for($tenant)->create(); + DB::statement('SET app.current_tenant_id = '.$tenant->id); + + $header = "\xEF\xBB\xBF".'id,Проект,Тег проекта,Телефон,Создано,Напоминание,Комментарий,Состояние,Имя'; + + // First import — creates the deal + $log1 = ImportLog::create([ + 'tenant_id' => $tenant->id, + 'user_id' => $user->id, + 'filename' => 'leads.csv', + 'file_path' => 'imports/x.csv', + 'dry_run' => false, + ]); + $rows1 = (new CsvLeadsParser)->parse($header."\n".'9904,Окна,окна,79161000004,2023/07/10 10:00:00,,,Новые,')->rows; + app(HistoricalImportService::class)->import($tenant->id, $user->id, $log1, $rows1); + + // Second import — updates the same deal + $log2 = ImportLog::create([ + 'tenant_id' => $tenant->id, + 'user_id' => $user->id, + 'filename' => 'leads2.csv', + 'file_path' => 'imports/x2.csv', + 'dry_run' => false, + ]); + $rows2 = (new CsvLeadsParser)->parse($header."\n".'9904,Окна,окна,79161000004,2023/07/10 10:00:00,,,Оплачено,')->rows; + app(HistoricalImportService::class)->import($tenant->id, $user->id, $log2, $rows2); + + // Only the first import wrote a pd log entry + $countLog1 = DB::table('pd_processing_log') + ->where('action', 'created') + ->where('purpose', 'lead_create_import_'.$log1->id) + ->count(); + $countLog2 = DB::table('pd_processing_log') + ->where('action', 'created') + ->where('purpose', 'lead_create_import_'.$log2->id) + ->count(); + + expect($countLog1)->toBe(1) + ->and($countLog2)->toBe(0); +});