florida_gym/app/Http/Controllers/InvestorController.php
2026-03-13 10:08:46 +05:30

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);
}
}