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