103 lines
5.8 KiB
PHP
103 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Enums\PurchasingRole;
|
|
use App\Models\Supplier;
|
|
use App\Models\SupplierBankDetail;
|
|
use App\Models\SupplierBankReview;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class SupplierBankWorkflow
|
|
{
|
|
private function actor(User $user, Supplier $supplier): void
|
|
{
|
|
abort_unless(auth()->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<string,mixed> $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';
|
|
}
|
|
}
|