392 lines
15 KiB
PHP
392 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Finance;
|
|
|
|
use App\Http\Controllers\Api\BaseApiController;
|
|
use App\Http\Requests\Files\FileNameRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementBatchAdminFileRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementBatchAssignmentRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementBatchCreateRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementBatchEmailRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementBatchLockRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementExportBatchRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementExportRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementIndexRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementMarkDonationRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementProcessRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementStoreRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementUnderProcessingRequest;
|
|
use App\Http\Requests\Reimbursements\ReimbursementUpdateRequest;
|
|
use App\Http\Resources\Reimbursements\ReimbursementBatchResource;
|
|
use App\Http\Resources\Reimbursements\ReimbursementExpenseResource;
|
|
use App\Http\Resources\Reimbursements\ReimbursementRecipientResource;
|
|
use App\Http\Resources\Reimbursements\ReimbursementResource;
|
|
use App\Http\Resources\Reimbursements\ReimbursementUnderProcessingItemResource;
|
|
use App\Models\Reimbursement;
|
|
use App\Models\ReimbursementBatch;
|
|
use App\Services\Files\FileServeService;
|
|
use App\Services\Reimbursements\ReimbursementBatchAssignmentService;
|
|
use App\Services\Reimbursements\ReimbursementBatchService;
|
|
use App\Services\Reimbursements\ReimbursementContextService;
|
|
use App\Services\Reimbursements\ReimbursementCrudService;
|
|
use App\Services\Reimbursements\ReimbursementDonationService;
|
|
use App\Services\Reimbursements\ReimbursementEmailService;
|
|
use App\Services\Reimbursements\ReimbursementExportService;
|
|
use App\Services\Reimbursements\ReimbursementFileService;
|
|
use App\Services\Reimbursements\ReimbursementQueryService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use RuntimeException;
|
|
|
|
class ReimbursementController extends BaseApiController
|
|
{
|
|
public function __construct(
|
|
private ReimbursementContextService $context,
|
|
private ReimbursementQueryService $queryService,
|
|
private ReimbursementDonationService $donationService,
|
|
private ReimbursementBatchService $batchService,
|
|
private ReimbursementBatchAssignmentService $assignmentService,
|
|
private ReimbursementFileService $fileService,
|
|
private ReimbursementExportService $exportService,
|
|
private ReimbursementEmailService $emailService,
|
|
private ReimbursementCrudService $crudService,
|
|
private FileServeService $fileServeService
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
public function underProcessing(ReimbursementUnderProcessingRequest $request): JsonResponse
|
|
{
|
|
$data = $this->queryService->underProcessing();
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'pendingItems' => ReimbursementUnderProcessingItemResource::collection($data['pendingItems'] ?? []),
|
|
'existingBatches' => ReimbursementBatchResource::collection($data['existingBatches'] ?? []),
|
|
'adminUsers' => ReimbursementRecipientResource::collection($data['adminUsers'] ?? []),
|
|
'itemsPayload' => ReimbursementUnderProcessingItemResource::collection($data['itemsPayload'] ?? []),
|
|
]);
|
|
}
|
|
|
|
public function markDonation(ReimbursementMarkDonationRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$userId = (int) (auth()->id() ?? 0);
|
|
|
|
try {
|
|
$this->donationService->markDonation((int) $payload['expense_id'], $userId);
|
|
} catch (RuntimeException $e) {
|
|
$message = $e->getMessage();
|
|
$status = str_contains($message, 'not found') ? 404 : (str_contains($message, 'already') ? 409 : 422);
|
|
return response()->json(['ok' => false, 'message' => $message], $status);
|
|
} catch (\Throwable $e) {
|
|
return response()->json(['ok' => false, 'message' => 'Unable to mark donation right now.'], 500);
|
|
}
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
|
|
public function createBatch(ReimbursementBatchCreateRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$userId = (int) (auth()->id() ?? 0);
|
|
$schoolYear = $this->context->getSchoolYear();
|
|
$semester = $this->context->getSemester();
|
|
|
|
$batch = $this->batchService->createBatch($payload['title'] ?? null, $userId, $schoolYear, $semester);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'batch' => $batch,
|
|
], 201);
|
|
}
|
|
|
|
public function updateBatchAssignment(ReimbursementBatchAssignmentRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
|
|
$batchId = (int) ($payload['batch_id'] ?? 0);
|
|
$batchNumber = $payload['batch_number'] ?? null;
|
|
if ($batchId <= 0 && $batchNumber !== null && trim((string) $batchNumber) !== '') {
|
|
$batchId = (int) $batchNumber;
|
|
}
|
|
|
|
$adminIdRaw = $payload['admin_id'] ?? null;
|
|
$adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;
|
|
$reimbursementId = !empty($payload['reimbursement_id']) ? (int) $payload['reimbursement_id'] : null;
|
|
|
|
try {
|
|
$result = $this->assignmentService->updateAssignment(
|
|
(int) $payload['expense_id'],
|
|
$batchId,
|
|
$adminId,
|
|
$reimbursementId,
|
|
$this->context->getSchoolYear(),
|
|
$this->context->getSemester()
|
|
);
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'assignment' => $result,
|
|
]);
|
|
}
|
|
|
|
public function lockBatch(ReimbursementBatchLockRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$userId = (int) (auth()->id() ?? 0);
|
|
|
|
try {
|
|
$this->batchService->lockBatch(
|
|
(int) $payload['batch_id'],
|
|
$userId,
|
|
$this->context->getSchoolYear(),
|
|
$this->context->getSemester()
|
|
);
|
|
} catch (RuntimeException $e) {
|
|
$message = $e->getMessage();
|
|
$status = str_contains($message, 'check file') ? 409 : 404;
|
|
return response()->json(['ok' => false, 'message' => $message], $status);
|
|
} catch (\Throwable $e) {
|
|
return response()->json(['ok' => false, 'message' => 'Unable to lock batch right now.'], 500);
|
|
}
|
|
|
|
return response()->json(['ok' => true, 'status' => 'closed']);
|
|
}
|
|
|
|
public function uploadBatchAdminFile(ReimbursementBatchAdminFileRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$batchId = (int) $payload['batch_id'];
|
|
$adminId = (int) $payload['admin_id'];
|
|
|
|
$batch = ReimbursementBatch::query()->find($batchId);
|
|
if (!$batch) {
|
|
return response()->json(['ok' => false, 'message' => 'Batch not found.'], 404);
|
|
}
|
|
if (strtolower((string) ($batch->status ?? '')) !== ReimbursementBatch::STATUS_OPEN) {
|
|
return response()->json(['ok' => false, 'message' => 'Cannot upload files for a locked batch.'], 409);
|
|
}
|
|
|
|
try {
|
|
$file = $this->fileService->storeAdminFile($batchId, $adminId, $request->file('check_file'), (int) (auth()->id() ?? 0));
|
|
} catch (\Throwable $e) {
|
|
return response()->json(['ok' => false, 'message' => 'Unable to store the uploaded file.'], 500);
|
|
}
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'file' => $file,
|
|
]);
|
|
}
|
|
|
|
public function serveAdminCheckFile(FileNameRequest $request, string $name, string $mode = 'inline'): Response|JsonResponse
|
|
{
|
|
$allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'];
|
|
$meta = $this->fileServeService->meta(storage_path('uploads/reimbursements'), $name, $allowedExtensions, null);
|
|
$disposition = strtolower(trim($mode)) === 'download' ? 'attachment' : 'inline';
|
|
|
|
return response(file_get_contents($meta['path']), 200, [
|
|
'Content-Type' => $meta['mime'],
|
|
'Content-Disposition' => $disposition . '; filename="' . $meta['download_name'] . '"',
|
|
'Content-Length' => (string) $meta['size'],
|
|
'ETag' => $meta['etag'],
|
|
'Last-Modified' => $meta['last_modified'],
|
|
]);
|
|
}
|
|
|
|
public function index(ReimbursementIndexRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$filters = [
|
|
'school_year' => $payload['school_year'] ?? null,
|
|
'semester' => $payload['semester'] ?? null,
|
|
'status' => $payload['status'] ?? null,
|
|
'user_id' => $payload['user_id'] ?? null,
|
|
];
|
|
|
|
$data = $this->queryService->index($filters);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'expenses' => ReimbursementExpenseResource::collection($data['expenses'] ?? []),
|
|
'users' => ReimbursementRecipientResource::collection($data['users'] ?? []),
|
|
'schoolYears' => $data['schoolYears'] ?? [],
|
|
'batchSummaries' => $data['batchSummaries'] ?? [],
|
|
'batchDetails' => $data['batchDetails'] ?? [],
|
|
'batchAttachments' => $data['batchAttachments'] ?? [],
|
|
'donationBatch' => $data['donationBatch'] ?? null,
|
|
'donationDetails' => $data['donationDetails'] ?? null,
|
|
]);
|
|
}
|
|
|
|
public function sendBatchEmail(ReimbursementBatchEmailRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$receiptIds = $payload['receipts'] ?? [];
|
|
$checkIds = $payload['checks'] ?? [];
|
|
|
|
try {
|
|
$ok = $this->emailService->sendBatchEmail(
|
|
(int) $payload['batch_id'],
|
|
$payload['recipient_email'],
|
|
(string) ($payload['message'] ?? ''),
|
|
$receiptIds,
|
|
$checkIds
|
|
);
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 404);
|
|
}
|
|
|
|
if (!$ok) {
|
|
return response()->json(['ok' => false, 'message' => 'Failed to send email. Please try again.'], 500);
|
|
}
|
|
|
|
return response()->json(['ok' => true, 'message' => 'Email sent successfully.']);
|
|
}
|
|
|
|
public function export(ReimbursementExportRequest $request): Response
|
|
{
|
|
$payload = $request->validated();
|
|
$type = $payload['type'] ?? 'processed';
|
|
|
|
if ($type === 'under_processing') {
|
|
$csv = $this->exportService->buildUnderProcessingCsv();
|
|
} else {
|
|
$csv = $this->exportService->buildProcessedCsv($payload);
|
|
}
|
|
|
|
return response()->streamDownload(function () use ($csv) {
|
|
$out = fopen('php://output', 'w');
|
|
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
|
foreach ($csv['rows'] as $row) {
|
|
fputcsv($out, $row);
|
|
}
|
|
fclose($out);
|
|
}, $csv['filename'], [
|
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
|
]);
|
|
}
|
|
|
|
public function exportBatch(ReimbursementExportBatchRequest $request): Response
|
|
{
|
|
$payload = $request->validated();
|
|
$csv = $this->exportService->buildBatchCsv((int) $payload['batch_id']);
|
|
|
|
return response()->streamDownload(function () use ($csv) {
|
|
$out = fopen('php://output', 'w');
|
|
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
|
foreach ($csv['rows'] as $row) {
|
|
fputcsv($out, $row);
|
|
}
|
|
fclose($out);
|
|
}, $csv['filename'], [
|
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
|
]);
|
|
}
|
|
|
|
public function store(ReimbursementStoreRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$userId = (int) (auth()->id() ?? 0);
|
|
$receiptName = null;
|
|
|
|
if ($request->hasFile('receipt')) {
|
|
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
|
|
}
|
|
|
|
try {
|
|
$reimb = $this->crudService->store(
|
|
$payload,
|
|
$userId,
|
|
$this->context->getSchoolYear(),
|
|
$this->context->getSemester(),
|
|
$receiptName
|
|
);
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
$data = $reimb->toArray();
|
|
$data['receipt_url'] = $this->fileService->receiptUrl($reimb->receipt_path ?? null);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'reimbursement' => new ReimbursementResource($data),
|
|
], 201);
|
|
}
|
|
|
|
public function process(ReimbursementProcessRequest $request): JsonResponse
|
|
{
|
|
$payload = $request->validated();
|
|
$userId = (int) (auth()->id() ?? 0);
|
|
$receiptName = null;
|
|
|
|
if ($request->hasFile('receipt')) {
|
|
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
|
|
}
|
|
|
|
try {
|
|
$reimb = $this->crudService->process(
|
|
$payload,
|
|
$userId,
|
|
$this->context->getSchoolYear(),
|
|
$this->context->getSemester(),
|
|
$receiptName
|
|
);
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
$data = $reimb->toArray();
|
|
$data['receipt_url'] = $this->fileService->receiptUrl($reimb->receipt_path ?? null);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'reimbursement' => new ReimbursementResource($data),
|
|
], 201);
|
|
}
|
|
|
|
public function reimbursedExpenses(): JsonResponse
|
|
{
|
|
$expenses = $this->queryService->reimbursedExpenses();
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'expenses' => $expenses,
|
|
]);
|
|
}
|
|
|
|
public function update(ReimbursementUpdateRequest $request, int $id): JsonResponse
|
|
{
|
|
$reimb = Reimbursement::query()->find($id);
|
|
if (!$reimb) {
|
|
return response()->json(['ok' => false, 'message' => 'Reimbursement not found.'], 404);
|
|
}
|
|
|
|
$payload = $request->validated();
|
|
$userId = (int) (auth()->id() ?? 0);
|
|
|
|
$receiptName = $reimb->receipt_path;
|
|
if ($request->hasFile('receipt')) {
|
|
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
|
|
}
|
|
if (!empty($payload['remove_receipt'])) {
|
|
$receiptName = null;
|
|
}
|
|
|
|
$updated = $this->crudService->update($reimb, $payload, $userId, $receiptName);
|
|
$data = $updated->toArray();
|
|
$data['receipt_url'] = $this->fileService->receiptUrl($updated->receipt_path ?? null);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'reimbursement' => new ReimbursementResource($data),
|
|
]);
|
|
}
|
|
}
|