diff --git a/app/app/Jobs/Supplier/DeleteSupplierProjectJob.php b/app/app/Jobs/Supplier/DeleteSupplierProjectJob.php new file mode 100644 index 00000000..4652738f --- /dev/null +++ b/app/app/Jobs/Supplier/DeleteSupplierProjectJob.php @@ -0,0 +1,80 @@ + $supplierProjectIds */ + public function __construct(public array $supplierProjectIds) {} + + public function handle(SupplierPortalClient $client): void + { + $needsResync = false; + + foreach ($this->supplierProjectIds as $id) { + $sp = SupplierProject::on(self::DB_CONNECTION)->find($id); + if ($sp === null) { + continue; + } + + $remaining = DB::connection(self::DB_CONNECTION) + ->table('project_supplier_links') + ->where('supplier_project_id', $id) + ->count(); + + if ($remaining > 0) { + $needsResync = true; + + continue; + } + + if ($sp->supplier_external_id !== null && $sp->supplier_external_id !== '') { + try { + $client->deleteProject((int) $sp->supplier_external_id); + } catch (Throwable $e) { + Log::warning('supplier.delete_donor_failed', [ + 'supplier_project_id' => $id, 'error' => $e->getMessage(), + ]); + throw $e; // retry the job + } + } + + $sp->delete(); + } + + if ($needsResync) { + SyncSupplierProjectsJob::dispatch(); + } + } +} diff --git a/app/phpstan-baseline.neon b/app/phpstan-baseline.neon index f1c480f9..e93cb016 100644 --- a/app/phpstan-baseline.neon +++ b/app/phpstan-baseline.neon @@ -1938,6 +1938,12 @@ parameters: count: 2 path: tests/Feature/Supplier/FailoverProjectChannelLiveSmokeTest.php + - + message: '#^Call to an undefined method Mockery\\ExpectationInterface\|Mockery\\HigherOrderMessage\:\:once\(\)\.$#' + identifier: method.notFound + count: 1 + path: tests/Feature/Supplier/DeleteSupplierProjectJobTest.php + - message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:artisan\(\)\.$#' identifier: method.notFound diff --git a/app/tests/Feature/Supplier/DeleteSupplierProjectJobTest.php b/app/tests/Feature/Supplier/DeleteSupplierProjectJobTest.php new file mode 100644 index 00000000..7fa8275c --- /dev/null +++ b/app/tests/Feature/Supplier/DeleteSupplierProjectJobTest.php @@ -0,0 +1,58 @@ +create([ + 'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79991110000', + 'supplier_external_id' => '555', 'current_limit' => 1, + ]); + + $mock = Mockery::mock(SupplierPortalClient::class); + $mock->shouldReceive('deleteProject')->once()->with(555); + app()->instance(SupplierPortalClient::class, $mock); + + (new DeleteSupplierProjectJob([$sp->id]))->handle(app(SupplierPortalClient::class)); + + expect(SupplierProject::find($sp->id))->toBeNull(); +}); + +it('does NOT delete donor at supplier when other consumers remain; re-syncs', function (): void { + Bus::fake([SyncSupplierProjectsJob::class]); + + $tenant = Tenant::factory()->create(['balance_leads' => 100]); + $sp = SupplierProject::query()->create([ + 'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79991110001', + 'supplier_external_id' => '556', 'current_limit' => 1, + ]); + $other = Project::factory()->create(['tenant_id' => $tenant->id]); + + DB::table('project_supplier_links')->insert([ + 'project_id' => $other->id, + 'supplier_project_id' => $sp->id, + 'platform' => 'B1', + 'subject_code' => null, + ]); + + $mock = Mockery::mock(SupplierPortalClient::class); + $mock->shouldNotReceive('deleteProject'); + app()->instance(SupplierPortalClient::class, $mock); + + (new DeleteSupplierProjectJob([$sp->id]))->handle(app(SupplierPortalClient::class)); + + expect(SupplierProject::find($sp->id))->not->toBeNull(); + Bus::assertDispatched(SyncSupplierProjectsJob::class); +});