296 lines
10 KiB
PHP
296 lines
10 KiB
PHP
<?php
|
|
|
|
namespace Modules\FinancialManagement\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use Modules\FinancialManagement\Enums\InvoiceStatus;
|
|
use Modules\FinancialManagement\Models\FinancialInvoice;
|
|
use Modules\FinancialManagement\Models\InvoiceLineItem;
|
|
use Modules\FinancialManagement\Models\RetentionEntry;
|
|
use Modules\FinancialManagement\Services\ProgressBillingService;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
|
|
use Modules\ApprovalWorkflow\Services\ApprovalService;
|
|
use App\Models\User;
|
|
|
|
class FinanceController extends Controller
|
|
{
|
|
public function __construct(
|
|
private ProgressBillingService $billingService,
|
|
private ApprovalService $approvalService,
|
|
) {}
|
|
|
|
// --- Invoice List ---
|
|
public function index(Request $request)
|
|
{
|
|
$query = FinancialInvoice::with('project:id,name,code');
|
|
|
|
if ($status = $request->status) {
|
|
$query->where('status', $status);
|
|
}
|
|
if ($projectId = $request->project_id) {
|
|
$query->where('project_id', $projectId);
|
|
}
|
|
|
|
$invoices = $query->latest()->paginate(15)->withQueryString();
|
|
$projects = Project::select('id', 'ulid', 'name', 'code')->get();
|
|
|
|
// Compute summary stats
|
|
$allInvoices = FinancialInvoice::query();
|
|
$summary = [
|
|
'total_billed' => (float) $allInvoices->sum('total_amount'),
|
|
'total_paid' => (float) $allInvoices->sum('paid_amount'),
|
|
'outstanding' => (float) $allInvoices->whereNotIn('status', ['paid'])->sum(\DB::raw('total_amount - paid_amount')),
|
|
'total_retention' => (float) RetentionEntry::where('type', 'debit')->sum('amount')
|
|
- (float) RetentionEntry::where('type', 'credit')->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()
|
|
{
|
|
$projects = Project::select('id', 'ulid', 'name', 'code', 'contract_value', 'last_billed_percentage')
|
|
->where('current_wizard_step', '>=', 7)
|
|
->whereNotIn('status', ['completed', 'closed'])
|
|
->get();
|
|
|
|
return Inertia::render('FinancialManagement::Invoices/Create', [
|
|
'projects' => $projects,
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$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)
|
|
{
|
|
$invoice->load(['project:id,name,code', 'lineItems', 'retentionEntries']);
|
|
|
|
return Inertia::render('FinancialManagement::Invoices/Show', [
|
|
'invoice' => $invoice,
|
|
]);
|
|
}
|
|
|
|
// --- State Transitions ---
|
|
public function submit(Request $request, FinancialInvoice $invoice)
|
|
{
|
|
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']);
|
|
})
|
|
->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.');
|
|
}
|
|
|
|
public function approve(FinancialInvoice $invoice)
|
|
{
|
|
$user = auth()->user();
|
|
$isApprover = $user->user_type === 'admin' ||
|
|
$user->roles()->whereIn('name', ['Super Admin', 'admin'])->exists();
|
|
|
|
if (!$isApprover) {
|
|
return back()->with('error', 'Unauthorized. Only Admin or Super Admin 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();
|
|
$isApprover = $user->user_type === 'admin' ||
|
|
$user->roles()->whereIn('name', ['Super Admin', 'admin'])->exists();
|
|
|
|
if (!$isApprover) {
|
|
return back()->with('error', 'Unauthorized. Only Admin or Super Admin 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)
|
|
{
|
|
try {
|
|
$invoice->transitionTo(InvoiceStatus::Sent);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Invoice sent to client.');
|
|
}
|
|
|
|
public function recordPayment(Request $request, FinancialInvoice $invoice)
|
|
{
|
|
$validated = $request->validate([
|
|
'amount' => 'required|numeric|min:0.01',
|
|
]);
|
|
|
|
try {
|
|
$this->billingService->recordPayment($invoice, $validated['amount']);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Payment recorded.');
|
|
}
|
|
|
|
// --- Retention Ledger ---
|
|
public function retention(Request $request)
|
|
{
|
|
$query = RetentionEntry::with('project:id,name,code', 'invoice:id,invoice_number');
|
|
|
|
if ($projectId = $request->project_id) {
|
|
$query->where('project_id', $projectId);
|
|
}
|
|
|
|
$entries = $query->latest()->paginate(20)->withQueryString();
|
|
$projects = Project::select('id', 'ulid', 'name', 'code')->get();
|
|
|
|
// Compute per-project totals
|
|
$projectTotals = RetentionEntry::selectRaw('project_id, type, SUM(amount) as total')
|
|
->groupBy('project_id', 'type')
|
|
->get()
|
|
->groupBy('project_id')
|
|
->map(function ($items) {
|
|
$debits = $items->where('type', 'debit')->sum('total');
|
|
$credits = $items->where('type', 'credit')->sum('total');
|
|
return ['held' => (float) $debits, 'released' => (float) $credits, 'balance' => (float) $debits - (float) $credits];
|
|
});
|
|
|
|
return Inertia::render('FinancialManagement::Retention/Index', [
|
|
'entries' => $entries,
|
|
'projects' => $projects,
|
|
'projectTotals' => $projectTotals,
|
|
'filters' => $request->only(['project_id']),
|
|
]);
|
|
}
|
|
|
|
// Cash Advances
|
|
public function cashAdvances(Request $request)
|
|
{
|
|
$query = \Modules\FinancialManagement\Models\CashAdvance::with(['project:id,name,code', 'requester:id,name,email', 'approver:id,name,email']);
|
|
|
|
if ($projectId = $request->project_id) {
|
|
$query->where('project_id', $projectId);
|
|
}
|
|
|
|
$cashAdvances = $query->latest()->paginate(20)->withQueryString();
|
|
$projects = Project::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::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(\Modules\FinancialManagement\Models\CashAdvance $cashAdvance)
|
|
{
|
|
$user = auth()->user();
|
|
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 = $user->user_type === 'admin' ||
|
|
$user->roles()->whereIn('name', ['Super Admin', 'admin', 'Project Manager', 'Main Contractor Admin'])->exists();
|
|
|
|
if (! $isApprover) {
|
|
return back()->with('error', 'Unauthorized to approve cash advance requests.');
|
|
}
|
|
|
|
$cashAdvance->update([
|
|
'status' => 'approved',
|
|
'approved_by' => auth()->id(),
|
|
]);
|
|
|
|
return back()->with('success', 'Cash advance request approved.');
|
|
}
|
|
}
|