fn ($query) => $query ->withoutGlobalScopes() ->select('id', 'ulid', 'name', 'code'), ]); if ($status = $request->status) { $query->where('status', $status); } if ($projectId = $request->project_id) { $query->where('project_id', $projectId); } $query->whereIn('project_id', $this->availableProjectIdsQuery()); $invoices = $query->latest()->paginate(15)->withQueryString(); $projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery()) ->select('id', 'ulid', 'name', 'code')->get(); // Compute summary stats $allInvoices = FinancialInvoice::whereIn('project_id', $this->availableProjectIdsQuery()); $summary = [ 'total_billed' => (float) $allInvoices->sum('total_amount'), 'total_paid' => (float) (clone $allInvoices)->where('status', 'paid')->sum('paid_amount'), 'outstanding' => (float) (clone $allInvoices)->where('status', '!=', 'paid')->sum('total_amount'), 'total_retention' => abs( (float) RetentionEntry::where('type', 'debit') ->whereIn('project_id', $this->availableProjectIdsQuery())->sum('amount') - (float) RetentionEntry::where('type', 'credit') ->whereIn('status', ['posted', 'paid']) ->whereIn('project_id', $this->availableProjectIdsQuery())->sum('amount') ), ]; return Inertia::render('FinancialManagement::Invoices/Index', [ 'invoices' => $invoices, 'projects' => $projects, 'summary' => $summary, 'filters' => $request->only(['status', 'project_id']), ]); } // --- Create (Progress Billing) --- public function create() { $user = auth()->user(); if ($user && $user->hasAnyRole(['Site Technical', 'Construction Supervisor', 'Site Operations', 'Site Engineer', 'Site Supervisor', 'Field Engineer', 'Safety Officer', 'Quality Inspector', 'Warehouse Staff'])) { abort(403, 'Unauthorized action. Site Operations roles are restricted from generating invoices.'); } $projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery()) ->select('id', 'ulid', 'name', 'code', 'contract_value', 'last_billed_percentage') ->where('current_wizard_step', '>=', 7) ->where('status', '!=', 'closed') ->get(); return Inertia::render('FinancialManagement::Invoices/Create', [ 'projects' => $projects, ]); } public function store(Request $request) { $user = $request->user(); if ($user && $user->hasAnyRole(['Site Technical', 'Construction Supervisor', 'Site Operations', 'Site Engineer', 'Site Supervisor', 'Field Engineer', 'Safety Officer', 'Quality Inspector', 'Warehouse Staff'])) { abort(403, 'Unauthorized action. Site Operations roles are restricted from generating invoices.'); } $validated = $request->validate([ 'project_id' => 'required|string', 'current_percentage' => 'required|numeric|min:0.01|max:100', 'retention_rate' => 'nullable|numeric|min:0|max:50', ]); $project = Project::findByUlid($validated['project_id']); if (!$project) { return back()->with('error', 'Project not found.'); } try { $this->billingService->generateInvoice( $project, $validated['current_percentage'], $validated['retention_rate'] ?? 10.00, ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return redirect()->route('finance.index')->with('success', 'Progress invoice generated.'); } // --- Show Invoice --- public function show(FinancialInvoice $invoice) { abort_unless($this->canAccessProject(auth()->user(), $invoice->project_id), 403); $invoice->load(['project:id,name,code', 'lineItems', 'retentionEntries', 'paymentProofSubmittedBy:id,name,email']); return Inertia::render('FinancialManagement::Invoices/Show', [ 'invoice' => $invoice, ]); } // --- State Transitions --- public function submit(Request $request, FinancialInvoice $invoice) { abort_unless($this->canAccessProject($request->user(), $invoice->project_id), 403); try { $invoice->transitionTo(InvoiceStatus::Submitted); // Fetch Admins and Super Admins as approvers $adminIds = User::where('user_type', 'admin') ->orWhereHas('roles', function ($q) { $q->whereIn('name', ['Super Admin', 'admin', 'Project Manager', 'Main Contractor Admin']); }) ->pluck('id') ->toArray(); if (!empty($adminIds)) { $this->approvalService->createChain( approvable: $invoice, approverIds: $adminIds, type: 'financial_invoice', initiatedBy: $request->user()->id, notes: "Progress Invoice {$invoice->invoice_number} submitted for approval.", ); } } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Invoice submitted for approval.'); } private function isExecutiveApprover(?User $user): bool { if (!$user) return false; $userType = strtolower($user->user_type ?? ''); if (in_array($userType, ['admin', 'super_admin', 'project_manager'], true)) { return true; } return $user->roles()->whereIn('name', ['Super Admin', 'admin', 'Admin', 'Project Manager', 'project_manager', 'Executive'])->exists(); } public function approve(FinancialInvoice $invoice) { $user = auth()->user(); abort_unless($this->canAccessProject($user, $invoice->project_id), 403); $isApprover = $this->isExecutiveApprover($user); if (!$isApprover) { return back()->with('error', 'Unauthorized. Only Super Admin, Admin, or Project Manager can approve client invoices.'); } try { $invoice->transitionTo(InvoiceStatus::Approved); $this->billingService->holdRetention($invoice); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Invoice approved. Retention held.'); } public function reject(FinancialInvoice $invoice) { $user = auth()->user(); abort_unless($this->canAccessProject($user, $invoice->project_id), 403); $isApprover = $this->isExecutiveApprover($user); if (!$isApprover) { return back()->with('error', 'Unauthorized. Only Super Admin, Admin, or Project Manager can reject client invoices.'); } try { $invoice->transitionTo(InvoiceStatus::Rejected); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Invoice rejected.'); } public function send(FinancialInvoice $invoice) { abort_unless($this->canAccessProject(auth()->user(), $invoice->project_id), 403); try { $invoice->transitionTo(InvoiceStatus::Sent); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Invoice sent to client.'); } public function sendPaymentProof(Request $request, FinancialInvoice $invoice) { $user = $request->user(); abort_unless($this->canAccessProject($user, $invoice->project_id), 403); abort_unless($this->isContractorAdmin($user), 403, 'Only Contractor Admin can submit payment proof.'); $hasFileInfo = extension_loaded('fileinfo'); $rules = [ 'media' => 'required|file|max:10240', 'notes' => 'nullable|string|max:1000', ]; if ($hasFileInfo) { $rules['media'] .= '|mimes:pdf,jpg,jpeg,png'; } $validated = $request->validate($rules); $file = $request->file('media'); if (!$hasFileInfo) { $extension = strtolower($file->getClientOriginalExtension() ?: pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION)); if (!in_array($extension, ['pdf', 'jpg', 'jpeg', 'png'], true)) { return back()->withErrors(['media' => 'The proof must be a file of type: pdf, jpg, jpeg, png.']); } } $path = $file->store("invoice_payment_proofs/{$invoice->id}", 'public'); try { $this->billingService->submitContractorPaymentProof( $invoice, $path, $file->getClientOriginalName(), $validated['notes'] ?? null, $user->id ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', '10% Payment proof sent to Executive. Awaiting payment receipt confirmation.'); } public function receivePayment(Request $request, FinancialInvoice $invoice) { $user = $request->user(); abort_unless($this->canAccessProject($user, $invoice->project_id), 403); $isApprover = $this->isExecutiveApprover($user); if (!$isApprover) { return back()->with('error', 'Only Super Admin, Admin, or Project Manager can confirm received payment.'); } $validated = $request->validate([ 'notes' => 'nullable|string|max:1000', ]); try { $this->billingService->confirmPaymentReceived($invoice, $validated['notes'] ?? null); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Payment receipt confirmed successfully. Invoice is now Paid.'); } public function viewPaymentProof(Request $request, FinancialInvoice $invoice) { abort_unless($this->canAccessProject($request->user(), $invoice->project_id), 403); if (!$invoice->payment_proof_path) { abort(404, 'No payment proof available.'); } $disk = \Storage::disk('public'); if (!$disk->exists($invoice->payment_proof_path)) { abort(404, 'Proof file not found on disk.'); } $mimeType = $disk->mimeType($invoice->payment_proof_path); return response()->file($disk->path($invoice->payment_proof_path), [ 'Content-Type' => $mimeType, 'Content-Disposition' => 'inline; filename="' . ($invoice->payment_proof_name ?: 'payment-proof') . '"', ]); } public function applyPenalty(Request $request, FinancialInvoice $invoice) { $user = $request->user(); abort_unless($this->canAccessProject($user, $invoice->project_id), 403); abort_unless($this->isExecutiveApprover($user), 403, 'Only Super Admin, Admin, or Project Manager can assess retention penalties.'); $validated = $request->validate([ 'penalty_rate' => 'required|numeric|min:0.01|max:100', 'reason' => 'required|string|min:3|max:1000', ]); try { $this->billingService->applyPenalty( $invoice, (float) $validated['penalty_rate'], $validated['reason'], $user->id ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', "Penalty of {$validated['penalty_rate']}% applied successfully to Invoice #{$invoice->invoice_number}."); } public function recordPayment(Request $request, FinancialInvoice $invoice) { abort_unless($this->canAccessProject($request->user(), $invoice->project_id), 403); $validated = $request->validate([ 'amount' => 'required|numeric|min:0.01', ]); try { $user = $request->user(); $isExecutive = $this->isExecutiveApprover($user); if ($isExecutive) { $this->billingService->recordExecutivePayment($invoice, (float) $validated['amount']); $msg = 'Payment recorded and sent for contractor receipt confirmation.'; } else { $this->billingService->confirmContractorPayment($invoice); $msg = 'Payment receipt confirmed.'; } } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', $msg); } public function confirmPayment(Request $request, FinancialInvoice $invoice) { abort_unless($this->canAccessProject($request->user(), $invoice->project_id), 403); abort_unless($this->isContractorAdmin($request->user()), 403, 'Only Contractor Admin can confirm payment receipts.'); try { $this->billingService->confirmContractorPayment($invoice); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Payment receipt confirmed successfully.'); } private function canAccessRetention(User $user): bool { // Site Operations roles are strictly excluded from retention $isSiteOps = $user->hasAnyRole([ 'Site Technical', 'Construction Supervisor', 'Site Operations', 'Site Engineer', 'Site Supervisor', ]); if ($isSiteOps) { return false; } $isAdmin = $user->user_type === 'admin' || $user->hasAnyRole(['Super Admin', 'admin', 'Admin', 'Project Manager', 'project_manager']); return $isAdmin || $this->isContractorAdmin($user); } // --- Retention Ledger --- public function retention(Request $request) { abort_unless($this->canAccessRetention($request->user()), 403, 'Unauthorized. Retention management is restricted to Contractor Admin and Executive roles.'); // Dynamic reconciliation: Ensure every Paid invoice has its full retention + penalty settled in the ledger $paidInvoices = FinancialInvoice::whereIn('project_id', $this->availableProjectIdsQuery()) ->where('status', InvoiceStatus::Paid) ->where('retention_amount', '>', 0) ->get(); foreach ($paidInvoices as $paidInv) { $totalDue = (float) $paidInv->retention_amount + (float) ($paidInv->penalty_amount ?? 0); $credit = RetentionEntry::where([ 'project_id' => $paidInv->project_id, 'invoice_id' => $paidInv->id, 'type' => 'credit', ])->first(); $desc = (float) ($paidInv->penalty_amount ?? 0) > 0 ? "Retention & Late Penalty Remittance Settled & Confirmed for Invoice #{$paidInv->invoice_number}" : "10% Retention Remittance Settled & Confirmed for Invoice #{$paidInv->invoice_number}"; if (!$credit) { RetentionEntry::create([ 'project_id' => $paidInv->project_id, 'invoice_id' => $paidInv->id, 'type' => 'credit', 'status' => 'paid', 'amount' => $totalDue, 'description' => $desc, ]); } elseif ((float) $credit->amount !== $totalDue) { $credit->update([ 'amount' => $totalDue, 'status' => 'paid', 'description' => $desc, ]); } } $query = RetentionEntry::with([ 'project' => fn ($query) => $query ->withoutGlobalScopes() ->select('id', 'ulid', 'name', 'code'), 'invoice:id,invoice_number', ]); if ($projectId = $request->project_id) { $query->where('project_id', $projectId); } $query->whereIn('project_id', $this->availableProjectIdsQuery()); $entries = $query->latest()->paginate(20)->withQueryString(); $projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery()) ->select('id', 'ulid', 'name', 'code', 'status')->get(); $pendingReleases = RetentionEntry::whereIn('project_id', $this->availableProjectIdsQuery()) ->where('type', 'credit') ->where('status', '!=', 'paid') ->get(['id', 'ulid', 'project_id', 'amount', 'status', 'media_path', 'media_original_name']) ->keyBy('project_id'); // Compute per-project totals $projectTotals = RetentionEntry::whereIn('project_id', $this->availableProjectIdsQuery()) ->selectRaw('project_id, type, status, SUM(amount) as total') ->groupBy('project_id', 'type', 'status') ->get() ->groupBy('project_id') ->map(function ($items) use ($pendingReleases) { $debits = $items->where('type', 'debit')->sum('total'); $credits = $items->where('type', 'credit')->whereIn('status', ['posted', 'paid'])->sum('total'); $pending = $pendingReleases->get($items->first()->project_id); return [ 'held' => (float) $debits, 'released' => (float) $credits, 'balance' => (float) $debits - (float) $credits, 'pending_release_id' => $pending?->id, 'pending_release_ulid' => $pending?->ulid, 'pending_release_status' => $pending?->status, 'pending_release_amount' => $pending ? (float) $pending->amount : null, 'pending_release_media_path' => $pending?->media_path, 'pending_release_media_name' => $pending?->media_original_name, ]; }); // Query overdue invoices: unpaid and older than 2 months (60 days) $twoMonthsAgo = now()->subMonths(2); $overdueInvoices = FinancialInvoice::with([ 'project' => fn ($q) => $q->withoutGlobalScopes()->select('id', 'ulid', 'name', 'code'), 'penaltyAppliedBy:id,name', ]) ->whereIn('project_id', $this->availableProjectIdsQuery()) ->whereIn('status', [InvoiceStatus::Approved, InvoiceStatus::Sent, InvoiceStatus::PaymentSent, InvoiceStatus::Submitted]) ->where(function ($q) use ($twoMonthsAgo) { $q->where('invoice_date', '<=', $twoMonthsAgo) ->orWhere('approved_at', '<=', $twoMonthsAgo) ->orWhere('created_at', '<=', $twoMonthsAgo); }) ->latest('invoice_date') ->get() ->map(function ($inv) { $referenceDate = $inv->approved_at ?? $inv->invoice_date ?? $inv->created_at; $daysOverdue = (int) abs(now()->diffInDays($referenceDate)); return [ 'id' => $inv->id, 'ulid' => $inv->ulid, 'invoice_number' => $inv->invoice_number, 'project' => $inv->project ? [ 'id' => $inv->project->id, 'ulid' => $inv->project->ulid, 'name' => $inv->project->name, 'code' => $inv->project->code, ] : null, 'status' => $inv->status instanceof InvoiceStatus ? $inv->status->value : $inv->status, 'invoice_date' => $inv->invoice_date?->format('Y-m-d'), 'approved_at' => $inv->approved_at?->format('Y-m-d H:i'), 'subtotal' => (float) $inv->subtotal, 'retention_amount' => (float) $inv->retention_amount, 'retention_rate' => (float) $inv->retention_rate, 'total_amount' => (float) $inv->total_amount, 'paid_amount' => (float) $inv->paid_amount, 'days_overdue' => $daysOverdue, 'penalty_rate' => $inv->penalty_rate !== null ? (float) $inv->penalty_rate : null, 'penalty_amount' => (float) $inv->penalty_amount, 'penalty_reason' => $inv->penalty_reason, 'penalty_applied_by' => $inv->penaltyAppliedBy?->name, 'penalty_applied_at' => $inv->penalty_applied_at?->format('Y-m-d H:i'), ]; }); return Inertia::render('FinancialManagement::Retention/Index', [ 'entries' => $entries, 'projects' => $projects, 'projectTotals' => $projectTotals, 'overdueInvoices' => $overdueInvoices, 'filters' => $request->only(['project_id']), ]); } public function exportRetentionCsv(Request $request) { abort_unless($this->canAccessRetention($request->user()), 403); $entries = RetentionEntry::with(['project', 'invoice']) ->whereIn('project_id', $this->availableProjectIdsQuery()) ->latest() ->get(); $filename = "retention_ledger_report_" . date('Y-m-d_His') . ".csv"; $headers = [ "Content-type" => "text/csv; charset=UTF-8", "Content-Disposition" => "attachment; filename={$filename}", "Pragma" => "no-cache", "Cache-Control" => "must-revalidate, post-check=0, pre-check=0", "Expires" => "0", ]; $callback = function () use ($entries) { $file = fopen('php://output', 'w'); fputs($file, "\xEF\xBB\xBF"); fputcsv($file, ['GSB Construction - Retention & Penalty Management Report']); fputcsv($file, ['Generated Date:', date('Y-m-d H:i:s')]); fputcsv($file, []); fputcsv($file, ['Project Name', 'Invoice #', 'Entry Type', 'Amount (PHP)', 'Description', 'Date Recorded']); foreach ($entries as $e) { fputcsv($file, [ $e->project->name ?? 'N/A', $e->invoice->invoice_number ?? 'N/A', $e->type === 'debit' ? 'Retention Holdback / Penalty' : 'Settled & Released', number_format((float) $e->amount, 2, '.', ''), $e->description ?? '', $e->created_at ? $e->created_at->format('Y-m-d H:i') : '', ]); } fclose($file); }; return response()->stream($callback, 200, $headers); } public function exportInvoicePdf(FinancialInvoice $invoice) { abort_unless($this->canAccessProject(auth()->user(), $invoice->project_id), 403); $invoice->load(['project', 'lineItems']); $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('financialmanagement::pdf.retention_statement', [ 'invoice' => $invoice, ]); return $pdf->download("Retention_Statement_{$invoice->invoice_number}.pdf"); } public function submitRetentionRelease(Request $request, Project $project) { abort_unless($this->canAccessProject($request->user(), $project->id), 403); $hasFileInfo = extension_loaded('fileinfo'); $rules = ['media' => 'required|file|max:10240']; if ($hasFileInfo) { $rules['media'] .= '|mimes:pdf'; } $validated = $request->validate($rules); $file = $request->file('media'); if (!$hasFileInfo) { $extension = strtolower($file->getClientOriginalExtension() ?: pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION)); if ($extension !== 'pdf') { return back()->withErrors(['media' => 'The proof must be a file of type: pdf, jpg, jpeg, png.']); } } $path = $file->store("retention-releases/{$project->id}", 'public'); try { $this->billingService->submitRetentionRelease($project, $path, $file->getClientOriginalName()); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Retention release submitted for payment.'); } public function markRetentionAsPaid(Request $request, RetentionEntry $retentionEntry) { abort_unless($this->canAccessProject($request->user(), $retentionEntry->project_id), 403); $isApprover = $request->user()->user_type === 'admin' || $request->user()->roles()->whereIn('name', ['Super Admin', 'admin', 'Admin', 'Project Manager', 'project_manager'])->exists(); if (!$isApprover) { return back()->with('error', 'Only Project Manager, Admin, or Super Admin can mark retention as paid.'); } $request->validate([ 'media' => ['nullable', 'file', 'mimes:pdf', 'max:10240'], ]); $mediaPath = null; $mediaName = null; if ($request->hasFile('media')) { $file = $request->file('media'); $mediaPath = $file->store('retention_proofs', 'public'); $mediaName = $file->getClientOriginalName(); } try { $this->billingService->markRetentionAsPaid($retentionEntry, $mediaPath, $mediaName); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Retention payment recorded and sent for contractor receipt confirmation.'); } public function confirmRetentionPayment(Request $request, RetentionEntry $retentionEntry) { abort_unless($this->canAccessProject($request->user(), $retentionEntry->project_id), 403); abort_unless($this->isContractorAdmin($request->user()), 403, 'Only Contractor Admin can confirm payment receipts.'); try { $this->billingService->confirmRetentionPayment($retentionEntry); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return back()->with('success', 'Retention payment receipt confirmed successfully.'); } private function isContractorAdmin(?User $user): bool { if (! $user) return false; $isSiteOps = $user->hasAnyRole([ 'Site Technical', 'Construction Supervisor', 'Site Operations', 'Site Engineer', 'Site Supervisor', ]); if ($isSiteOps) { return false; } $userType = strtolower($user->user_type ?? ''); $roles = strtolower($user->getRoleNames()->implode(',')); $isExecutive = in_array($userType, ['admin', 'super_admin', 'project_manager']) || $user->hasAnyRole(['Super Admin', 'admin', 'Admin', 'Project Manager', 'Executive']); if ($isExecutive) { return false; } return !is_null($user->contractor_id) || $userType === 'contractor' || $user->hasAnyRole(['Main Contractor Admin', 'Contractor Admin']) || \Illuminate\Support\Str::contains($roles, 'contractor'); } public function viewRetentionMedia(Request $request, RetentionEntry $retentionEntry) { abort_unless($this->canAccessProject($request->user(), $retentionEntry->project_id), 403); if (!$retentionEntry->media_path) { abort(404, 'No supporting document uploaded.'); } $disk = \Storage::disk('public'); if (!$disk->exists($retentionEntry->media_path)) { abort(404, 'Supporting document not found.'); } return response()->file($disk->path($retentionEntry->media_path), [ 'Content-Type' => $disk->mimeType($retentionEntry->media_path) ?: 'application/octet-stream', 'Content-Disposition' => 'inline; filename="' . addslashes($retentionEntry->media_original_name ?: 'retention-proof') . '"', ]); } // Cash Advances public function cashAdvances(Request $request) { $user = $request->user(); $isApprover = $this->canApproveCashAdvance($user); $query = $isApprover ? \Modules\FinancialManagement\Models\CashAdvance::withoutGlobalScopes() : \Modules\FinancialManagement\Models\CashAdvance::query(); $query->with(['project:id,name,code', 'requester:id,name,email', 'approver:id,name,email']); $query->whereIn('project_id', $this->availableProjectIdsQuery()); if ($projectId = $request->project_id) { $query->where('project_id', $projectId); } $cashAdvances = $query->latest()->paginate(20)->withQueryString(); $projects = Project::whereIn('projects.id', $this->availableProjectIdsQuery()) ->select('id', 'ulid', 'name', 'code')->get(); return Inertia::render('FinancialManagement::CashAdvances/Index', [ 'cashAdvances' => $cashAdvances, 'projects' => $projects, 'filters' => $request->only(['project_id']), ]); } public function storeCashAdvance(Request $request) { $validated = $request->validate([ 'project_ulid' => 'required|string', 'amount' => 'required|numeric|min:1', 'reason' => 'required|string|max:1000', ]); $project = Project::whereIn('id', $this->availableProjectIdsQuery()) ->where('ulid', $validated['project_ulid']) ->firstOrFail(); $cashAdvance = \Modules\FinancialManagement\Models\CashAdvance::create([ 'project_id' => $project->id, 'amount' => $validated['amount'], 'reason' => $validated['reason'], 'status' => 'pending', 'requested_by' => auth()->id(), ]); return back()->with('success', 'Cash advance request submitted successfully.'); } public function approveCashAdvance(string $cashAdvance) { $user = auth()->user(); $cashAdvance = \Modules\FinancialManagement\Models\CashAdvance::withoutGlobalScopes() ->where('ulid', $cashAdvance) ->firstOrFail(); if ($cashAdvance->requested_by === $user->id && $user->user_type !== 'admin' && !$user->hasRole('Super Admin')) { return back()->with('error', 'You cannot approve your own cash advance request.'); } $isApprover = $this->canApproveCashAdvance($user); if (! $isApprover) { return back()->with('error', 'Unauthorized to approve cash advance requests.'); } if (! $this->isPlatformUser($user) && ! $this->availableProjectIdsQuery()->where('projects.id', $cashAdvance->project_id)->exists()) { return back()->with('error', 'You cannot approve a cash advance for an unrelated project.'); } $cashAdvance->update([ 'status' => 'approved', 'approved_by' => auth()->id(), ]); return back()->with('success', 'Cash advance request approved.'); } public function rejectCashAdvance(string $cashAdvance) { $user = auth()->user(); $cashAdvance = \Modules\FinancialManagement\Models\CashAdvance::withoutGlobalScopes() ->where('ulid', $cashAdvance)->firstOrFail(); if (! $this->canApproveCashAdvance($user)) { return back()->with('error', 'Unauthorized to reject cash advance requests.'); } $cashAdvance->update(['status' => 'rejected', 'approved_by' => $user->id]); return back()->with('success', 'Cash advance request rejected.'); } private function canApproveCashAdvance(User $user): bool { return $user->user_type === 'admin' || $user->hasAnyRole(['Super Admin', 'admin', 'Project Manager', 'Main Contractor Admin']); } private function availableProjectIdsQuery() { return Project::query()->select('projects.id'); } private function isPlatformUser(User $user): bool { return $user->user_type === 'admin' || $user->hasAnyRole(['Super Admin', 'admin']); } private function canAccessProject(User $user, ?int $projectId): bool { return $projectId !== null && ($this->isPlatformUser($user) || $this->availableProjectIdsQuery()->where('projects.id', $projectId)->exists()); } }