id() === $user->id && $user->tenant_id !== null && $supplier->tenant_id === $user->tenant_id, 403); if (app(ConflictGuard::class)->hasConflict($user->id, $supplier->id, null)) { throw ValidationException::withMessages(['bank' => 'Conflito de interesses com este fornecedor.']); } } /** @param array $data */ public function propose(User $user, Supplier $supplier, array $data): SupplierBankDetail { $this->actor($user, $supplier); abort_unless($user->purchasing_role === PurchasingRole::Procurement, 403); return DB::transaction(function () use ($user, $supplier, $data) { app(ConflictGuard::class)->lockCompany($user); $user = app(CurrentActor::class)->lock($user, [PurchasingRole::Procurement]); $supplier = Supplier::query()->lockForUpdate()->findOrFail($supplier->id); $this->actor($user, $supplier); $latest = SupplierBankDetail::query()->where('supplier_id', $supplier->id)->latest('revision')->first(); if ((int) $data['previous_id'] !== ($latest->id ?? 0)) { throw ValidationException::withMessages(['previous_id' => 'Dados bancários alterados. Reabra a ficha antes de propor outra versão.']); } $detail = SupplierBankDetail::query()->create(['tenant_id' => $user->tenant_id, 'supplier_id' => $supplier->id, 'created_by' => $user->id, 'revision' => ($latest->revision ?? 0) + 1, ...array_intersect_key($data, array_flip(['bank_name', 'country', 'currency', 'beneficiary', 'account_identifier', 'evidence_reference', 'reason'])), 'created_at' => now()]); app(RequisitionWorkflow::class)->audit($user, 'supplier.bank_proposed', null, null, ['supplier_id' => $supplier->id, 'bank_detail_id' => $detail->id, 'revision' => $detail->revision]); return $detail; }); } public function review(User $user, SupplierBankDetail $detail, string $decision, string $reason): void { abort_unless($user->purchasing_role === PurchasingRole::Finance && $detail->tenant_id === $user->tenant_id && $detail->created_by !== $user->id, 403); if (! in_array($decision, ['approved', 'rejected']) || trim($reason) === '') { throw ValidationException::withMessages(['bank' => 'Decisão e fundamento obrigatórios.']); } DB::transaction(function () use ($user, $detail, $decision, $reason) { app(ConflictGuard::class)->lockCompany($user); $user = app(CurrentActor::class)->lock($user, [PurchasingRole::Finance]); $supplier = Supplier::query()->lockForUpdate()->findOrFail($detail->supplier_id); $this->actor($user, $supplier); $latest = SupplierBankDetail::query()->where('supplier_id', $supplier->id)->latest('revision')->firstOrFail(); if ($latest->id !== $detail->id) { throw ValidationException::withMessages(['bank' => 'Versão substituída. Reveja os dados bancários actuais.']); } $existing = SupplierBankReview::query()->where('supplier_bank_detail_id', $detail->id)->where('reviewed_by', $user->id)->first(); if ($existing) { if ($existing->decision !== $decision || $existing->reason !== $reason) { throw ValidationException::withMessages(['bank' => 'A decisão anterior é imutável. Proponha nova versão para corrigir dados.']); } return; } if (! $supplier->eligible() || SupplierBankReview::query()->where('supplier_bank_detail_id', $detail->id)->where('decision', 'rejected')->exists() || SupplierBankReview::query()->where('supplier_bank_detail_id', $detail->id)->count() >= 2) { throw ValidationException::withMessages(['bank' => 'Fornecedor não elegível ou versão já decidida.']); } SupplierBankReview::query()->create(['tenant_id' => $user->tenant_id, 'supplier_bank_detail_id' => $detail->id, 'reviewed_by' => $user->id, 'decision' => $decision, 'reason' => $reason, 'created_at' => now()]); app(RequisitionWorkflow::class)->audit($user, 'supplier.bank_'.$decision, null, null, ['supplier_id' => $supplier->id, 'bank_detail_id' => $detail->id]); }); } /** Re-evaluate inside the payment transaction in the future; UI readiness is not authorisation. */ public function state(Supplier $supplier): string { $detail = SupplierBankDetail::query()->where('supplier_id', $supplier->id)->latest('revision')->first(); if (! $detail) { return 'missing'; } $reviews = SupplierBankReview::query()->where('supplier_bank_detail_id', $detail->id)->get(); if ($reviews->contains('decision', 'rejected')) { return 'rejected'; } if ($reviews->count() < 2) { return 'pending'; } if (! $supplier->eligible() || app(ConflictGuard::class)->hasConflict($detail->created_by, $supplier->id, null)) { return 'blocked'; } foreach ($reviews as $review) { $user = User::query()->where('tenant_id', $supplier->tenant_id)->find($review->reviewed_by); if (! $user || $user->purchasing_role !== PurchasingRole::Finance || $user->id === $detail->created_by || app(ConflictGuard::class)->hasConflict($user->id, $supplier->id, null)) { return 'blocked'; } } return 'approved'; } }