fix financial and certificates
This commit is contained in:
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user