Spaces:
Running
Running
| /** | |
| * ApiValidator Class | |
| * | |
| * Handles API request validation, authentication, and input sanitization | |
| */ | |
| class ApiValidator | |
| { | |
| private $pdo; | |
| private $apiKeys; | |
| public function __construct($pdo, $apiKeys = []) | |
| { | |
| $this->pdo = $pdo; | |
| $this->apiKeys = $apiKeys; | |
| } | |
| /** | |
| * Validate API request | |
| * Checks Content-Type and optionally validates API key | |
| * | |
| * @param array $allowedMethods Array of allowed HTTP methods (e.g. ['POST', 'GET']) | |
| * @return array Validation result | |
| */ | |
| public function validateRequest($allowedMethods = ['POST']) | |
| { | |
| // Check request method | |
| $method = $_SERVER['REQUEST_METHOD']; | |
| if (!in_array($method, $allowedMethods)) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => "Invalid request method. Allowed: " . implode(', ', $allowedMethods), | |
| 'http_code' => 405 | |
| ]; | |
| } | |
| // Check Content-Type header (only for methods with body) | |
| if (in_array($method, ['POST', 'PUT', 'PATCH'])) { | |
| $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; | |
| if (stripos($contentType, 'application/json') === false) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Invalid Content-Type. Expected application/json', | |
| 'http_code' => 400 | |
| ]; | |
| } | |
| } | |
| // Validate API key if configured | |
| if (!empty($this->apiKeys)) { | |
| $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; | |
| if (empty($authHeader)) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Missing Authorization header', | |
| 'http_code' => 401 | |
| ]; | |
| } | |
| // Extract Bearer token | |
| if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Invalid Authorization header format. Expected: Bearer <API_KEY>', | |
| 'http_code' => 401 | |
| ]; | |
| } | |
| $apiKey = $matches[1]; | |
| if (!in_array($apiKey, $this->apiKeys)) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Invalid API key', | |
| 'http_code' => 401 | |
| ]; | |
| } | |
| } | |
| return ['valid' => true]; | |
| } | |
| /** | |
| * Parse and validate JSON payload | |
| * | |
| * @return array Parsed data or error | |
| */ | |
| public function parseJsonPayload() | |
| { | |
| $rawInput = file_get_contents('php://input'); | |
| if (empty($rawInput)) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Empty request body', | |
| 'http_code' => 400 | |
| ]; | |
| } | |
| $data = json_decode($rawInput, true); | |
| if (json_last_error() !== JSON_ERROR_NONE) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Invalid JSON: ' . json_last_error_msg(), | |
| 'http_code' => 400 | |
| ]; | |
| } | |
| return [ | |
| 'valid' => true, | |
| 'data' => $data | |
| ]; | |
| } | |
| /** | |
| * Validate payment request fields | |
| * | |
| * @param array $data Request data | |
| * @return array Validation result with errors | |
| */ | |
| public function validatePaymentRequest($data) | |
| { | |
| $errors = []; | |
| // Validate student_id | |
| if (empty($data['student_id'])) { | |
| $errors['student_id'] = 'Student ID is required'; | |
| } elseif (!is_string($data['student_id']) && !is_numeric($data['student_id'])) { | |
| $errors['student_id'] = 'Student ID must be a string or number'; | |
| } | |
| // Validate teller_no | |
| if (empty($data['teller_no'])) { | |
| $errors['teller_no'] = 'Teller number is required'; | |
| } elseif (!is_string($data['teller_no']) && !is_numeric($data['teller_no'])) { | |
| $errors['teller_no'] = 'Teller number must be a string or number'; | |
| } | |
| // Validate amount | |
| if (!isset($data['amount'])) { | |
| $errors['amount'] = 'Amount is required'; | |
| } elseif (!is_numeric($data['amount'])) { | |
| $errors['amount'] = 'Amount must be a number'; | |
| } elseif (floatval($data['amount']) <= 0) { | |
| $errors['amount'] = 'Amount must be greater than zero'; | |
| } | |
| // Validate payment_date | |
| if (empty($data['payment_date'])) { | |
| $errors['payment_date'] = 'Payment date is required'; | |
| } elseif (!strtotime($data['payment_date'])) { | |
| $errors['payment_date'] = 'Invalid payment date format'; | |
| } | |
| if (!empty($errors)) { | |
| return [ | |
| 'valid' => false, | |
| 'errors' => $errors, | |
| 'http_code' => 400 | |
| ]; | |
| } | |
| return ['valid' => true]; | |
| } | |
| /** | |
| * Validate student exists in database | |
| * | |
| * @param string $studentId Student ID | |
| * @return array Validation result with student data | |
| */ | |
| public function validateStudentExists($studentId) | |
| { | |
| $sql = "SELECT sr.id, sr.student_code, | |
| CONCAT(sr.last_name, ' ', sr.first_name, ' ', COALESCE(sr.other_name, '')) AS full_name, | |
| sr.admission_status, | |
| al.level_name | |
| FROM tb_student_registrations sr | |
| LEFT JOIN tb_academic_levels al ON sr.level_id = al.id | |
| WHERE sr.id = :student_id"; | |
| $stmt = $this->pdo->prepare($sql); | |
| $stmt->execute(['student_id' => $studentId]); | |
| $student = $stmt->fetch(); | |
| if (!$student) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => "Student not found (ID searched: '$studentId')", | |
| 'http_code' => 404 | |
| ]; | |
| } | |
| if ($student['admission_status'] !== 'Active') { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Student is not active', | |
| 'http_code' => 400 | |
| ]; | |
| } | |
| return [ | |
| 'valid' => true, | |
| 'student' => $student | |
| ]; | |
| } | |
| /** | |
| * Validate teller number exists and has unreconciled amount | |
| * | |
| * @param string $tellerNo Teller number | |
| * @return array Validation result with teller data | |
| */ | |
| public function validateTellerNumber($tellerNo) | |
| { | |
| $sql = "SELECT | |
| bs.id, | |
| bs.description, | |
| bs.amount_paid, | |
| bs.payment_date, | |
| COALESCE(fp.total_registered_fee, 0.00) AS registered_amount, | |
| (bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount | |
| FROM tb_account_bank_statements bs | |
| LEFT JOIN ( | |
| SELECT teller_no, SUM(amount_paid) AS total_registered_fee | |
| FROM tb_account_school_fee_payments | |
| GROUP BY teller_no | |
| ) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no | |
| WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number | |
| LIMIT 1"; | |
| $stmt = $this->pdo->prepare($sql); | |
| $stmt->execute(['teller_number' => $tellerNo]); | |
| $teller = $stmt->fetch(); | |
| if (!$teller) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Teller number not found', | |
| 'http_code' => 404 | |
| ]; | |
| } | |
| if ($teller['unreconciled_amount'] <= 0) { | |
| return [ | |
| 'valid' => false, | |
| 'error' => 'Teller number has no unreconciled amount', | |
| 'http_code' => 400 | |
| ]; | |
| } | |
| return [ | |
| 'valid' => true, | |
| 'teller' => $teller | |
| ]; | |
| } | |
| /** | |
| * Check for duplicate teller number usage | |
| * Prevents the same teller number from being used multiple times | |
| * | |
| * @param string $tellerNo Teller number | |
| * @param string $studentId Student ID (optional, for better error message) | |
| * @return array Validation result | |
| */ | |
| public function checkTellerDuplicate($tellerNo, $studentId = null) | |
| { | |
| // Note: The system allows the same teller to be used for different students | |
| // This check is to ensure the teller has available unreconciled amount | |
| // The actual duplicate check is handled by the validateTellerNumber method | |
| return ['valid' => true]; | |
| } | |
| /** | |
| * Sanitize input data | |
| * | |
| * @param array $data Input data | |
| * @return array Sanitized data | |
| */ | |
| public function sanitizeInput($data) | |
| { | |
| return [ | |
| 'student_id' => trim($data['student_id'] ?? ''), | |
| 'teller_no' => trim($data['teller_no'] ?? ''), | |
| 'amount' => floatval($data['amount'] ?? 0), | |
| 'payment_date' => trim($data['payment_date'] ?? '') | |
| ]; | |
| } | |
| } | |