findOrFail($partId); $asset = Equipment::query()->findOrFail($equipmentId); if (($departmentId !== null && $asset->department_id !== $departmentId) || ! EquipmentSparePart::query()->where('equipment_id', $asset->id)->where('spare_part_id', $part->id)->exists()) { throw ValidationException::withMessages(['equipment_id' => 'A peça deve estar associada a um equipamento da área de destino e da mesma empresa.']); } return $part; } /** @param array $line */ public function line(array $line, int $departmentId, string $priority): void { if ($line['purchase_type'] === 'equipment' && empty(trim($line['production_area'] ?? ''))) { throw ValidationException::withMessages(['items' => 'Indique a área ou linha de produção do equipamento novo.']); } if ($line['purchase_type'] !== 'spare_part') { return; } $part = $this->linkedPart((int) ($line['spare_part_id'] ?? 0), (int) ($line['equipment_id'] ?? 0), $departmentId); $stock = StockItem::query()->findOrFail($part->stock_item_id); if (mb_strtolower(trim($stock->unit)) !== mb_strtolower(trim($line['unit']))) { throw ValidationException::withMessages(['items' => 'A unidade da peça deve coincidir com o artigo de stock, sem conversões automáticas.']); } $level = CriticalityLevel::query()->findOrFail($part->criticality_level_id); if ($priority === 'urgent' && $level->is_critical && empty($line['downtime_started_at'])) { throw ValidationException::withMessages(['items' => 'Peça crítica urgente: indique o início da paragem do equipamento (RF-AF-07).']); } } public function basis(Requisition $requisition): string { return hash('sha256', json_encode([$requisition->only(['id', 'tenant_id', 'department_id', 'requester_id', 'priority', 'needed_by', 'justification', 'total_minor']), $requisition->items()->orderBy('id')->get()->toArray()], JSON_THROW_ON_ERROR)); } public function canReview(User $user, Requisition $requisition): bool { return $requisition->status === 'draft' && $requisition->items()->where('purchase_type', 'equipment')->exists() && $this->eligible($user, $requisition); } private function eligible(User $user, Requisition $requisition): bool { return $requisition->tenant_id === $user->tenant_id && $user->purchasing_role === PurchasingRole::DepartmentHead && $user->department_id === $requisition->department_id && $user->id !== $requisition->requester_id && ! app(ConflictGuard::class)->participationBlocked($user, $requisition); } public function review(User $user, Requisition $requisition, string $basis, string $decision, string $reason): TechnicalReview { return DB::transaction(function () use ($user, $requisition, $basis, $decision, $reason) { app(ConflictGuard::class)->lockCompany($user); $user = app(CurrentActor::class)->lock($user, [PurchasingRole::DepartmentHead]); $requisition = Requisition::query()->lockForUpdate()->findOrFail($requisition->id); abort_unless($this->canReview($user, $requisition), 403); app(ConflictGuard::class)->assertParticipation($user, $requisition); if ($basis !== $this->basis($requisition)) { throw ValidationException::withMessages(['basis_hash' => 'O pedido mudou. Reveja a versão actual antes de decidir.']); } if (TechnicalReview::query()->where('requisition_id', $requisition->id)->where('basis_hash', $basis)->exists()) { throw ValidationException::withMessages(['technical' => 'Esta versão já tem uma decisão técnica. A decisão anterior permanece no histórico.']); } $review = TechnicalReview::query()->create(['tenant_id' => $user->tenant_id, 'requisition_id' => $requisition->id, 'reviewed_by' => $user->id, 'basis_hash' => $basis, 'decision' => $decision, 'reason' => $reason, 'reviewed_at' => now(), 'snapshot' => ['reviewer' => $user->only(['id', 'name', 'department_id']), 'items' => $requisition->items()->get()->toArray(), 'development_only' => true, 'budget_checked' => false]]); app(RequisitionWorkflow::class)->audit($user, 'equipment.technical_'.$decision, $requisition, null, $review->toArray()); return $review; }); } public function assertTechnical(Requisition $requisition): void { if (! $requisition->items()->where('purchase_type', 'equipment')->exists()) { return; } $review = TechnicalReview::query()->where('requisition_id', $requisition->id)->where('basis_hash', $this->basis($requisition))->first(); $user = null; if ($review) { $query = User::query()->where('tenant_id', $requisition->tenant_id); if (DB::connection()->transactionLevel() > 0) { $query->lockForUpdate(); } $user = $query->find($review->reviewed_by); } if (! $review || $review->decision !== 'approved' || ! $user || ! $this->eligible($user, $requisition)) { throw ValidationException::withMessages(['technical' => 'É necessária validação técnica válida do responsável da área, distinto do solicitante, para a versão actual.']); } } }