(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); }