fix financial and certificates

This commit is contained in:
root
2026-06-05 01:51:08 -04:00
parent d28d11e2e5
commit ad968eaff7
94 changed files with 9654 additions and 214 deletions
@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Controller;
use App\Http\Requests\Finance\CarryforwardAdjustmentRequest;
use App\Http\Requests\Finance\CarryforwardFinalizeRequest;
use App\Http\Requests\Finance\CarryforwardPreviewRequest;
use App\Services\Finance\PriorYearBalanceCarryforwardService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class BalanceCarryforwardController extends Controller
{
public function __construct(private PriorYearBalanceCarryforwardService $service) {}
public function preview(CarryforwardPreviewRequest $request): JsonResponse
{
return response()->json(['ok' => true, 'preview' => $this->service->preview($request->validated())]);
}
public function storeDraft(CarryforwardFinalizeRequest $request): JsonResponse
{
return response()->json(['ok' => true, 'result' => $this->service->storeDrafts($request->validated(), optional($request->user())->id)]);
}
public function approve(Request $request, int $carryforward): JsonResponse
{
return response()->json(['ok' => true, 'carryforward' => $this->service->approve($carryforward, optional($request->user())->id)]);
}
public function postToNewYear(Request $request, int $carryforward): JsonResponse
{
return response()->json(['ok' => true, 'carryforward' => $this->service->postToNewYear($carryforward, optional($request->user())->id)]);
}
public function waive(CarryforwardAdjustmentRequest $request, int $carryforward): JsonResponse
{
$data = $request->validated();
return response()->json(['ok' => true, 'carryforward' => $this->service->waive($carryforward, (float) $data['amount'], $data['reason'] ?? null, optional($request->user())->id)]);
}
public function adjust(CarryforwardAdjustmentRequest $request, int $carryforward): JsonResponse
{
$data = $request->validated();
return response()->json(['ok' => true, 'carryforward' => $this->service->adjust($carryforward, (float) $data['amount'], $data['reason'] ?? null)]);
}
public function report(Request $request): JsonResponse
{
return response()->json(['ok' => true, 'report' => $this->service->report($request->query())]);
}
public function reportCsv(Request $request): StreamedResponse
{
$rows = $this->service->csvRows($this->service->report($request->query()));
return response()->streamDownload(function () use ($rows) {
$out = fopen('php://output', 'w');
foreach ($rows as $row) fputcsv($out, $row);
fclose($out);
}, 'carryforward_report_' . date('Ymd_His') . '.csv', ['Content-Type' => 'text/csv']);
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Controller;
use App\Http\Requests\Finance\EventChargeLifecycleRequest;
use App\Models\EventCharge;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class EventChargeController extends Controller
{
public function index(Request $request): JsonResponse
{
$q = EventCharge::query();
foreach (['parent_id','student_id','event_id','school_year','semester'] as $field) {
if ($request->filled($field)) $q->where($field, $request->query($field));
}
if ($request->filled('status')) {
$status = $request->query('status');
if (Schema::hasColumn('event_charges', 'status')) $q->where('status', $status);
elseif ($status === 'paid') $q->where('event_paid', 1);
elseif ($status === 'pending') $q->where(function ($qq) { $qq->whereNull('event_paid')->orWhere('event_paid', 0); });
}
return response()->json(['ok' => true, 'event_charges' => $q->orderByDesc('id')->paginate((int) $request->query('per_page', 25))]);
}
public function store(EventChargeLifecycleRequest $request): JsonResponse
{
$data = $this->normalizePayload($request->validated());
$data['created_by'] = optional($request->user())->id;
if (Schema::hasColumn('event_charges', 'status')) $data['status'] = $data['status'] ?? 'draft';
$charge = EventCharge::query()->create($data);
return response()->json(['ok' => true, 'event_charge' => $charge], 201);
}
public function show(int $eventCharge): JsonResponse
{
return response()->json(['ok' => true, 'event_charge' => EventCharge::query()->findOrFail($eventCharge)]);
}
public function update(EventChargeLifecycleRequest $request, int $eventCharge): JsonResponse
{
$charge = EventCharge::query()->findOrFail($eventCharge);
$charge->fill($this->normalizePayload($request->validated()))->save();
return response()->json(['ok' => true, 'event_charge' => $charge->fresh()]);
}
public function destroy(int $eventCharge): JsonResponse
{
return $this->void($eventCharge);
}
public function approve(int $eventCharge): JsonResponse
{
$charge = EventCharge::query()->findOrFail($eventCharge);
if (Schema::hasColumn('event_charges', 'status')) $charge->status = 'approved';
if (Schema::hasColumn('event_charges', 'charged')) $charge->charged = 1;
$charge->save();
return response()->json(['ok' => true, 'event_charge' => $charge->fresh()]);
}
public function void(int $eventCharge): JsonResponse
{
$charge = EventCharge::query()->findOrFail($eventCharge);
if (Schema::hasColumn('event_charges', 'status')) $charge->status = 'voided';
if (Schema::hasColumn('event_charges', 'charged')) $charge->charged = 0;
$charge->save();
return response()->json(['ok' => true, 'event_charge' => $charge->fresh()]);
}
public function attachToInvoice(Request $request, int $eventCharge): JsonResponse
{
$request->validate(['invoice_id' => ['required','integer']]);
$charge = EventCharge::query()->findOrFail($eventCharge);
$invoiceId = (int) $request->input('invoice_id');
abort_if(!DB::table('invoices')->where('id', $invoiceId)->exists(), 404, 'Invoice not found.');
if (Schema::hasColumn('event_charges', 'invoice_id')) $charge->invoice_id = $invoiceId;
if (Schema::hasColumn('event_charges', 'status')) $charge->status = 'invoiced';
if (Schema::hasColumn('event_charges', 'charged')) $charge->charged = 1;
$charge->save();
return response()->json(['ok' => true, 'event_charge' => $charge->fresh(), 'warning' => Schema::hasColumn('event_charges','invoice_id') ? null : 'invoice_id column does not exist; charge marked as charged only.']);
}
private function normalizePayload(array $data): array
{
if (isset($data['title']) && !isset($data['event_name'])) $data['event_name'] = $data['title'];
unset($data['title'], $data['invoice_id']);
return array_filter($data, fn($v) => $v !== null);
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Controller;
use App\Http\Requests\Finance\FinanceNotificationLogRequest;
use App\Services\Finance\FinanceNotificationLogService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class FinanceNotificationController extends Controller
{
public function __construct(private FinanceNotificationLogService $service) {}
public function sendPaymentReceipt(Request $request, int $payment): JsonResponse
{
$p = DB::table('payments')->where('id', $payment)->first();
abort_if(!$p, 404, 'Payment not found.');
$receipt = $this->service->nextReceiptNumber($p->school_year ?? date('Y'));
$log = $this->service->log([
'parent_id' => $p->parent_id ?? null,
'invoice_id' => $p->invoice_id ?? null,
'payment_id' => $payment,
'notification_type' => 'payment_receipt',
'channel' => 'database',
'recipient' => (string) ($p->parent_id ?? ''),
'subject' => 'Payment receipt ' . $receipt,
], optional($request->user())->id);
return response()->json(['ok' => true, 'receipt_number' => $receipt, 'notification_log' => $log]);
}
public function sendRefundReceipt(Request $request, int $refund): JsonResponse
{
$log = $this->service->log(['refund_id' => $refund, 'notification_type' => 'refund_receipt', 'channel' => 'database', 'subject' => 'Refund receipt'], optional($request->user())->id);
return response()->json(['ok' => true, 'notification_log' => $log]);
}
public function sendInvoiceStatement(Request $request, int $invoice): JsonResponse
{
$inv = DB::table('invoices')->where('id', $invoice)->first();
abort_if(!$inv, 404, 'Invoice not found.');
$log = $this->service->log(['parent_id' => $inv->parent_id ?? null, 'invoice_id' => $invoice, 'notification_type' => 'statement_available', 'channel' => 'database', 'subject' => 'Statement available'], optional($request->user())->id);
return response()->json(['ok' => true, 'notification_log' => $log]);
}
public function sendOverdueReminder(Request $request, int $parent): JsonResponse
{
$log = $this->service->log(['parent_id' => $parent, 'notification_type' => 'overdue_balance_reminder', 'channel' => 'database', 'subject' => 'Overdue balance reminder'], optional($request->user())->id);
return response()->json(['ok' => true, 'notification_log' => $log]);
}
public function sendInstallmentReminder(Request $request, int $installment): JsonResponse
{
$log = $this->service->log(['installment_id' => $installment, 'notification_type' => 'installment_reminder', 'channel' => 'database', 'subject' => 'Installment reminder'], optional($request->user())->id);
return response()->json(['ok' => true, 'notification_log' => $log]);
}
public function logs(FinanceNotificationLogRequest $request): JsonResponse
{
return response()->json(['ok' => true, 'logs' => $this->service->list($request->validated())]);
}
}
@@ -5,7 +5,10 @@ namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\Core\BaseApiController;
use App\Http\Requests\Finance\FinancialReportRequest;
use App\Http\Requests\Finance\FinancialSummaryRequest;
use App\Http\Requests\Finance\StakeholderFinancialAnalysisRequest;
use App\Http\Requests\Finance\FinancialUnpaidParentsRequest;
use App\Http\Requests\Finance\ParentPaymentFollowUpRequest;
use App\Http\Requests\Finance\FollowUpNoteRequest;
use App\Http\Resources\Finance\FinancialReportResource;
use App\Http\Resources\Finance\FinancialSummaryResource;
use App\Http\Resources\Finance\FinancialUnpaidParentResource;
@@ -13,7 +16,9 @@ use App\Services\Finance\FinancialPdfReportService;
use App\Services\Finance\FinancialReportService;
use App\Services\Finance\FinancialSchoolYearService;
use App\Services\Finance\FinancialSummaryService;
use App\Services\Finance\StakeholderFinancialAnalysisService;
use App\Services\Finance\FinancialUnpaidParentsService;
use App\Services\Finance\ParentPaymentFollowUpService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -24,7 +29,9 @@ class FinancialController extends BaseApiController
private FinancialSummaryService $summaryService,
private FinancialUnpaidParentsService $unpaidParentsService,
private FinancialSchoolYearService $schoolYears,
private FinancialPdfReportService $pdfService
private FinancialPdfReportService $pdfService,
private StakeholderFinancialAnalysisService $stakeholderAnalysisService,
private ParentPaymentFollowUpService $parentPaymentFollowUpService
) {
parent::__construct();
}
@@ -75,6 +82,95 @@ class FinancialController extends BaseApiController
]);
}
public function parentPaymentFollowups(ParentPaymentFollowUpRequest $request): JsonResponse
{
return response()->json([
'ok' => true,
'followups' => $this->parentPaymentFollowUpService->report($request->validated()),
]);
}
public function parentPaymentFollowupsCsv(ParentPaymentFollowUpRequest $request): StreamedResponse
{
$report = $this->parentPaymentFollowUpService->report($request->validated());
$rows = $this->parentPaymentFollowUpService->csvRows($report);
return response()->streamDownload(function () use ($rows) {
$out = fopen('php://output', 'w');
foreach ($rows as $row) {
fputcsv($out, $row);
}
fclose($out);
}, 'parent_payment_followups_' . ($report['schoolYear'] ?? 'report') . '_' . date('Ymd_His') . '.csv', ['Content-Type' => 'text/csv']);
}
public function storeParentFollowUpNote(FollowUpNoteRequest $request, int $parent): JsonResponse
{
return response()->json([
'ok' => true,
'note' => $this->parentPaymentFollowUpService->storeNote($parent, $request->validated(), optional($request->user())->id),
], 201);
}
public function markParentContacted(FollowUpNoteRequest $request, int $parent): JsonResponse
{
$payload = array_merge($request->validated(), ['status' => $request->input('status', 'contacted')]);
return response()->json(['ok' => true, 'note' => $this->parentPaymentFollowUpService->storeNote($parent, $payload, optional($request->user())->id)], 201);
}
public function storePromiseToPay(FollowUpNoteRequest $request, int $parent): JsonResponse
{
$payload = array_merge($request->validated(), ['status' => 'promise_to_pay']);
return response()->json(['ok' => true, 'note' => $this->parentPaymentFollowUpService->storeNote($parent, $payload, optional($request->user())->id)], 201);
}
public function resolveParentFollowUp(FollowUpNoteRequest $request, int $parent): JsonResponse
{
$payload = array_merge($request->validated(), ['status' => 'resolved']);
return response()->json(['ok' => true, 'note' => $this->parentPaymentFollowUpService->storeNote($parent, $payload, optional($request->user())->id)], 201);
}
public function stakeholderAnalysis(StakeholderFinancialAnalysisRequest $request): JsonResponse
{
$payload = $request->validated();
$analysis = $this->stakeholderAnalysisService->analyze(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null,
$payload['compare_school_year'] ?? null
);
return response()->json([
'ok' => true,
'analysis' => $analysis,
]);
}
public function stakeholderAnalysisCsv(StakeholderFinancialAnalysisRequest $request): StreamedResponse
{
$payload = $request->validated();
$analysis = $this->stakeholderAnalysisService->analyze(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null,
$payload['compare_school_year'] ?? null
);
$rows = $this->stakeholderAnalysisService->csvRows($analysis);
$schoolYear = $analysis['schoolYear'] ?? 'report';
return response()->streamDownload(function () use ($rows) {
$out = fopen('php://output', 'w');
foreach ($rows as $row) {
fputcsv($out, $row);
}
fclose($out);
}, 'stakeholder_financial_analysis_' . $schoolYear . '_' . date('Ymd_His') . '.csv', ['Content-Type' => 'text/csv']);
}
public function downloadCsv(FinancialReportRequest $request): StreamedResponse
{
$payload = $request->validated();
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Controller;
use App\Http\Requests\Finance\InstallmentPlanRequest;
use App\Http\Requests\Finance\PaymentAllocationRequest;
use App\Services\Finance\InstallmentPlanService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class InstallmentPlanController extends Controller
{
public function __construct(private InstallmentPlanService $service) {}
public function index(Request $request): JsonResponse { return response()->json(['ok' => true, 'plans' => $this->service->list($request->query())]); }
public function store(InstallmentPlanRequest $request, int $invoice): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->createForInvoice($invoice, $request->validated(), optional($request->user())->id)], 201); }
public function show(int $plan): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->show($plan)]); }
public function activate(Request $request, int $plan): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->activate($plan, optional($request->user())->id)]); }
public function cancel(int $plan): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->cancel($plan)]); }
public function restructure(InstallmentPlanRequest $request, int $plan): JsonResponse
{
$old = $this->service->cancel($plan);
return response()->json(['ok' => true, 'old_plan' => $old, 'message' => 'Old plan cancelled. Create a replacement plan against the invoice to preserve audit history.']);
}
public function allocatePayment(PaymentAllocationRequest $request, int $payment): JsonResponse { return response()->json(['ok' => true, 'result' => $this->service->allocatePayment($payment, $request->validated())]); }
public function due(): JsonResponse { return response()->json(['ok' => true, 'installments' => $this->service->due(false)]); }
public function overdue(): JsonResponse { $this->service->markOverdue(); return response()->json(['ok' => true, 'installments' => $this->service->due(true)]); }
}
@@ -128,6 +128,23 @@ class InvoiceController extends BaseApiController
]);
}
public function preview(int $invoiceId): JsonResponse
{
$data = $this->pdfService->previewData($invoiceId);
if (isset($data['error'])) {
return response()->json([
'ok' => false,
'message' => $data['error'],
], 404);
}
return response()->json([
'ok' => true,
...$data,
]);
}
public function pdf(int $invoiceId): StreamedResponse
{
$pdfBytes = $this->pdfService->buildPdf($invoiceId);
@@ -35,6 +35,7 @@ use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Validator;
use RuntimeException;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ReimbursementController extends BaseApiController
{
@@ -287,7 +288,7 @@ class ReimbursementController extends BaseApiController
return response()->json(['ok' => true, 'message' => 'Email sent successfully.']);
}
public function export(ReimbursementExportRequest $request): Response
public function export(ReimbursementExportRequest $request): StreamedResponse
{
$payload = $request->validated();
$type = $payload['type'] ?? 'processed';
@@ -310,7 +311,7 @@ class ReimbursementController extends BaseApiController
]);
}
public function exportBatch(ReimbursementExportBatchRequest $request): Response
public function exportBatch(ReimbursementExportBatchRequest $request): StreamedResponse
{
$payload = $request->validated();
$csv = $this->exportService->buildBatchCsv((int) $payload['batch_id']);