415 lines
17 KiB
PHP
415 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Investor;
|
|
use App\Models\Account;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
class InvestorController extends Controller
|
|
{
|
|
public function index()
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
$query = Investor::with('branches');
|
|
if ($user && $user->isReceptionist()) {
|
|
$query->where(function($q) use ($user) {
|
|
$q->where('applicable_to_all_branches', true)
|
|
->orWhereHas('branches', function($bq) use ($user) {
|
|
$bq->where('branches.id', $user->branch_id);
|
|
});
|
|
});
|
|
}
|
|
return response()->json($query->get());
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'investment_date' => 'required|date',
|
|
'investment_amount' => 'required|numeric',
|
|
'applicable_to_all_branches' => 'required|boolean',
|
|
'roi_type' => 'required|string|in:Percentage,Fixed Amount',
|
|
'roi_value' => 'nullable|numeric',
|
|
'roi_period' => 'nullable|string',
|
|
'branch_ids' => 'nullable|array',
|
|
'branch_ids.*' => 'exists:branches,id',
|
|
'security_proof_document' => 'nullable|file|mimes:pdf,png,jpg,jpeg|max:10240',
|
|
], [
|
|
'security_proof_document.file' => 'The security proof document must be a file.',
|
|
'security_proof_document.mimes' => 'The security proof document must be a PDF, PNG, or JPG.',
|
|
]);
|
|
|
|
if ($request->hasFile('security_proof_document')) {
|
|
$path = $request->file('security_proof_document')->store('investor_docs', 'public');
|
|
$validated['security_proof_document'] = $path;
|
|
}
|
|
|
|
$investor = Investor::create($validated);
|
|
|
|
// Log to Accounts
|
|
Account::create([
|
|
'date' => Carbon::now()->toDateString(),
|
|
'time' => Carbon::now()->toTimeString(),
|
|
'branch_id' => $investor->applicable_to_all_branches ? null : $investor->branches->first()?->id,
|
|
'credit' => $investor->investment_amount,
|
|
'debit' => 0,
|
|
'type' => 'investor',
|
|
'accountable_id' => $investor->id,
|
|
'accountable_type' => Investor::class,
|
|
'description' => "Initial investment from {$investor->name}"
|
|
]);
|
|
|
|
if (!$validated['applicable_to_all_branches'] && isset($validated['branch_ids'])) {
|
|
$investor->branches()->attach($validated['branch_ids']);
|
|
}
|
|
|
|
return response()->json($investor->load('branches'), 201);
|
|
}
|
|
|
|
public function show($id)
|
|
{
|
|
$investor = Investor::with('branches')->findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist()) {
|
|
$isLinked = $investor->applicable_to_all_branches || $investor->branches->contains($user->branch_id);
|
|
if (!$isLinked) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
}
|
|
return response()->json($investor);
|
|
}
|
|
|
|
public function update(Request $request, $id)
|
|
{
|
|
$investor = Investor::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist()) {
|
|
$isLinked = $investor->applicable_to_all_branches || $investor->branches->contains($user->branch_id);
|
|
if (!$isLinked) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'name' => 'required|string|max:255',
|
|
'investment_date' => 'required|date',
|
|
'investment_amount' => 'required|numeric',
|
|
'applicable_to_all_branches' => 'required|boolean',
|
|
'roi_type' => 'required|string|in:Percentage,Fixed Amount',
|
|
'roi_value' => 'nullable|numeric',
|
|
'roi_period' => 'nullable|string',
|
|
'branch_ids' => 'nullable|array',
|
|
'branch_ids.*' => 'exists:branches,id',
|
|
'security_proof_document' => 'nullable|file|mimes:pdf,png,jpg,jpeg|max:10240',
|
|
], [
|
|
'security_proof_document.file' => 'The security proof document must be a file.',
|
|
'security_proof_document.mimes' => 'The security proof document must be a PDF, PNG, or JPG.',
|
|
]);
|
|
|
|
// Check if payouts exist before allowing core financial changes
|
|
$hasPayouts = \App\Models\InvestorPayout::where('investor_id', $id)->exists();
|
|
if ($hasPayouts) {
|
|
$coreFields = ['investment_date', 'investment_amount', 'roi_type', 'roi_value', 'roi_period'];
|
|
foreach ($coreFields as $field) {
|
|
if (isset($validated[$field]) && $validated[$field] != $investor->$field) {
|
|
return response()->json([
|
|
'message' => 'Cannot modify core investment terms after payouts have been processed.',
|
|
'errors' => [$field => ['Modification restricted due to existing payouts.']]
|
|
], 422);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($request->hasFile('security_proof_document')) {
|
|
if ($investor->security_proof_document) {
|
|
Storage::disk('public')->delete($investor->security_proof_document);
|
|
}
|
|
$path = $request->file('security_proof_document')->store('investor_docs', 'public');
|
|
$validated['security_proof_document'] = $path;
|
|
}
|
|
|
|
$investor->update($validated);
|
|
|
|
if ($validated['applicable_to_all_branches']) {
|
|
$investor->branches()->detach();
|
|
} elseif (isset($validated['branch_ids'])) {
|
|
$investor->branches()->sync($validated['branch_ids']);
|
|
}
|
|
|
|
return response()->json($investor->load('branches'));
|
|
}
|
|
|
|
public function destroy($id)
|
|
{
|
|
$investor = Investor::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist()) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
|
|
// Delete associated Account records (investments)
|
|
Account::where('accountable_id', $investor->id)
|
|
->where('accountable_type', Investor::class)
|
|
->delete();
|
|
|
|
// Also delete associated payouts and their account entries
|
|
$payouts = \App\Models\InvestorPayout::where('investor_id', $id)->get();
|
|
foreach ($payouts as $payout) {
|
|
Account::where('accountable_id', $payout->id)
|
|
->where('accountable_type', \App\Models\InvestorPayout::class)
|
|
->delete();
|
|
$payout->delete();
|
|
}
|
|
|
|
// Detach branches first to avoid foreign key issues
|
|
$investor->branches()->detach();
|
|
|
|
if ($investor->security_proof_document) {
|
|
Storage::disk('public')->delete($investor->security_proof_document);
|
|
}
|
|
$investor->delete();
|
|
return response()->json(['message' => 'Investor and associated financial records deleted successfully']);
|
|
}
|
|
|
|
public function getPayouts($id)
|
|
{
|
|
$investor = Investor::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist()) {
|
|
$isLinked = $investor->applicable_to_all_branches || $investor->branches->contains($user->branch_id);
|
|
if (!$isLinked) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
}
|
|
$payouts = \App\Models\InvestorPayout::where('investor_id', $id)->orderBy('payout_date', 'desc')->get();
|
|
return response()->json($payouts);
|
|
}
|
|
|
|
public function getROIPayoutStatus($id)
|
|
{
|
|
$investor = Investor::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist()) {
|
|
$isLinked = $investor->applicable_to_all_branches || $investor->branches->contains($user->branch_id);
|
|
if (!$isLinked) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
}
|
|
$investmentDate = Carbon::parse($investor->investment_date)->startOfMonth();
|
|
$currentMonth = Carbon::now()->startOfMonth();
|
|
|
|
// Fetch all payouts for this investor
|
|
$payouts = \App\Models\InvestorPayout::where('investor_id', $id)->get();
|
|
|
|
$status = [];
|
|
$tempMonth = $investmentDate->copy();
|
|
|
|
$periodMonths = 1;
|
|
if ($investor->roi_period === 'Quarterly') $periodMonths = 3;
|
|
if ($investor->roi_period === 'Yearly') $periodMonths = 12;
|
|
|
|
$carryOver = 0;
|
|
$canSettleFound = false;
|
|
|
|
while ($tempMonth->lessThanOrEqualTo($currentMonth)) {
|
|
$monthKey = $tempMonth->format('F Y');
|
|
|
|
$baseROI = round($investor->roi_type === 'Percentage'
|
|
? ($investor->investment_amount * ($investor->roi_value / 100))
|
|
: ($investor->roi_value ?? 0), 2);
|
|
|
|
$targetAmount = $baseROI + $carryOver;
|
|
|
|
// Check if there is a payout recorded for this specific month
|
|
$monthPayouts = $payouts->filter(function($p) use ($monthKey) {
|
|
return $p->payout_month === $monthKey;
|
|
});
|
|
|
|
if ($monthPayouts->isNotEmpty()) {
|
|
$paidForThisMonth = $monthPayouts->sum('amount');
|
|
$status[] = [
|
|
'month' => $monthKey,
|
|
'status' => 'Paid',
|
|
'base_amount' => $baseROI,
|
|
'carry_from_previous' => $carryOver,
|
|
'target_amount' => $targetAmount,
|
|
'paid' => $paidForThisMonth,
|
|
'amount' => 0,
|
|
'can_settle' => false
|
|
];
|
|
// Carry forward the difference
|
|
$carryOver = round($targetAmount - $paidForThisMonth, 2);
|
|
} else {
|
|
$canSettle = false;
|
|
if (!$canSettleFound) {
|
|
$canSettle = true;
|
|
$canSettleFound = true;
|
|
}
|
|
|
|
$status[] = [
|
|
'month' => $monthKey,
|
|
'status' => 'Pending',
|
|
'base_amount' => $baseROI,
|
|
'carry_from_previous' => $carryOver,
|
|
'target_amount' => $targetAmount,
|
|
'paid' => 0,
|
|
'amount' => $targetAmount,
|
|
'can_settle' => $canSettle
|
|
];
|
|
// No carry over update here, it stays until this month is settled
|
|
// Actually, if we don't settle Jan, Feb's target should eventually include Jan's?
|
|
// The user says "Older months must be settled first", so we don't need to accumulate carry automatically
|
|
// across pending months, because you can't skip.
|
|
}
|
|
|
|
$tempMonth->addMonths($periodMonths);
|
|
}
|
|
|
|
return response()->json(array_reverse($status));
|
|
}
|
|
|
|
public function settleROIPayout(Request $request, $id)
|
|
{
|
|
$investor = Investor::findOrFail($id);
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
if ($user && $user->isReceptionist()) {
|
|
$isLinked = $investor->applicable_to_all_branches || $investor->branches->contains($user->branch_id);
|
|
if (!$isLinked) {
|
|
return response()->json(['message' => 'Unauthorized'], 403);
|
|
}
|
|
}
|
|
$validated = $request->validate([
|
|
'payout_month' => 'required|string',
|
|
'amount' => 'required|numeric',
|
|
'payout_date' => 'required|date',
|
|
'payment_method' => 'nullable|string',
|
|
'remarks' => 'nullable|string'
|
|
]);
|
|
|
|
$payout = \App\Models\InvestorPayout::create(array_merge($validated, [
|
|
'investor_id' => $id,
|
|
'status' => 'Paid'
|
|
]));
|
|
|
|
// Log to Accounts
|
|
Account::create([
|
|
'date' => $validated['payout_date'],
|
|
'time' => Carbon::now()->toTimeString(),
|
|
'branch_id' => $investor->applicable_to_all_branches ? null : $investor->branches->first()?->id,
|
|
'credit' => 0,
|
|
'debit' => $validated['amount'],
|
|
'type' => 'payout',
|
|
'accountable_id' => $payout->id,
|
|
'accountable_type' => \App\Models\InvestorPayout::class,
|
|
'description' => "ROI Payout for {$investor->name} - {$validated['payout_month']}"
|
|
]);
|
|
|
|
// Log to Expenses
|
|
$roiCategory = \App\Models\ExpenseCategory::where('name', 'ROI Payout')->first();
|
|
\App\Models\Expense::create([
|
|
'date' => $validated['payout_date'],
|
|
'branch_id' => $investor->applicable_to_all_branches ? null : $investor->branches->first()?->id,
|
|
'expense_category_id' => $roiCategory ? $roiCategory->id : 1,
|
|
'expense_type' => 'Account',
|
|
'amount' => $validated['amount'],
|
|
'remarks' => "ROI Payout to {$investor->name} for {$validated['payout_month']}"
|
|
]);
|
|
|
|
return response()->json(['message' => 'ROI settled successfully', 'payout' => $payout]);
|
|
}
|
|
|
|
public function getAllPendingROIs(Request $request)
|
|
{
|
|
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
|
|
$branchId = $user && $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
|
|
|
|
$query = Investor::with('branches');
|
|
|
|
if ($branchId) {
|
|
$query->where(function($q) use ($branchId) {
|
|
$q->where('applicable_to_all_branches', true)
|
|
->orWhereHas('branches', function($bq) use ($branchId) {
|
|
$bq->where('branches.id', $branchId);
|
|
});
|
|
});
|
|
}
|
|
|
|
$investors = $query->get();
|
|
$pending = [];
|
|
$currentMonth = Carbon::now()->startOfMonth();
|
|
|
|
foreach ($investors as $investor) {
|
|
$investmentDate = Carbon::parse($investor->investment_date)->startOfMonth();
|
|
$payouts = \App\Models\InvestorPayout::where('investor_id', $investor->id)->get();
|
|
$tempMonth = $investmentDate->copy();
|
|
|
|
$periodMonths = 1;
|
|
if ($investor->roi_period === 'Quarterly') $periodMonths = 3;
|
|
if ($investor->roi_period === 'Yearly') $periodMonths = 12;
|
|
|
|
$investorPendingMonths = [];
|
|
$carryOver = 0;
|
|
|
|
while ($tempMonth->lessThanOrEqualTo($currentMonth)) {
|
|
$monthKey = $tempMonth->format('F Y');
|
|
$baseROI = round($investor->roi_type === 'Percentage'
|
|
? ($investor->investment_amount * ($investor->roi_value / 100))
|
|
: ($investor->roi_value ?? 0), 2);
|
|
|
|
$targetAmount = $baseROI + $carryOver;
|
|
|
|
$monthPayouts = $payouts->filter(function($p) use ($monthKey) {
|
|
return $p->payout_month === $monthKey;
|
|
});
|
|
|
|
if ($monthPayouts->isNotEmpty()) {
|
|
$paidForThisMonth = $monthPayouts->sum('amount');
|
|
$carryOver = round($targetAmount - $paidForThisMonth, 2);
|
|
} else {
|
|
$investorPendingMonths[] = [
|
|
'payout_month' => $monthKey,
|
|
'base_amount' => $baseROI,
|
|
'carry_from_previous' => $carryOver,
|
|
'amount' => $targetAmount
|
|
];
|
|
// If a month is missing, we stop accumulating carry for the global view
|
|
// until that month is settled, to avoid confusing numbers.
|
|
// Or should we? Let's stay consistent with getROIPayoutStatus.
|
|
$carryOver = 0;
|
|
}
|
|
$tempMonth->addMonths($periodMonths);
|
|
}
|
|
|
|
if (!empty($investorPendingMonths)) {
|
|
$totalPending = array_sum(array_column($investorPendingMonths, 'amount'));
|
|
|
|
$pending[] = [
|
|
'investor_id' => $investor->id,
|
|
'investor_name' => $investor->name,
|
|
'investment_amount' => $investor->investment_amount,
|
|
'roi_percentage' => $investor->roi_value,
|
|
'roi_type' => $investor->roi_type,
|
|
'roi_period' => $investor->roi_period,
|
|
'pending_count' => count($investorPendingMonths),
|
|
'total_pending' => $totalPending,
|
|
'pending_months' => $investorPendingMonths
|
|
];
|
|
}
|
|
}
|
|
|
|
return response()->json($pending);
|
|
}
|
|
|
|
public function storePayout(Request $request, $id)
|
|
{
|
|
// ... (keep existing storePayout or just use settleROI if we want unity)
|
|
// I'll keep it for now but settleROI is more complete with accounts/expenses
|
|
return $this->settleROIPayout($request, $id);
|
|
}
|
|
}
|