fix financial and certificates
This commit is contained in:
@@ -24,6 +24,9 @@ use App\Services\Promotions\PromotionEnrollmentService;
|
||||
use App\Services\Promotions\PromotionQueryService;
|
||||
use App\Services\Promotions\PromotionReminderService;
|
||||
use App\Services\Promotions\PromotionStatusService;
|
||||
use App\Services\Promotions\Placement\SectionPlacementPreviewService;
|
||||
use App\Models\SectionPlacementBatch;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -42,7 +45,8 @@ class AdministratorPromotionController extends BaseApiController
|
||||
private PromotionEnrollmentService $enrollmentService,
|
||||
private PromotionReminderService $reminders,
|
||||
private PromotionAuditService $audit,
|
||||
private LevelProgressionService $progression
|
||||
private LevelProgressionService $progression,
|
||||
private SectionPlacementPreviewService $sectionPlacement
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -133,6 +137,69 @@ class AdministratorPromotionController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function createPlacementPreview(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validate([
|
||||
'from_school_year' => ['required', 'string', 'max:25'],
|
||||
'to_school_year' => ['required', 'string', 'max:25'],
|
||||
'from_grade_level_id' => ['nullable', 'integer', 'min:1'],
|
||||
'to_grade_level_id' => ['required', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$batch = $this->sectionPlacement->createDraft(
|
||||
$payload['from_school_year'],
|
||||
$payload['to_school_year'],
|
||||
$payload['from_grade_level_id'] ?? null,
|
||||
$payload['to_grade_level_id'] ?? null,
|
||||
$this->getCurrentUserId()
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Section placement preview generation failed', ['exception' => $e]);
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->sectionPlacement->previewPayload($batch),
|
||||
]);
|
||||
}
|
||||
|
||||
public function showPlacementBatch(int $batchId): JsonResponse
|
||||
{
|
||||
$batch = SectionPlacementBatch::query()->find($batchId);
|
||||
if (!$batch) {
|
||||
return response()->json(['ok' => false, 'message' => 'Placement batch not found.'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->sectionPlacement->previewPayload($batch),
|
||||
]);
|
||||
}
|
||||
|
||||
public function finalizePlacementBatch(int $batchId): JsonResponse
|
||||
{
|
||||
try {
|
||||
$batch = $this->sectionPlacement->finalize($batchId, $this->getCurrentUserId());
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Section placement finalization failed', ['batch_id' => $batchId, 'exception' => $e]);
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->sectionPlacement->previewPayload($batch),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateStatus(UpdatePromotionStatusRequest $request, int $promotionId): JsonResponse
|
||||
{
|
||||
$record = $this->findOrFail($promotionId);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Administrator;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\Administrator\TrophyReportRequest;
|
||||
use App\Services\Administrator\Trophy\TrophyReportService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class TrophyController extends BaseApiController
|
||||
{
|
||||
public function __construct(private TrophyReportService $service)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(TrophyReportRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->projection(
|
||||
$payload['school_year'] ?? null,
|
||||
isset($payload['percentile']) ? (float) $payload['percentile'] : null
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function winners(TrophyReportRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->winners(
|
||||
$payload['school_year'] ?? null,
|
||||
isset($payload['percentile']) ? (float) $payload['percentile'] : null
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function final(TrophyReportRequest $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->final(
|
||||
$payload['school_year'] ?? null,
|
||||
isset($payload['percentile']) ? (float) $payload['percentile'] : null
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -5,149 +5,141 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Api\Certificates;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Models\Configuration;
|
||||
use App\Services\Certificates\CertificateAdminService;
|
||||
use App\Services\Certificates\CertificatePdfService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CertificateController extends BaseApiController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
public function __construct(
|
||||
private CertificateAdminService $service,
|
||||
private CertificatePdfService $pdfService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/administrator/certificates/form-options
|
||||
*/
|
||||
public function formOptions(Request $request): JsonResponse
|
||||
public function dashboard(Request $request): JsonResponse
|
||||
{
|
||||
$schoolYearParam = $request->query('school_year');
|
||||
if ($schoolYearParam !== null) {
|
||||
$schoolYear = trim((string) $schoolYearParam);
|
||||
} else {
|
||||
try {
|
||||
$schoolYear = trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
} catch (\Throwable) {
|
||||
$schoolYear = '';
|
||||
}
|
||||
}
|
||||
$classSectionId = $request->query('class_section_id');
|
||||
|
||||
try {
|
||||
$classSections = DB::table('classSection')
|
||||
->select('class_section_id', 'class_section_name')
|
||||
->orderBy('class_section_name', 'ASC')
|
||||
->get()
|
||||
->map(fn ($r) => (array) $r)
|
||||
->all();
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->dashboard($request->query('school_year')),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate formOptions class query failed: ' . $e->getMessage());
|
||||
return $this->error('Failed to load classes: ' . $e->getMessage(), 500);
|
||||
Log::error('Certificate dashboard failed', ['exception' => $e]);
|
||||
|
||||
return $this->error('Failed to load certificate dashboard.', 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function formOptions(Request $request): JsonResponse
|
||||
{
|
||||
return $this->dashboard($request);
|
||||
}
|
||||
|
||||
public function auditLog(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->auditLog($request->query('school_year')),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate audit log failed', ['exception' => $e]);
|
||||
|
||||
return $this->error('Failed to load certificate audit log.', 500);
|
||||
}
|
||||
|
||||
$students = [];
|
||||
if ($classSectionId) {
|
||||
try {
|
||||
$studentQuery = DB::table('student_class as sc')
|
||||
->select('s.id', 's.firstname', 's.lastname', 'cs.class_section_name as grade')
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('sc.class_section_id', (int) $classSectionId)
|
||||
->where('s.is_active', 1)
|
||||
->orderBy('s.lastname')
|
||||
->orderBy('s.firstname');
|
||||
|
||||
if ($schoolYear !== '') {
|
||||
$studentQuery->where('sc.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
$students = $studentQuery->get()->map(fn ($r) => (array) $r)->all();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate formOptions student query failed: ' . $e->getMessage());
|
||||
return $this->error('Failed to load students: ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'class_sections' => $classSections,
|
||||
'students' => $students,
|
||||
'school_year' => $schoolYear,
|
||||
'selected_class_id' => $classSectionId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/administrator/certificates/generate
|
||||
*/
|
||||
public function generate(Request $request): Response|JsonResponse
|
||||
{
|
||||
$studentIds = $request->input('student_ids', []);
|
||||
$certDate = trim((string) ($request->input('cert_date') ?? date('m/d/Y')));
|
||||
$studentIds = $request->input('student_ids', []);
|
||||
$certDate = trim((string) ($request->input('cert_date') ?? date('m/d/Y')));
|
||||
$classSectionId = $request->input('class_section_id');
|
||||
|
||||
if (empty($studentIds) || !is_array($studentIds)) {
|
||||
return $this->error('Please select at least one student.', 422);
|
||||
}
|
||||
|
||||
$studentIds = array_values(array_filter(array_map('intval', $studentIds)));
|
||||
$certDate = preg_replace('/[^0-9\/\-]/', '', $certDate) ?? date('m/d/Y');
|
||||
|
||||
if (empty($studentIds)) {
|
||||
return $this->error('Invalid student selection.', 422);
|
||||
}
|
||||
|
||||
$students = [];
|
||||
foreach ($studentIds as $id) {
|
||||
$row = null;
|
||||
|
||||
if ($classSectionId) {
|
||||
$row = DB::table('student_class as sc')
|
||||
->select('s.id', 's.firstname', 's.lastname', 'cs.class_section_name as grade')
|
||||
->join('students as s', 's.id', '=', 'sc.student_id')
|
||||
->join('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
|
||||
->where('sc.class_section_id', (int) $classSectionId)
|
||||
->where('s.id', $id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
$row = DB::table('students')
|
||||
->select('id', 'firstname', 'lastname', 'registration_grade as grade')
|
||||
->where('id', $id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($row) {
|
||||
$students[] = (array) $row;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($students)) {
|
||||
return $this->error('No valid students found.', 422);
|
||||
}
|
||||
$schoolYear = $request->input('school_year');
|
||||
|
||||
try {
|
||||
$pdfService = app(CertificatePdfService::class);
|
||||
$pdfContent = $pdfService->generate($students, $certDate);
|
||||
$payload = $this->service->issueCertificates(
|
||||
is_array($studentIds) ? $studentIds : [],
|
||||
$certDate,
|
||||
$classSectionId !== null && $classSectionId !== '' ? (int) $classSectionId : null,
|
||||
is_string($schoolYear) ? $schoolYear : null,
|
||||
$this->getCurrentUserId()
|
||||
);
|
||||
$pdfContent = $this->pdfService->generate(
|
||||
$payload['students'],
|
||||
$payload['cert_date_display']
|
||||
);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage(), 422);
|
||||
} catch (\RuntimeException $e) {
|
||||
return $this->error($e->getMessage(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate PDF generation failed: ' . $e->getMessage(), ['exception' => $e]);
|
||||
Log::error('Certificate generation failed', ['exception' => $e]);
|
||||
|
||||
return $this->error('Failed to generate certificates.', 500);
|
||||
}
|
||||
|
||||
$filename = 'Certificates_' . date('Ymd_His') . '.pdf';
|
||||
$filename = 'Certificates_'.date('Ymd_His').'.pdf';
|
||||
|
||||
return response($pdfContent, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="' . $filename . '"',
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
||||
]);
|
||||
}
|
||||
|
||||
private function currentSchoolYear(): string
|
||||
public function reprint(string $certificateNumber): Response|JsonResponse
|
||||
{
|
||||
return trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
try {
|
||||
$payload = $this->service->reprintPayload($certificateNumber);
|
||||
if ($payload === null) {
|
||||
return $this->error('Certificate not found: '.$certificateNumber, 404);
|
||||
}
|
||||
|
||||
$pdfContent = $this->pdfService->generate(
|
||||
[$payload['student']],
|
||||
$payload['cert_date_display']
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate reprint failed', [
|
||||
'certificate_number' => $certificateNumber,
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return $this->error('Failed to reprint certificate.', 500);
|
||||
}
|
||||
|
||||
$filename = 'Certificate_'.strtoupper($certificateNumber).'.pdf';
|
||||
|
||||
return response($pdfContent, 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
||||
]);
|
||||
}
|
||||
|
||||
public function verify(string $token): JsonResponse
|
||||
{
|
||||
try {
|
||||
$payload = $this->service->verifyPayload($token);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Certificate verify failed', ['token' => $token, 'exception' => $e]);
|
||||
|
||||
return $this->error('Failed to verify certificate.', 500);
|
||||
}
|
||||
|
||||
if ($payload === null) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'Certificate not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $payload,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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