201 lines
7.3 KiB
PHP
201 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\TripExpense;
|
|
use App\Models\Trip;
|
|
use App\Models\User;
|
|
use App\Services\EmployeeService;
|
|
use Illuminate\Http\Request;
|
|
use Exception;
|
|
|
|
class ReimbursementApiController extends Controller
|
|
{
|
|
protected EmployeeService $employeeService;
|
|
|
|
public function __construct(EmployeeService $employeeService)
|
|
{
|
|
$this->employeeService = $employeeService;
|
|
}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
$user = $request->user();
|
|
|
|
$expenses = TripExpense::with('trip')
|
|
->where('created_by', $user->id)
|
|
->orderBy('expense_date', 'desc')
|
|
->get();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $expenses->map(function ($e) {
|
|
$rawStatus = strtolower($e->status ?? ($e->trip->reimbursement_status ?? ($e->trip->status ?? 'pending')));
|
|
if (in_array($rawStatus, ['approved', 'paid', 'completed', 'reconciled'])) {
|
|
$status = 'approved';
|
|
} elseif (in_array($rawStatus, ['rejected', 'cancelled'])) {
|
|
$status = 'rejected';
|
|
} else {
|
|
$status = 'pending';
|
|
}
|
|
|
|
return [
|
|
'id' => $e->id,
|
|
'item_name' => $e->description ?? $e->expense_type,
|
|
'amount' => (float)$e->amount,
|
|
'category' => $e->expense_type ?? 'General',
|
|
'purchase_date' => $e->expense_date ? $e->expense_date->format('Y-m-d') : date('Y-m-d'),
|
|
'status' => $status,
|
|
'raw_status' => $rawStatus,
|
|
'receipt_url' => $e->receipt ? asset('storage/' . $e->receipt) : null,
|
|
'notes' => $e->description,
|
|
];
|
|
}),
|
|
'message' => null,
|
|
'errors' => null,
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request)
|
|
{
|
|
$request->validate([
|
|
'item_name' => 'required|string',
|
|
'amount' => 'required|numeric|min:0',
|
|
'category' => 'nullable|string',
|
|
'purchase_date' => 'required|date',
|
|
'notes' => 'nullable|string',
|
|
'receipt' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:10240',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$employee = $this->employeeService->resolveEmployee($user);
|
|
|
|
$receiptPath = null;
|
|
if ($request->hasFile('receipt')) {
|
|
$receiptPath = $request->file('receipt')->store('receipts', 'public');
|
|
}
|
|
|
|
$trip = Trip::firstOrCreate(
|
|
['employee_id' => $user->id, 'purpose' => 'General Employee Expenses'],
|
|
[
|
|
'title' => 'Reimbursement Trip for ' . $user->name,
|
|
'destination' => 'Local',
|
|
'start_date' => date('Y-m-d'),
|
|
'end_date' => date('Y-m-d'),
|
|
'status' => 'approved',
|
|
'created_by' => $user->id,
|
|
]
|
|
);
|
|
|
|
$expense = TripExpense::create([
|
|
'trip_id' => $trip->id,
|
|
'expense_type' => $request->category ?? 'General',
|
|
'expense_date' => $request->purchase_date,
|
|
'amount' => $request->amount,
|
|
'currency' => 'PHP',
|
|
'description' => $request->item_name . ($request->notes ? ' - ' . $request->notes : ''),
|
|
'receipt' => $receiptPath,
|
|
'is_reimbursable' => true,
|
|
'status' => 'pending',
|
|
'created_by' => $user->id,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Reimbursement claim submitted successfully',
|
|
'data' => [
|
|
'id' => $expense->id,
|
|
'item_name' => $request->item_name,
|
|
'amount' => (float)$expense->amount,
|
|
'category' => $expense->expense_type,
|
|
'purchase_date' => $expense->expense_date ? $expense->expense_date->format('Y-m-d') : date('Y-m-d'),
|
|
'status' => $expense->status,
|
|
'receipt_url' => $receiptPath ? asset('storage/' . $receiptPath) : null,
|
|
],
|
|
'errors' => null,
|
|
], 201);
|
|
}
|
|
|
|
public function updateStatus(Request $request, $id)
|
|
{
|
|
$request->validate([
|
|
'status' => 'required|string|in:approved,rejected,pending,cancelled',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$expense = TripExpense::find($id);
|
|
|
|
if (!$expense) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => 'Reimbursement claim not found',
|
|
'errors' => null,
|
|
], 404);
|
|
}
|
|
|
|
// Authorization checks
|
|
if ($request->status === 'cancelled') {
|
|
if ($expense->created_by !== $user->id && !in_array($user->type, ['company', 'hr', 'superadmin', 'manager'])) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => 'Unauthorized to cancel this claim',
|
|
'errors' => null,
|
|
], 403);
|
|
}
|
|
} else { // approved or rejected
|
|
$userType = strtolower($user->type ?? '');
|
|
$isElevatedRole = in_array($userType, ['company', 'hr', 'superadmin', 'manager']);
|
|
|
|
if (!$isElevatedRole) {
|
|
try {
|
|
$isElevatedRole = $user->hasAnyRole(['company', 'hr', 'superadmin', 'manager']);
|
|
} catch (\Throwable $e) {
|
|
$isElevatedRole = false;
|
|
}
|
|
}
|
|
|
|
$isSelf = ($expense->created_by === $user->id) && ($userType === 'employee' || !$isElevatedRole);
|
|
|
|
$appCreator = User::find($expense->created_by)
|
|
?? Employee::find($expense->created_by)?->user;
|
|
|
|
$appCompanyId = $appCreator ? ($appCreator->type === 'company' ? $appCreator->id : ($appCreator->created_by ?: $appCreator->id)) : null;
|
|
$approverCompanyId = $user->type === 'company' ? $user->id : ($user->created_by ?: $user->id);
|
|
|
|
$isSuperAdmin = in_array($userType, ['superadmin']);
|
|
$isSameCompany = ($appCompanyId !== null && $approverCompanyId !== null && (int)$appCompanyId === (int)$approverCompanyId);
|
|
|
|
if (!$isElevatedRole || $isSelf || (!$isSameCompany && !$isSuperAdmin)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'data' => null,
|
|
'message' => 'Unauthorized to review this reimbursement claim. Approval requires authorized manager or HR within the same organization.',
|
|
'errors' => null,
|
|
], 403);
|
|
}
|
|
}
|
|
|
|
$expense->update(['status' => $request->status]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'Reimbursement claim status updated to ' . $request->status,
|
|
'data' => $expense,
|
|
'errors' => null,
|
|
]);
|
|
}
|
|
|
|
public function approve(Request $request, $id)
|
|
{
|
|
return $this->updateStatus($request->merge(['status' => 'approved']), $id);
|
|
}
|
|
|
|
public function reject(Request $request, $id)
|
|
{
|
|
return $this->updateStatus($request->merge(['status' => 'rejected']), $id);
|
|
}
|
|
}
|