126 lines
5.4 KiB
PHP
126 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\ConflictDeclaration;
|
|
use App\Models\ProposalEvaluation;
|
|
use App\Models\PurchaseAward;
|
|
use App\Models\PurchaseAwardApproval;
|
|
use App\Models\Quotation;
|
|
use App\Models\Requisition;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class ConflictGuard
|
|
{
|
|
/** Call first inside a transaction, before locking a requisition or supplier. */
|
|
public function lockCompany(User $user): void
|
|
{
|
|
Tenant::query()->whereKey($user->tenant_id)->lockForUpdate()->firstOrFail();
|
|
}
|
|
|
|
public function hasConflict(int $userId, ?int $supplierId, ?int $requisitionId): bool
|
|
{
|
|
return ConflictDeclaration::query()->where('user_id', $userId)->where('has_conflict', true)
|
|
->where(function ($scopes) use ($supplierId, $requisitionId) {
|
|
// Supplier-wide statements apply only once that supplier is known.
|
|
$scopes->where(function ($supplierScope) use ($supplierId) {
|
|
$supplierScope->whereNull('requisition_id')->where('supplier_id', $supplierId ?? 0);
|
|
});
|
|
if ($requisitionId !== null) {
|
|
$scopes->orWhere(function ($processScope) use ($supplierId, $requisitionId) {
|
|
$processScope->where('requisition_id', $requisitionId)->where(function ($supplier) use ($supplierId) {
|
|
$supplier->whereNull('supplier_id');
|
|
if ($supplierId !== null) {
|
|
$supplier->orWhere('supplier_id', $supplierId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
})->exists();
|
|
}
|
|
|
|
public function participationBlocked(User $user, Requisition $requisition): bool
|
|
{
|
|
if ($this->hasConflict($user->id, null, $requisition->id)) {
|
|
return true;
|
|
}
|
|
foreach ($this->participants($requisition) as $userId) {
|
|
if ($this->hasConflict($userId, null, $requisition->id)) {
|
|
return true;
|
|
}
|
|
}
|
|
foreach (Quotation::query()->where('requisition_id', $requisition->id)->pluck('supplier_id') as $supplierId) {
|
|
if ($this->processBlocked($requisition, $supplierId, $user)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function assertParticipation(User $user, Requisition $requisition): void
|
|
{
|
|
if ($this->participationBlocked($user, $requisition)) {
|
|
throw ValidationException::withMessages(['conflict' => 'Existe um conflito de interesses activo que impede a sua participação neste processo.']);
|
|
}
|
|
}
|
|
|
|
/** @return list<int> */
|
|
private function participants(Requisition $requisition): array
|
|
{
|
|
$quotes = Quotation::query()->where('requisition_id', $requisition->id);
|
|
$awards = PurchaseAward::query()->where('requisition_id', $requisition->id);
|
|
|
|
$participants = collect([$requisition->requester_id])
|
|
->merge($requisition->steps()->whereNotNull('approver_id')->pluck('approver_id'))
|
|
->merge((clone $quotes)->pluck('created_by'))
|
|
->merge(ProposalEvaluation::query()->whereIn('quotation_id', (clone $quotes)->select('id'))->pluck('evaluator_id'))
|
|
->merge((clone $awards)->pluck('created_by'))
|
|
->merge(PurchaseAwardApproval::query()->whereIn('purchase_award_id', (clone $awards)->select('id'))->whereNotNull('approver_id')->pluck('approver_id'))
|
|
->merge(AuditLog::query()->where('requisition_id', $requisition->id)->whereIn('action', [
|
|
'requisition.created', 'requisition.updated', 'requisition.submitted',
|
|
'approval.approved', 'approval.rejected', 'quotation.created', 'quotation.evaluated',
|
|
'award.created', 'award.approved', 'award.rejected', 'purchase_order.issued',
|
|
'equipment.technical_approved', 'equipment.technical_rejected',
|
|
'invoice.created', 'invoice.validated',
|
|
'payment.registered', 'payment.confirmed', 'payment.rejected',
|
|
])->pluck('user_id'))
|
|
->unique()->values()->all();
|
|
$ids = [];
|
|
foreach ($participants as $participant) {
|
|
$id = filter_var($participant, FILTER_VALIDATE_INT);
|
|
if ($id === false) {
|
|
throw new \LogicException('Identificador inválido no histórico de intervenientes.');
|
|
}
|
|
$ids[] = $id;
|
|
}
|
|
|
|
return array_values(array_unique($ids));
|
|
}
|
|
|
|
public function processBlocked(Requisition $requisition, int $supplierId, ?User $actor = null): bool
|
|
{
|
|
$participants = $this->participants($requisition);
|
|
if ($actor !== null) {
|
|
$participants[] = $actor->id;
|
|
}
|
|
foreach (array_unique($participants) as $userId) {
|
|
if ($this->hasConflict($userId, $supplierId, $requisition->id)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function assertProcess(Requisition $requisition, int $supplierId, User $actor): void
|
|
{
|
|
if ($this->processBlocked($requisition, $supplierId, $actor)) {
|
|
throw ValidationException::withMessages(['conflict' => 'Existe um conflito de interesses activo de um interveniente neste processo com este fornecedor. A operação está impedida, mesmo com declaração de ausência de conflito.']);
|
|
}
|
|
}
|
|
}
|