132 lines
6.3 KiB
PHP
132 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Enums\PurchasingRole;
|
|
use App\Models\AuditLog;
|
|
use App\Models\AwardCancellation;
|
|
use App\Models\PurchaseAward;
|
|
use App\Models\PurchaseOrder;
|
|
use App\Models\RecommendationAttempt;
|
|
use App\Models\Requisition;
|
|
use App\Models\User;
|
|
use App\Services\FormalRecommendation;
|
|
use App\Services\OrganizationAccess;
|
|
use App\Services\ProposalComparison;
|
|
use App\Services\PurchaseWorkflow;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class PurchaseController extends Controller
|
|
{
|
|
private function access(Request $request): User
|
|
{
|
|
$actor = $request->user('web');
|
|
abort_unless($actor instanceof User, 403);
|
|
abort_unless($actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
|
|
|
|
return $actor;
|
|
}
|
|
|
|
public function award(Request $request, Requisition $requisition, ProposalComparison $comparison, PurchaseWorkflow $workflow): Response
|
|
{
|
|
$actor = $this->access($request);
|
|
Requisition::query()->visibleTo($actor)->findOrFail($requisition->id);
|
|
$award = PurchaseAward::query()->where('requisition_id', $requisition->id)->with('approvals')->first();
|
|
$frozen = $award && AwardCancellation::query()->where('purchase_award_id', $award->id)->where('status', 'pending')->exists();
|
|
$recommendation = app(FormalRecommendation::class);
|
|
$adoption = $recommendation->adoption($requisition);
|
|
$candidate = null;
|
|
if ($adoption) {
|
|
$latest = RecommendationAttempt::query()->where('recommendation_adoption_id', $adoption->id)->latest('revision')->first();
|
|
if ($latest) {
|
|
try {
|
|
$recommendation->selection($requisition, $latest->quotation_id);
|
|
$candidate = $latest->quotation_id;
|
|
} catch (ValidationException) {
|
|
// The POST revalidates again; stale recommendations cannot enable the form.
|
|
}
|
|
}
|
|
}
|
|
|
|
return Inertia::render('purchases/award', [
|
|
'requisition' => $requisition, 'comparison' => $comparison->inspect($requisition), 'award' => $award,
|
|
'canSelect' => ! app(OrganizationAccess::class)->maintenanceOnly($actor) && $actor->hasFunctions([PurchasingRole::Procurement]) && $award === null && $actor->id !== $requisition->requester_id && (! $adoption || $candidate !== null),
|
|
'recommendation' => ['adopted' => $adoption !== null, 'quotation_id' => $candidate],
|
|
'canDecide' => ! $frozen && $award !== null && $workflow->canDecide($actor, $award, $requisition),
|
|
'canIssue' => $award !== null && $workflow->canIssue($actor, $award, $requisition),
|
|
'order' => PurchaseOrder::query()->where('requisition_id', $requisition->id)->first(['id', 'number']),
|
|
]);
|
|
}
|
|
|
|
public function select(Request $request, Requisition $requisition, PurchaseWorkflow $workflow): RedirectResponse
|
|
{
|
|
$actor = $this->access($request);
|
|
$data = $request->validate([
|
|
'quotation_id' => 'required|integer', 'justification' => 'required|string|min:10|max:5000',
|
|
'delivery_date' => 'required|date_format:Y-m-d|after_or_equal:today', 'delivery_location' => 'required|string|min:3|max:500',
|
|
'status' => 'prohibited', 'total_minor' => 'prohibited', 'subtotal_minor' => 'prohibited', 'tax_minor' => 'prohibited',
|
|
'approval_snapshot' => 'prohibited', 'decision_matrix' => 'prohibited', 'created_by' => 'prohibited', 'tenant_id' => 'prohibited',
|
|
'override' => 'prohibited', 'exception' => 'prohibited',
|
|
]);
|
|
$workflow->select($actor, $requisition, $data);
|
|
|
|
return to_route('purchases.award', $requisition);
|
|
}
|
|
|
|
public function decide(Request $request, PurchaseAward $award, PurchaseWorkflow $workflow): RedirectResponse
|
|
{
|
|
$actor = $this->access($request);
|
|
$data = $request->validate(['decision' => ['required', Rule::in(['approved', 'rejected'])], 'comment' => 'nullable|required_if:decision,rejected|string|max:2000']);
|
|
$workflow->decide($actor, $award, $data['decision'], $data['comment'] ?? null);
|
|
|
|
return to_route('purchases.award', $award->requisition_id);
|
|
}
|
|
|
|
public function issue(Request $request, PurchaseAward $award, PurchaseWorkflow $workflow): RedirectResponse
|
|
{
|
|
$actor = $this->access($request);
|
|
$request->validate([
|
|
'status' => 'prohibited', 'snapshot' => 'prohibited', 'total_minor' => 'prohibited', 'subtotal_minor' => 'prohibited', 'tax_minor' => 'prohibited',
|
|
'issued_by' => 'prohibited', 'tenant_id' => 'prohibited', 'quotation_id' => 'prohibited', 'override' => 'prohibited', 'exception' => 'prohibited',
|
|
]);
|
|
$order = $workflow->issue($actor, $award);
|
|
|
|
return to_route('orders.show', $order);
|
|
}
|
|
|
|
public function index(Request $request): Response
|
|
{
|
|
$actor = $this->access($request);
|
|
|
|
return Inertia::render('purchases/index', [
|
|
'orders' => PurchaseOrder::query()->visibleTo($actor)->latest('id')->paginate(15),
|
|
'awards' => PurchaseAward::query()->whereIn('requisition_id', Requisition::query()->visibleTo($actor)->select('id'))->latest()->paginate(15, ['*'], 'awards_page'),
|
|
]);
|
|
}
|
|
|
|
public function show(Request $request, PurchaseOrder $order): Response
|
|
{
|
|
$actor = $this->access($request);
|
|
PurchaseOrder::query()->visibleTo($actor)->findOrFail($order->id);
|
|
|
|
return Inertia::render('purchases/show', [
|
|
'order' => $order->load('items'),
|
|
'history' => AuditLog::query()->where('requisition_id', $order->requisition_id)->whereIn('action', ['award.created', 'award.approved', 'award.rejected', 'purchase_order.issued'])->orderBy('id')->get(['id', 'user_id', 'action', 'created_at']),
|
|
]);
|
|
}
|
|
|
|
public function print(Request $request, PurchaseOrder $order): View
|
|
{
|
|
$actor = $this->access($request);
|
|
PurchaseOrder::query()->visibleTo($actor)->findOrFail($order->id);
|
|
|
|
return view('purchases.print', ['order' => $order->load('items'), 'snapshot' => $order->snapshot]);
|
|
}
|
|
}
|