,?Requisition):Requisition $save */ public function __construct(private User $actor, private \Closure $save, private ?SupplyRecord $automaticRule = null) {} private function model(int $id): Requisition { return Requisition::query()->whereKey($id)->firstOrFail(); } public function actorId(): int { return $this->actor->id; } public function atomic(callable $work): mixed { return DB::transaction(function () use ($work) { app(ConflictGuard::class)->lockCompany($this->actor); $this->actor = app(CurrentActor::class)->lock($this->actor); abort_unless(app(OrganizationAccess::class)->permissions($this->actor) !== [], 403); abort_if(app(OrganizationAccess::class)->maintenanceOnly($this->actor), 403); if (request()->hasSession() && app(WorkContext::class)->enforced($this->actor)) { abort_unless(app(WorkContext::class)->active(request()) === 'procurement.internal', 403); } return $work(); }); } public function authorize(string $action, ?array $record): void { if ($this->automaticRule !== null) { abort_unless($action === 'save' && $record === null, 403); app(AutomaticPurchases::class)->requester($this->actor, $this->automaticRule); return; } if ($record !== null) { $r = $this->model($record['id']); if ($action === 'view') { Requisition::query()->visibleTo($this->actor)->findOrFail($record['id']); } abort_unless(($r->work_context_key ?? 'procurement.internal') === 'procurement.internal', 403); if ($action === 'decide' || str_starts_with($action, 'step:')) { abort_if(app(ConflictGuard::class)->participationBlocked($this->actor, $r), 403); } elseif ($action !== 'view') { app(ConflictGuard::class)->assertParticipation($this->actor, $r); } } if (in_array($action, ['save', 'submit'], true)) { abort_unless(app(OrganizationAccess::class)->canRequest($this->actor), 403); if ($record !== null) { abort_unless($record['requester_id'] === $this->actor->id, 403); } } elseif ($action === 'decide') { abort_unless($this->actor->hasFunctions(['department_head', 'procurement', 'finance']), 403); } elseif (str_starts_with($action, 'step:')) { $role = substr($action, 5); abort_unless($this->actor->hasFunctions([$role]) && ($role !== 'department_head' || $this->actor->department_id === $record['department_id']), 403); } } public function load(int $id): array { $r = Requisition::query()->lockForUpdate()->findOrFail($id); return $this->snapshot($r); } /** @return array */ private function snapshot(Requisition $r): array { $r->load(['steps' => fn ($q) => $q->orderBy('position'), 'items']); return $r->toArray() + ['version' => (int) ($r->getAttribute('process_version') ?? 1), 'has_equipment' => $r->items->contains('purchase_type', 'equipment')]; } public function draft(array $data, ?int $id): array { return $this->snapshot(($this->save)($data, $id === null ? null : $this->model($id))); } public function policy(array $record): array { $r = $this->model($record['id']); if ($this->actor->department_id !== $r->department_id) { throw ValidationException::withMessages(['submission' => 'O departamento do solicitante mudou. Reveja o rascunho e o centro de custo antes de submeter.']); } foreach ($r->items()->get() as $item) { app(EquipmentWorkflow::class)->line($item->toArray(), $r->department_id, $r->priority); } app(EquipmentWorkflow::class)->assertTechnical($r); if ($r->needed_by < today()) { throw ValidationException::withMessages(['submission' => 'Actualize a data de entrega antes de submeter.']); } return Tenant::query()->findOrFail($this->actor->tenant_id)->only(['first_limit_minor', 'second_limit_minor', 'approval_includes_tax', 'rules_provisional']); } public function reserve(array $record): ?array { return app(BudgetWorkflow::class)->record($this->actor, $this->model($record['id']), 'reserve', $record['total_minor'], $record['subtotal_minor']); } public function submitted(array $record, array $policy, array $roles): array { $r = $this->model($record['id']); foreach ($roles as $position => $role) { $r->steps()->create(['tenant_id' => $this->actor->tenant_id, 'position' => $position + 1, 'role' => $role]); } $values = ['status' => 'pending', 'submitted_at' => now(), 'approval_snapshot' => $policy]; if (Schema::hasColumn('requisitions', 'process_version')) { $values['process_version'] = $record['version'] + 1; } $r->update($values); app(RequisitionWorkflow::class)->audit($this->actor, 'requisition.submitted', $r, ['status' => 'draft'], ['status' => 'pending', 'rules' => $policy, 'roles' => $roles]); app(WorkflowNotices::class)->capture($r, $this->actor); return $this->snapshot($r); } public function decided(array $record, array $step, string $decision, ?string $comment, string $status): array { $r = $this->model($record['id']); if ($decision === 'approved') { app(EquipmentWorkflow::class)->assertTechnical($r); } $r->steps()->whereKey($step['id'])->firstOrFail()->update(['decision' => $decision, 'approver_id' => $this->actor->id, 'comment' => $comment, 'decided_at' => now()]); $values = ['status' => $status]; if (Schema::hasColumn('requisitions', 'process_version')) { $values['process_version'] = $record['version'] + 1; } $r->update($values); app(RequisitionWorkflow::class)->audit($this->actor, 'approval.'.$decision, $r, ['status' => 'pending'], ['status' => $status, 'step_id' => $step['id'], 'role' => $step['role'], 'comment' => $comment]); app(WorkflowNotices::class)->capture($r, $this->actor); app(WorkflowNotices::class)->outcome($r, $step['id']); return $this->snapshot($r); } public function release(array $record): void { app(BudgetWorkflow::class)->record($this->actor, $this->model($record['id']), 'release'); } public function replay(string $key, string $hash): ?array { abort_unless(Schema::hasTable('requisition_commands'), 503, 'Instale a migração de recibos antes de usar chaves de operação.'); $old = DB::table('requisition_commands')->where(['tenant_id' => $this->actor->tenant_id, 'actor_id' => $this->actor->id, 'operation_key' => $key])->first(); if ($old === null) { return null; } abort_unless(hash_equals($old->request_hash, $hash), 409, 'Chave usada com outro conteúdo.'); return json_decode($old->result, true, 64, JSON_THROW_ON_ERROR); } public function remember(string $key, string $hash, array $result): void { DB::table('requisition_commands')->insert(['tenant_id' => $this->actor->tenant_id, 'actor_id' => $this->actor->id, 'operation_key' => $key, 'request_hash' => $hash, 'result' => json_encode($result, JSON_THROW_ON_ERROR), 'created_at' => now()]); } }