php / easypay-api /includes /ApiValidator.php
kingkay000's picture
Upload 25 files
e31284f verified
<?php
/**
* 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'] ?? '')
];
}
}