create([ 'tenant_id' => $tenant->id, 'project_id' => $project->id, 'phone' => $phone, 'phones' => ['+79995556677'], 'contact_name' => 'Иван', 'received_at' => now(), 'deleted_at' => $deletedAt, ]); } /** Сырое чтение сделки без soft-delete scope (PDO делится через SharesSupplierPdo). */ function rawDeal(int $id): ?object { return DB::connection('pgsql_supplier')->table('deals')->where('id', $id)->first(); } it('анонимизирует ПДн soft-deleted сделок старше окна, не трогая свежие и живые', function () { SystemSetting::create(['key' => 'pd_scrub_soft_deleted_deals_days', 'value' => '30', 'type' => 'int']); $tenant = Tenant::factory()->create(); DB::statement('SET app.current_tenant_id = '.$tenant->id); $project = Project::factory()->create(['tenant_id' => $tenant->id]); $old = makeRetentionDeal($tenant, $project, now()->subDays(40)); // старше окна → чистим $fresh = makeRetentionDeal($tenant, $project, now()->subDays(5)); // в окне → не трогаем $live = makeRetentionDeal($tenant, $project, null); // живой → не трогаем $this->artisan('pd:scrub-soft-deleted-deals')->assertExitCode(0); $oldRow = rawDeal($old->id); expect($oldRow->phone)->toBe('+7000XXXXXXX'); expect($oldRow->contact_name)->toBe('Удалено'); expect($oldRow->phones)->toBeNull(); expect(rawDeal($fresh->id)->phone)->toBe('+79991112233'); expect(rawDeal($live->id)->phone)->toBe('+79991112233'); }); it('no-op если retention не настроен (ключ отсутствует)', function () { $tenant = Tenant::factory()->create(); DB::statement('SET app.current_tenant_id = '.$tenant->id); $project = Project::factory()->create(['tenant_id' => $tenant->id]); $old = makeRetentionDeal($tenant, $project, now()->subDays(40)); $this->artisan('pd:scrub-soft-deleted-deals')->assertExitCode(0); expect(rawDeal($old->id)->phone)->toBe('+79991112233'); }); it('идемпотентна: повторный прогон ничего не меняет', function () { SystemSetting::create(['key' => 'pd_scrub_soft_deleted_deals_days', 'value' => '30', 'type' => 'int']); $tenant = Tenant::factory()->create(); DB::statement('SET app.current_tenant_id = '.$tenant->id); $project = Project::factory()->create(['tenant_id' => $tenant->id]); $old = makeRetentionDeal($tenant, $project, now()->subDays(40)); $this->artisan('pd:scrub-soft-deleted-deals')->assertExitCode(0); $this->artisan('pd:scrub-soft-deleted-deals')->assertExitCode(0); expect(rawDeal($old->id)->phone)->toBe('+7000XXXXXXX'); }); it('пишет запись в pd_processing_log при анонимизации (системный актор)', function () { SystemSetting::create(['key' => 'pd_scrub_soft_deleted_deals_days', 'value' => '30', 'type' => 'int']); $tenant = Tenant::factory()->create(); DB::statement('SET app.current_tenant_id = '.$tenant->id); $project = Project::factory()->create(['tenant_id' => $tenant->id]); makeRetentionDeal($tenant, $project, now()->subDays(40)); $this->artisan('pd:scrub-soft-deleted-deals')->assertExitCode(0); $log = DB::connection('pgsql_supplier')->table('pd_processing_log') ->where('action', 'deleted') ->where('purpose', '152-FZ retention scrub') ->first(); expect($log)->not->toBeNull(); expect($log->actor_admin_user_id)->toBeNull(); expect($log->actor_tenant_user_id)->toBeNull(); });