user('web'); abort_unless($actor instanceof User && $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); return $actor; } private function access(Request $request, int $id): Requisition { abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); return Requisition::query()->visibleTo($this->actor($request))->findOrFail($id); } public function policy(Request $request): Response { abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Admin]), 403); Inertia::encryptHistory(); $response = Inertia::render('purchases/recommendation-policy', ['policies' => RecommendationPolicy::query()->latest('revision')->paginate(20)->withQueryString(), 'latest' => RecommendationPolicy::query()->latest('revision')->first()])->toResponse($request); $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; } public function configure(Request $request, FormalRecommendation $workflow): RedirectResponse { $data = $request->validate(['previous_id' => 'required|integer|min:0', 'enabled' => 'required|boolean', 'roles' => 'required|array|min:1|max:2', 'roles.*' => 'required|distinct|in:finance,department_head', 'required_count' => 'required|integer|min:1|max:50', 'reason' => 'required|string|min:10|max:4000', 'tenant_id' => 'prohibited', 'created_by' => 'prohibited', 'snapshot' => 'prohibited']); $data['enabled'] = (bool) $data['enabled']; $workflow->policy($this->actor($request), $data); return to_route('recommendations.policy'); } public function show(Request $request, Requisition $requisition, FormalRecommendation $workflow): Response { $requisition = $this->access($request, $requisition->id); $filter = $request->validate(['version' => 'nullable|integer|min:1']); $adoption = $workflow->adoption($requisition); $selected = isset($filter['version']) ? RecommendationAdoption::query()->where('requisition_id', $requisition->id)->findOrFail((int) $filter['version']) : $adoption; $focusInput = $request->validate(['attempt_id' => ['nullable', 'integer', 'min:1', Rule::exists('recommendation_attempts', 'id')->where('tenant_id', $requisition->tenant_id)->where('recommendation_adoption_id', $selected->id ?? 0)]]); $focus = isset($focusInput['attempt_id']) ? (int) $focusInput['attempt_id'] : null; $current = $selected && $selected->id === $adoption?->id; $basis = $workflow->basis($requisition); $latest = RecommendationAttempt::query()->where('recommendation_adoption_id', $selected->id ?? 0)->latest('revision')->first(); $closed = $requisition->status !== 'approved' || PurchaseAward::query()->where('requisition_id', $requisition->id)->exists(); $open = $workflow->canParticipate($this->actor($request), $requisition); $history = RecommendationAttempt::query()->where('recommendation_adoption_id', $selected->id ?? 0)->when($focus !== null, fn ($query) => $query->whereKey($focus))->latest('revision')->paginate(10)->withQueryString(); $history->through(function ($attempt) use ($workflow, $selected, $current, $requisition, $request) { $fresh = $current && $workflow->currentReviewBasis($requisition, $selected, $attempt); return [...$attempt->toArray(), 'status' => $workflow->status($attempt, $selected), 'current' => (bool) $fresh, 'reviews' => RecommendationReview::query()->where('recommendation_attempt_id', $attempt->id)->orderBy('id')->get(), 'canReview' => $fresh && $workflow->canReview($this->actor($request), $requisition, $attempt)]; }); Inertia::encryptHistory(); $response = Inertia::render('purchases/recommendation', ['focusAttemptId' => $focus, 'requisition' => $requisition->only(['id', 'number']), 'policy' => RecommendationPolicy::query()->latest('revision')->first(), 'adoption' => $adoption, 'selected' => $selected, 'adoptions' => RecommendationAdoption::query()->where('requisition_id', $requisition->id)->latest('revision')->get(), 'hasAdmission' => app(FormalAdmission::class)->adoption($requisition) !== null, 'basis' => ['hash' => $basis['hash'], 'ready' => $basis['ready'], 'top_ids' => $basis['top_ids'], 'rows' => collect($basis['payload']['rows'])->map(fn ($row) => array_intersect_key($row, array_flip(['id', 'reference', 'supplier_name', 'currency', 'total_minor', 'eligible', 'ineligibility_reasons', 'evaluation_count', 'score_minor'])))->all()], 'previousId' => $latest->id ?? 0, 'history' => $history, 'closed' => $closed, 'canAdopt' => $open && $this->actor($request)->hasFunctions([PurchasingRole::Procurement]), 'canPropose' => $open && $current && $basis['ready'] && $this->actor($request)->hasFunctions([PurchasingRole::Procurement]) && $this->actor($request)->id !== $requisition->requester_id])->toResponse($request); $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; } public function adopt(Request $request, Requisition $requisition, FormalRecommendation $workflow): RedirectResponse { $this->access($request, $requisition->id); $data = $request->validate(['policy_id' => 'required|integer', 'previous_id' => 'required|integer|min:0', 'reason' => 'required|string|min:10|max:4000', 'tenant_id' => 'prohibited', 'snapshot' => 'prohibited', 'created_by' => 'prohibited']); $workflow->adopt($this->actor($request), $requisition, (int) $data['policy_id'], (int) $data['previous_id'], $data['reason']); return to_route('recommendations.show', $requisition); } public function propose(Request $request, Requisition $requisition, FormalRecommendation $workflow): RedirectResponse { $this->access($request, $requisition->id); $data = $request->validate(['quotation_id' => 'required|integer', 'previous_id' => 'required|integer|min:0', 'basis_hash' => 'required|string|size:64', 'reason' => 'required|string|min:10|max:4000', 'tenant_id' => 'prohibited', 'created_by' => 'prohibited', 'snapshot' => 'prohibited', 'status' => 'prohibited']); $workflow->propose($this->actor($request), $requisition, $data); return to_route('recommendations.show', $requisition); } public function review(Request $request, RecommendationAttempt $attempt, FormalRecommendation $workflow): RedirectResponse { $adoption = RecommendationAdoption::query()->findOrFail($attempt->recommendation_adoption_id); $this->access($request, $adoption->requisition_id); $data = $request->validate(['verdict' => 'required|in:approved,returned', 'reason' => 'required|string|min:10|max:4000', 'reviewed_by' => 'prohibited', 'snapshot' => 'prohibited', 'tenant_id' => 'prohibited']); $workflow->review($this->actor($request), $attempt, $data['verdict'], $data['reason']); return to_route('recommendations.show', $adoption->requisition_id); } public function print(Request $request, RecommendationAttempt $attempt, FormalRecommendation $workflow): Response { return DB::transaction(function () use ($request, $attempt, $workflow) { if (DB::connection()->getDriverName() === 'pgsql') { DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY'); } $actor = User::query()->where('tenant_id', $this->actor($request)->tenant_id)->findOrFail($this->actor($request)->id); $request->setUserResolver(fn () => $actor); $adoption = RecommendationAdoption::query()->findOrFail($attempt->recommendation_adoption_id); $requisition = $this->access($request, $adoption->requisition_id); $basis = $workflow->basis($requisition); $currentAdoption = $workflow->adoption($requisition); $latest = RecommendationAttempt::query()->where('recommendation_adoption_id', $adoption->id)->latest('revision')->firstOrFail(); $history = RecommendationAttempt::query()->where('recommendation_adoption_id', $adoption->id)->where('id', '<', $attempt->id)->orderBy('revision')->get()->map(fn ($previous) => ['attempt' => $previous, 'state' => $workflow->status($previous, $adoption), 'reviews' => RecommendationReview::query()->where('recommendation_attempt_id', $previous->id)->orderBy('id')->get()]); return response()->view('purchases.recommendation-print', ['requisition' => $requisition, 'attempt' => $attempt, 'adoption' => $adoption, 'history' => $history, 'reviews' => RecommendationReview::query()->where('recommendation_attempt_id', $attempt->id)->orderBy('id')->get(), 'state' => $workflow->status($attempt, $adoption), 'current' => $currentAdoption?->id === $adoption->id && $latest->id === $attempt->id && $basis['ready'] && $basis['hash'] === $attempt->basis_hash, 'generatedAt' => now()->timezone('Africa/Johannesburg'), 'generatedBy' => $this->actor($request)->name], 200, ['Cache-Control' => 'private, no-store, max-age=0']); }); } }