Spaces:
Running
Running
| /** | |
| * Payment API Endpoint | |
| * | |
| * POST /api/payments/process | |
| * | |
| * Accepts JSON payload to process student fee payments via external systems. | |
| * This endpoint uses the same internal payment logic as the web UI. | |
| * | |
| * Request Format: | |
| * { | |
| * "student_id": "string|integer", | |
| * "teller_no": "string", | |
| * "amount": number | |
| * } | |
| * | |
| * Headers Required: | |
| * - Content-Type: application/json | |
| * - Authorization: Bearer <API_KEY> (if API_AUTH_ENABLED is true) | |
| */ | |
| // Set error reporting for production | |
| error_reporting(E_ALL); | |
| ini_set('display_errors', 0); | |
| // Include required files | |
| require_once __DIR__ . '/../../db_config.php'; | |
| require_once __DIR__ . '/../../config/api_config.php'; | |
| require_once __DIR__ . '/../../includes/ApiValidator.php'; | |
| require_once __DIR__ . '/../../includes/PaymentProcessor.php'; | |
| require_once __DIR__ . '/../../includes/ReceiptGenerator.php'; | |
| // Set response headers | |
| header('Content-Type: application/json'); | |
| // CORS headers (if enabled) | |
| if (defined('API_CORS_ENABLED') && API_CORS_ENABLED) { | |
| header('Access-Control-Allow-Origin: ' . API_CORS_ORIGIN); | |
| header('Access-Control-Allow-Methods: POST, OPTIONS'); | |
| header('Access-Control-Allow-Headers: Content-Type, Authorization'); | |
| // Handle preflight requests | |
| if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { | |
| http_response_code(200); | |
| exit; | |
| } | |
| } | |
| /** | |
| * Send JSON response | |
| */ | |
| function sendResponse($data, $httpCode = 200) | |
| { | |
| http_response_code($httpCode); | |
| echo json_encode($data, JSON_PRETTY_PRINT); | |
| exit; | |
| } | |
| /** | |
| * Log API request | |
| */ | |
| function logApiRequest($data, $response, $httpCode) | |
| { | |
| if (!defined('API_LOG_ENABLED') || !API_LOG_ENABLED) { | |
| return; | |
| } | |
| $logDir = dirname(API_LOG_FILE); | |
| if (!is_dir($logDir)) { | |
| mkdir($logDir, 0755, true); | |
| } | |
| $logEntry = [ | |
| 'timestamp' => date('Y-m-d H:i:s'), | |
| 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', | |
| 'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown', | |
| 'request' => $data, | |
| 'response' => $response, | |
| 'http_code' => $httpCode | |
| ]; | |
| file_put_contents( | |
| API_LOG_FILE, | |
| json_encode($logEntry) . PHP_EOL, | |
| FILE_APPEND | |
| ); | |
| } | |
| // Main execution | |
| try { | |
| // Initialize validator | |
| $apiKeys = defined('API_AUTH_ENABLED') && API_AUTH_ENABLED ? array_keys(API_KEYS) : []; | |
| $validator = new ApiValidator($pdo, $apiKeys); | |
| // Step 1: Validate request (method, headers, auth) | |
| $requestValidation = $validator->validateRequest(); | |
| if (!$requestValidation['valid']) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => $requestValidation['error'] | |
| ]; | |
| logApiRequest([], $response, $requestValidation['http_code']); | |
| sendResponse($response, $requestValidation['http_code']); | |
| } | |
| // Step 2: Parse JSON payload | |
| $payloadValidation = $validator->parseJsonPayload(); | |
| if (!$payloadValidation['valid']) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => $payloadValidation['error'] | |
| ]; | |
| logApiRequest([], $response, $payloadValidation['http_code']); | |
| sendResponse($response, $payloadValidation['http_code']); | |
| } | |
| $requestData = $payloadValidation['data']; | |
| // Step 3: Validate required fields | |
| $fieldValidation = $validator->validatePaymentRequest($requestData); | |
| if (!$fieldValidation['valid']) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => 'Validation failed', | |
| 'errors' => $fieldValidation['errors'] | |
| ]; | |
| logApiRequest($requestData, $response, $fieldValidation['http_code']); | |
| sendResponse($response, $fieldValidation['http_code']); | |
| } | |
| // Sanitize input | |
| $sanitizedData = $validator->sanitizeInput($requestData); | |
| // Step 4: Validate student exists | |
| $studentValidation = $validator->validateStudentExists($sanitizedData['student_id']); | |
| if (!$studentValidation['valid']) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => $studentValidation['error'] | |
| ]; | |
| logApiRequest($requestData, $response, $studentValidation['http_code']); | |
| sendResponse($response, $studentValidation['http_code']); | |
| } | |
| $student = $studentValidation['student']; | |
| // Add level_name for receipt | |
| $result['data']['level_name'] = $student['level_name'] ?? ''; | |
| // Step 5: Validate teller number | |
| $tellerValidation = $validator->validateTellerNumber($sanitizedData['teller_no']); | |
| if (!$tellerValidation['valid']) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => $tellerValidation['error'] | |
| ]; | |
| logApiRequest($requestData, $response, $tellerValidation['http_code']); | |
| sendResponse($response, $tellerValidation['http_code']); | |
| } | |
| $teller = $tellerValidation['teller']; | |
| // Step 6: Validate amount doesn't exceed unreconciled amount | |
| if ($sanitizedData['amount'] > $teller['unreconciled_amount']) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => 'Amount exceeds unreconciled amount on teller', | |
| 'errors' => [ | |
| 'amount' => 'Requested amount (' . number_format($sanitizedData['amount'], 2) . | |
| ') exceeds available unreconciled amount (' . | |
| number_format($teller['unreconciled_amount'], 2) . ')' | |
| ] | |
| ]; | |
| logApiRequest($requestData, $response, 400); | |
| sendResponse($response, 400); | |
| } | |
| // Step 7: Get outstanding fees for the student | |
| $processor = new PaymentProcessor($pdo); | |
| $outstandingFees = $processor->getOutstandingFees($student['id']); | |
| if (empty($outstandingFees)) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => 'No outstanding fees found for this student' | |
| ]; | |
| logApiRequest($requestData, $response, 400); | |
| sendResponse($response, 400); | |
| } | |
| // Step 8: Prepare payment parameters | |
| // The API will automatically allocate the payment to outstanding fees (oldest first) | |
| $paymentParams = [ | |
| 'student_id' => $student['id'], | |
| 'student_code' => $student['student_code'], | |
| 'selected_fees' => $outstandingFees, | |
| 'teller_number' => $sanitizedData['teller_no'], | |
| 'payment_date' => $sanitizedData['payment_date'], // Use provided date | |
| 'amount_to_use' => $sanitizedData['amount'], | |
| 'source' => 'api' // Mark this payment as API-initiated | |
| ]; | |
| // Step 9: Process payment using the internal payment logic | |
| $result = $processor->processPayment($paymentParams); | |
| if ($result['success']) { | |
| // Fetch ALL fees for comprehensive receipt (matching web interface behavior) | |
| // This ensures the receipt shows complete fee picture, not just current transaction | |
| $studentId = $result['data']['student_id']; | |
| $paymentDate = $result['data']['payment_date']; | |
| $receiptNo = $result['data']['receipt_no']; | |
| // Fetch all fees from receivables | |
| $sqlFees = "SELECT ar.fee_id, ar.actual_value as amount_billed, | |
| ar.academic_session, ar.term_of_session, | |
| sf.description as fee_description | |
| FROM tb_account_receivables ar | |
| JOIN tb_account_school_fees sf ON ar.fee_id = sf.id | |
| WHERE ar.student_id = :sid | |
| ORDER BY ar.academic_session ASC, ar.term_of_session ASC"; | |
| $stmtFees = $pdo->prepare($sqlFees); | |
| $stmtFees->execute(['sid' => $studentId]); | |
| $allFees = $stmtFees->fetchAll(PDO::FETCH_ASSOC); | |
| $allocations = []; | |
| $receiptTotalPaid = 0; | |
| foreach ($allFees as $fee) { | |
| // Calculate Paid To Date (up to this receipt's date) | |
| $sqlPaid = "SELECT SUM(amount_paid) as total_paid | |
| FROM tb_account_payment_registers | |
| WHERE student_id = :sid | |
| AND fee_id = :fid | |
| AND academic_session = :as | |
| AND term_of_session = :ts | |
| AND payment_date <= :pd"; | |
| $stmtPaid = $pdo->prepare($sqlPaid); | |
| $stmtPaid->execute([ | |
| 'sid' => $studentId, | |
| 'fid' => $fee['fee_id'], | |
| 'as' => $fee['academic_session'], | |
| 'ts' => $fee['term_of_session'], | |
| 'pd' => $paymentDate | |
| ]); | |
| $paidResult = $stmtPaid->fetch(PDO::FETCH_ASSOC); | |
| $paidToDate = floatval($paidResult['total_paid'] ?? 0); | |
| // Calculate Amount paid IN THIS RECEIPT (for total calculation) | |
| $sqlReceiptPay = "SELECT SUM(amount_paid) as receipt_paid | |
| FROM tb_account_payment_registers | |
| WHERE receipt_no = :rno | |
| AND fee_id = :fid | |
| AND academic_session = :as | |
| AND term_of_session = :ts"; | |
| $stmtReceiptPay = $pdo->prepare($sqlReceiptPay); | |
| $stmtReceiptPay->execute([ | |
| 'rno' => $receiptNo, | |
| 'fid' => $fee['fee_id'], | |
| 'as' => $fee['academic_session'], | |
| 'ts' => $fee['term_of_session'] | |
| ]); | |
| $receiptPayResult = $stmtReceiptPay->fetch(PDO::FETCH_ASSOC); | |
| $paidInReceipt = floatval($receiptPayResult['receipt_paid'] ?? 0); | |
| $receiptTotalPaid += $paidInReceipt; | |
| $balance = floatval($fee['amount_billed']) - $paidToDate; | |
| // Show if (Balance > 0) OR (PaidInReceipt > 0) | |
| // This filters out old fully paid fees but keeps current payments | |
| if ($balance > 0.001 || $paidInReceipt > 0.001) { | |
| $allocations[] = [ | |
| 'description' => $fee['fee_description'], | |
| 'academic_session' => $fee['academic_session'], | |
| 'term_of_session' => $fee['term_of_session'], | |
| 'amount_billed' => floatval($fee['amount_billed']), | |
| 'amount' => $paidInReceipt, | |
| 'total_paid_to_date' => $paidToDate, | |
| 'balance' => $balance | |
| ]; | |
| } | |
| } | |
| // Prepare comprehensive receipt data | |
| $receiptData = [ | |
| 'receipt_no' => $receiptNo, | |
| 'student_name' => $student['full_name'], | |
| 'student_code' => $student['student_code'], | |
| 'level_name' => $student['level_name'] ?? '', | |
| 'payment_date' => $paymentDate, | |
| 'total_paid' => $receiptTotalPaid, | |
| 'allocations' => $allocations | |
| ]; | |
| // Generate receipt with complete fee information | |
| $generator = new ReceiptGenerator(); | |
| $receiptBase64 = $generator->generateBase64($receiptData); | |
| $response = [ | |
| 'status' => 'success', | |
| 'message' => $result['message'], | |
| 'data' => [ | |
| 'student_id' => $result['data']['student_id'], | |
| 'teller_no' => $result['data']['teller_no'], | |
| 'amount' => $result['data']['total_paid'], | |
| 'payment_id' => $result['data']['transaction_id'], | |
| 'receipt_no' => $result['data']['receipt_no'], | |
| 'payment_date' => $result['data']['payment_date'], | |
| 'receipt_image' => $receiptBase64, | |
| 'fees_settled' => array_map(function ($allocation) { | |
| return [ | |
| 'fee_description' => $allocation['description'], | |
| 'session' => $allocation['academic_session'], | |
| 'term' => $allocation['term_of_session'], | |
| 'amount' => $allocation['amount'] | |
| ]; | |
| }, $result['data']['allocations']) | |
| ] | |
| ]; | |
| logApiRequest($requestData, $response, 201); | |
| sendResponse($response, 201); | |
| } else { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => $result['message'] | |
| ]; | |
| logApiRequest($requestData, $response, 500); | |
| sendResponse($response, 500); | |
| } | |
| } catch (Exception $e) { | |
| $response = [ | |
| 'status' => 'error', | |
| 'message' => 'Internal server error', | |
| 'error_detail' => $e->getMessage() // Remove in production | |
| ]; | |
| logApiRequest($requestData ?? [], $response, 500); | |
| sendResponse($response, 500); | |
| } | |