Files
compras/app/Services/ConflictGuard.php
T

130 lines
5.7 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',
'opening.recorded',
'qualification.defined', 'qualification.assessed',
'admission.adopted', 'admission.proposed', 'admission.reviewed',
'equipment.technical_approved', 'equipment.technical_rejected',
'invoice.created', 'invoice.validated',
'payment.registered', 'payment.confirmed', 'payment.rejected',
'cancellation.requested', 'cancellation.approved', 'cancellation.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.']);
}
}
}