add controllers, servoices

This commit is contained in:
root
2026-03-09 02:52:13 -04:00
parent c8de5f7edc
commit d76c871cb7
501 changed files with 34439 additions and 21843 deletions
@@ -0,0 +1,218 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Finance\FinancialReportRequest;
use App\Http\Requests\Finance\FinancialSummaryRequest;
use App\Http\Requests\Finance\FinancialUnpaidParentsRequest;
use App\Http\Resources\Finance\FinancialReportResource;
use App\Http\Resources\Finance\FinancialSummaryResource;
use App\Http\Resources\Finance\FinancialUnpaidParentResource;
use App\Services\Finance\FinancialPdfReportService;
use App\Services\Finance\FinancialReportService;
use App\Services\Finance\FinancialSchoolYearService;
use App\Services\Finance\FinancialSummaryService;
use App\Services\Finance\FinancialUnpaidParentsService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FinancialController extends BaseApiController
{
public function __construct(
private FinancialReportService $reportService,
private FinancialSummaryService $summaryService,
private FinancialUnpaidParentsService $unpaidParentsService,
private FinancialSchoolYearService $schoolYears,
private FinancialPdfReportService $pdfService
) {
parent::__construct();
}
public function report(FinancialReportRequest $request): JsonResponse
{
$payload = $request->validated();
$report = $this->reportService->getReport(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'report' => new FinancialReportResource($report),
]);
}
public function summary(FinancialSummaryRequest $request): JsonResponse
{
$payload = $request->validated();
$summary = $this->summaryService->getSummary(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$schoolYears = $this->schoolYears->listYears($summary['schoolYear'] ?? null);
return response()->json([
'ok' => true,
'summary' => new FinancialSummaryResource($summary),
'schoolYears' => $schoolYears,
]);
}
public function unpaidParents(FinancialUnpaidParentsRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->unpaidParentsService->getUnpaidParents($payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'schoolYear' => $data['schoolYear'] ?? null,
'schoolYears' => $data['schoolYears'] ?? [],
'results' => FinancialUnpaidParentResource::collection($data['results'] ?? []),
]);
}
public function downloadCsv(FinancialReportRequest $request): StreamedResponse
{
$payload = $request->validated();
$report = $this->reportService->getReport(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$paymentsMap = [];
foreach ($report['payments'] as $payment) {
$invoiceId = (int) ($payment['invoice_id'] ?? 0);
$paymentsMap[$invoiceId] = (float) ($payment['paid_amount'] ?? 0);
}
$refundsMap = [];
foreach ($report['refunds'] as $refund) {
$invoiceId = (int) ($refund['invoice_id'] ?? 0);
$refundsMap[$invoiceId] = (float) ($refund['total_refunded'] ?? 0);
}
$discountsMap = [];
foreach ($report['discounts'] as $discount) {
$invoiceId = (int) ($discount['invoice_id'] ?? 0);
$discountsMap[$invoiceId] = (float) ($discount['discount_amount'] ?? 0);
}
$breakdown = $report['paymentBreakdown'] ?? [];
$filename = 'financial_report_' . date('Ymd_His') . '.csv';
return response()->streamDownload(function () use ($report, $paymentsMap, $refundsMap, $discountsMap, $breakdown) {
$out = fopen('php://output', 'w');
fputcsv($out, ['Invoice #', 'Parent', 'School Year', 'Total', 'Paid', 'Cash', 'Credit', 'Check', 'Balance', 'Refund', 'Discount', 'Status']);
$sumTotal = 0.0;
$sumPaid = 0.0;
$sumCash = 0.0;
$sumCredit = 0.0;
$sumCheck = 0.0;
$sumBalance = 0.0;
$sumRefund = 0.0;
$sumDiscount = 0.0;
foreach ($report['invoices'] as $inv) {
$invoiceId = (int) ($inv['id'] ?? 0);
$paid = (float) ($paymentsMap[$invoiceId] ?? 0);
$bd = $breakdown[$invoiceId] ?? [];
$cash = (float) ($bd['cash'] ?? 0);
$credit = (float) ($bd['credit'] ?? 0);
$check = (float) ($bd['check'] ?? 0);
$refunded = (float) ($refundsMap[$invoiceId] ?? 0);
$discount = (float) ($discountsMap[$invoiceId] ?? 0);
$total = (float) ($inv['total_amount'] ?? 0);
$balance = $total - $paid - $discount - $refunded;
if ($balance < 0) {
$balance = 0.0;
}
$status = $balance === 0.0 ? 'Paid' : 'Unpaid';
fputcsv($out, [
$inv['invoice_number'] ?? '',
$inv['parent_name'] ?? '',
$inv['school_year'] ?? '',
$total,
$paid,
$cash,
$credit,
$check,
$balance,
$refunded,
$discount,
$status,
]);
$sumTotal += $total;
$sumPaid += $paid;
$sumCash += $cash;
$sumCredit += $credit;
$sumCheck += $check;
$sumBalance += $balance;
$sumRefund += $refunded;
$sumDiscount += $discount;
}
fputcsv($out, [
'TOTALS',
'',
'',
$sumTotal,
$sumPaid,
$sumCash,
$sumCredit,
$sumCheck,
$sumBalance,
$sumRefund,
$sumDiscount,
'',
]);
fputcsv($out, []);
fputcsv($out, ['Expenses Summary']);
fputcsv($out, ['Category', 'Total Amount']);
foreach ($report['expenses'] as $exp) {
fputcsv($out, [
$exp['category'] ?? '',
$exp['total_amount'] ?? 0,
]);
}
fputcsv($out, []);
fputcsv($out, ['Reimbursements Summary']);
fputcsv($out, ['Status', 'Total Amount']);
foreach ($report['reimbursements'] as $row) {
fputcsv($out, [
$row['status'] ?? '',
$row['total_amount'] ?? 0,
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv']);
}
public function downloadPdf(FinancialReportRequest $request): StreamedResponse
{
$payload = $request->validated();
$summary = $this->summaryService->getSummary(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$pdfBytes = $this->pdfService->buildPdf($summary);
$schoolYear = $summary['schoolYear'] ?? 'report';
return response()->streamDownload(function () use ($pdfBytes) {
echo $pdfBytes;
}, 'Financial_Report_' . $schoolYear . '.pdf', ['Content-Type' => 'application/pdf']);
}
}
@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Invoices\InvoiceGenerateRequest;
use App\Http\Requests\Invoices\InvoiceManagementRequest;
use App\Http\Requests\Invoices\InvoiceParentRequest;
use App\Http\Requests\Invoices\InvoiceStatusRequest;
use App\Http\Resources\Invoices\InvoiceManagementParentResource;
use App\Http\Resources\Invoices\InvoiceResource;
use App\Models\Invoice;
use App\Services\Invoices\InvoiceGenerationService;
use App\Services\Invoices\InvoiceManagementService;
use App\Services\Invoices\InvoicePaymentService;
use App\Services\Invoices\InvoicePdfService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class InvoiceController extends BaseApiController
{
public function __construct(
private InvoiceManagementService $management,
private InvoiceGenerationService $generation,
private InvoicePaymentService $paymentService,
private InvoicePdfService $pdfService
) {
parent::__construct();
}
public function management(InvoiceManagementRequest $request): JsonResponse
{
$schoolYear = $request->validated()['school_year'] ?? null;
$schoolYear = $schoolYear ?: $request->input('schoolYear');
$schoolYear = $schoolYear ?: $request->input('year');
$data = $this->management->getManagementData($schoolYear);
return response()->json([
'ok' => true,
'schoolYear' => $data['schoolYear'],
'schoolYears' => $data['schoolYears'],
'invoices' => InvoiceManagementParentResource::collection($data['invoices']),
]);
}
public function generate(InvoiceGenerateRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->generation->generateInvoice((int) $payload['parent_id'], $payload['school_year'] ?? null);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to generate invoice.'], 422);
}
return response()->json([
'ok' => true,
'updated' => $result['updated'] ?? false,
'updated_ids' => $result['updated_ids'] ?? [],
'insert_id' => $result['insert_id'] ?? null,
]);
}
public function byParent(InvoiceParentRequest $request, int $parentId): JsonResponse
{
$schoolYear = $request->validated()['school_year'] ?? null;
$invoices = Invoice::getInvoicesByParentId($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($invoices),
]);
}
public function parentPayment(InvoiceParentRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$schoolYear = $request->validated()['school_year'] ?? null;
$data = $this->paymentService->getParentInvoiceSummary($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($data['invoices']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
'currentSchoolYear' => $data['currentSchoolYear'],
'dueDate' => $data['dueDate'],
]);
}
public function updateStatus(InvoiceStatusRequest $request, int $invoiceId): JsonResponse
{
$status = $request->validated()['status'];
$updated = Invoice::updateInvoiceStatus($invoiceId, $status);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Invoice not found.'], 404);
}
return response()->json(['ok' => true, 'status' => $status]);
}
public function unpaid(): JsonResponse
{
$rows = Invoice::getUnpaidInvoices();
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($rows),
]);
}
public function pdf(int $invoiceId): StreamedResponse
{
$pdfBytes = $this->pdfService->buildPdf($invoiceId);
return response()->streamDownload(function () use ($pdfBytes) {
echo $pdfBytes;
}, 'invoice_' . $invoiceId . '.pdf', ['Content-Type' => 'application/pdf']);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentByParentRequest;
use App\Http\Requests\Payments\PaymentCreateRequest;
use App\Http\Requests\Payments\PaymentUpdateBalanceRequest;
use App\Http\Resources\Payments\PaymentResource;
use App\Services\Payments\PaymentBalanceService;
use App\Services\Payments\PaymentLookupService;
use App\Services\Payments\PaymentPlanService;
use Illuminate\Http\JsonResponse;
class PaymentController extends BaseApiController
{
public function __construct(
private PaymentPlanService $planService,
private PaymentLookupService $lookupService,
private PaymentBalanceService $balanceService
) {
parent::__construct();
}
public function store(PaymentCreateRequest $request): JsonResponse
{
$payload = $request->validated();
$payment = $this->planService->createPlan($payload);
return response()->json([
'ok' => true,
'payment' => new PaymentResource($payment),
], 201);
}
public function byParent(PaymentByParentRequest $request, int $parentId): JsonResponse
{
$payload = $request->validated();
$payments = $this->lookupService->getByParent($parentId, $payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'payments' => PaymentResource::collection($payments),
]);
}
public function updateBalance(PaymentUpdateBalanceRequest $request, int $paymentId): JsonResponse
{
$payload = $request->validated();
$updated = $this->balanceService->updateBalance($paymentId, (float) $payload['paid_amount']);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Payment not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentEventChargesListRequest;
use App\Http\Requests\Payments\PaymentEventChargesStoreRequest;
use App\Services\Payments\PaymentEventChargesService;
use Illuminate\Http\JsonResponse;
class PaymentEventChargesController extends BaseApiController
{
public function __construct(private PaymentEventChargesService $service)
{
parent::__construct();
}
public function index(PaymentEventChargesListRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->listCharges($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json(['ok' => true] + $data);
}
public function store(PaymentEventChargesStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$count = $this->service->addCharges($payload, $userId);
if ($count === 0) {
return response()->json(['ok' => false, 'message' => 'No student selected.'], 422);
}
return response()->json(['ok' => true, 'inserted' => $count]);
}
public function enrolledStudents(PaymentEventChargesListRequest $request, int $parentId): JsonResponse
{
$payload = $request->validated();
$students = $this->service->getEnrolledStudents($parentId, $payload['school_year'] ?? null);
return response()->json(['ok' => true, 'students' => $students]);
}
}
@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentManualEditRequest;
use App\Http\Requests\Payments\PaymentManualSearchRequest;
use App\Http\Requests\Payments\PaymentManualSuggestRequest;
use App\Http\Requests\Payments\PaymentManualUpdateRequest;
use App\Services\Payments\PaymentManualService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PaymentManualController extends BaseApiController
{
public function __construct(private PaymentManualService $service)
{
parent::__construct();
}
public function search(PaymentManualSearchRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->search(
(string) ($payload['search_term'] ?? ''),
$payload['school_year'] ?? null
);
return response()->json(['ok' => true] + $data);
}
public function suggest(PaymentManualSuggestRequest $request): JsonResponse
{
$payload = $request->validated();
$items = $this->service->suggest((string) ($payload['q'] ?? ''));
return response()->json(['ok' => true, 'items' => $items]);
}
public function record(PaymentManualUpdateRequest $request, int $invoiceId): JsonResponse
{
$payload = $request->validated();
$result = $this->service->recordPayment(
$invoiceId,
(float) $payload['amount'],
(string) $payload['payment_method'],
$payload['payment_date'] ?? null,
$payload['check_number'] ?? null,
$payload['payment_type'] ?? null,
$request->file('payment_file'),
$payload['school_year'] ?? null,
$payload['semester'] ?? null,
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true] + $result);
}
public function edit(PaymentManualEditRequest $request, int $paymentId): JsonResponse
{
$payload = $request->validated();
$this->service->editPayment(
$paymentId,
(float) $payload['paid_amount'],
(string) $payload['payment_method'],
$payload['check_number'] ?? null,
$request->file('payment_file'),
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function file(Request $request, string $filename)
{
$mode = (string) ($request->query('mode') ?? 'download');
return $this->service->serveCheckFile($filename, $mode);
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentNotificationListRequest;
use App\Http\Requests\Payments\PaymentNotificationSendRequest;
use App\Http\Resources\Payments\PaymentNotificationLogResource;
use App\Services\Payments\PaymentNotificationService;
use Illuminate\Http\JsonResponse;
class PaymentNotificationController extends BaseApiController
{
public function __construct(private PaymentNotificationService $service)
{
parent::__construct();
}
public function index(PaymentNotificationListRequest $request): JsonResponse
{
$payload = $request->validated();
$logs = $this->service->listLogs(
$payload['from'] ?? null,
$payload['to'] ?? null,
$payload['type'] ?? null
);
return response()->json([
'ok' => true,
'logs' => PaymentNotificationLogResource::collection($logs),
]);
}
public function send(PaymentNotificationSendRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->send($payload);
return response()->json(['ok' => true] + $result);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentTransactionCreateRequest;
use App\Http\Requests\Payments\PaymentTransactionStatusRequest;
use App\Http\Resources\Payments\PaymentTransactionResource;
use App\Services\Payments\PaymentTransactionService;
use Illuminate\Http\JsonResponse;
class PaymentTransactionController extends BaseApiController
{
public function __construct(private PaymentTransactionService $service)
{
parent::__construct();
}
public function store(PaymentTransactionCreateRequest $request): JsonResponse
{
$payload = $request->validated();
$transaction = $this->service->create($payload);
return response()->json([
'ok' => true,
'transaction' => new PaymentTransactionResource($transaction->toArray()),
], 201);
}
public function byPayment(int $paymentId): JsonResponse
{
$transactions = $this->service->getByPayment($paymentId);
return response()->json([
'ok' => true,
'transactions' => PaymentTransactionResource::collection($transactions),
]);
}
public function updateStatus(PaymentTransactionStatusRequest $request, string $transactionId): JsonResponse
{
$payload = $request->validated();
$updated = $this->service->updateStatus($transactionId, $payload['status']);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Transaction not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaypalExecuteRequest;
use App\Services\Payments\PaypalPaymentService;
use Illuminate\Http\JsonResponse;
class PaypalPaymentController extends BaseApiController
{
public function __construct(private PaypalPaymentService $service)
{
parent::__construct();
}
public function redirect(): JsonResponse
{
return response()->json([
'ok' => true,
'redirect_url' => $this->service->getRedirectUrl(),
]);
}
public function create(int $paymentId): JsonResponse
{
$result = $this->service->createPayment($paymentId);
return response()->json(['ok' => true] + $result);
}
public function execute(PaypalExecuteRequest $request): JsonResponse
{
$payload = $request->validated();
$this->service->executePayment(
(string) $payload['payer_id'],
$payload['paypal_payment_id'] ?? null
);
return response()->json(['ok' => true]);
}
public function cancel(): JsonResponse
{
$this->service->cancelPayment();
return response()->json([
'ok' => true,
'message' => 'Payment was canceled.',
]);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaypalTransactionsListRequest;
use App\Http\Resources\Payments\PaypalTransactionResource;
use App\Services\Payments\PaypalTransactionsService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PaypalTransactionsController extends BaseApiController
{
public function __construct(private PaypalTransactionsService $service)
{
parent::__construct();
}
public function index(PaypalTransactionsListRequest $request): JsonResponse
{
$payload = $request->validated();
$perPage = (int) ($payload['per_page'] ?? 10);
$rows = $this->service->list($payload['q'] ?? null, $perPage);
return response()->json([
'ok' => true,
'transactions' => PaypalTransactionResource::collection($rows->items()),
'pagination' => [
'current_page' => $rows->currentPage(),
'per_page' => $rows->perPage(),
'total' => $rows->total(),
'last_page' => $rows->lastPage(),
],
]);
}
public function downloadCsv(PaypalTransactionsListRequest $request): StreamedResponse
{
$payload = $request->validated();
$rows = $this->service->listAll($payload['q'] ?? null);
$filename = 'paypal_transactions_' . date('Ymd_His') . '.csv';
return response()->streamDownload(function () use ($rows) {
$out = fopen('php://output', 'w');
fputcsv($out, [
'ID', 'Transaction ID', 'Order ID', 'Parent School ID',
'Email', 'Amount', 'Net Amount', 'Currency',
'Status', 'Event Type', 'Created At',
]);
foreach ($rows as $row) {
fputcsv($out, [
$row['id'] ?? null,
$row['transaction_id'] ?? null,
$row['order_id'] ?? null,
$row['parent_school_id'] ?? null,
$row['payer_email'] ?? null,
$row['amount'] ?? null,
$row['net_amount'] ?? null,
$row['currency'] ?? null,
$row['status'] ?? null,
$row['event_type'] ?? null,
$row['created_at'] ?? null,
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv']);
}
}
@@ -0,0 +1,391 @@
<?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),
]);
}
}