Spaces:
Running
Running
Upload 18 files
Browse files- easypay/README.md +204 -0
- easypay/ajax_handlers.php +122 -0
- easypay/api/.htaccess +21 -0
- easypay/api/API_DOCUMENTATION.md +414 -0
- easypay/api/README.md +323 -0
- easypay/api/payments/process.php +249 -0
- easypay/api/test.html +350 -0
- easypay/assets/logo.png +0 -0
- easypay/config/api_config.php +30 -0
- easypay/db_config.php +27 -0
- easypay/download_receipt.php +111 -0
- easypay/includes/ApiValidator.php +287 -0
- easypay/includes/PaymentProcessor.php +564 -0
- easypay/includes/ReceiptGenerator.php +170 -0
- easypay/index.php +711 -0
- easypay/logs/api.log +3 -0
- easypay/process_payment - v0.php +663 -0
- easypay/process_payment.php +668 -0
easypay/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Student Fee Payment Registration System
|
| 2 |
+
|
| 3 |
+
A streamlined 2-page PHP application that simplifies the school fee payment process by automatically allocating payments across outstanding fees from oldest to newest.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **AJAX-powered student search** - Find students quickly by name or student code
|
| 8 |
+
- **Outstanding fees display** - View all unpaid fees sorted from oldest to newest
|
| 9 |
+
- **Automatic payment allocation** - Payments are automatically distributed across selected fees in chronological order
|
| 10 |
+
- **Teller validation** - Automatic lookup of bank statements with unreconciled amount calculation
|
| 11 |
+
- **Duplicate prevention** - Prevents multiple payments for the same student on the same date
|
| 12 |
+
- **Transactional integrity** - All database operations wrapped in transactions with automatic rollback on error
|
| 13 |
+
- **Comprehensive receipts** - Detailed payment confirmation with all settled fees
|
| 14 |
+
|
| 15 |
+
## Database Tables Used
|
| 16 |
+
|
| 17 |
+
The application interacts with the following tables in the `one_arps_aci` database:
|
| 18 |
+
|
| 19 |
+
- `tb_student_registrations` - Student information
|
| 20 |
+
- `tb_academic_levels` - Academic level details
|
| 21 |
+
- `tb_account_school_fees` - Fee definitions
|
| 22 |
+
- `tb_account_receivables` - Billed fees (invoices)
|
| 23 |
+
- `tb_account_bank_statements` - Bank transaction data
|
| 24 |
+
- `tb_account_school_fee_payments` - Individual fee payment records
|
| 25 |
+
- `tb_account_school_fee_sum_payments` - Aggregated payment records
|
| 26 |
+
- `tb_account_student_payments` - Cumulative payment tracking
|
| 27 |
+
- `tb_account_payment_registers` - Receipt records
|
| 28 |
+
- `tb_student_logistics` - Student outstanding balance tracking
|
| 29 |
+
|
| 30 |
+
## Installation
|
| 31 |
+
|
| 32 |
+
### Prerequisites
|
| 33 |
+
|
| 34 |
+
- PHP 7.4 or higher
|
| 35 |
+
- MySQL 5.7 or higher
|
| 36 |
+
- Web server (Apache, Nginx, or PHP built-in server)
|
| 37 |
+
- Existing `one_arps_aci` database with required tables
|
| 38 |
+
|
| 39 |
+
### Setup Steps
|
| 40 |
+
|
| 41 |
+
1. **Copy files to your web directory**
|
| 42 |
+
```
|
| 43 |
+
c:\ESD\ARPS EasyPayments\
|
| 44 |
+
├── db_config.php
|
| 45 |
+
├── index.php
|
| 46 |
+
├── process_payment.php
|
| 47 |
+
└── ajax_handlers.php
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
2. **Configure database connection**
|
| 51 |
+
|
| 52 |
+
Edit `db_config.php` and update the following constants:
|
| 53 |
+
```php
|
| 54 |
+
define('DB_HOST', 'localhost'); // Your MySQL host
|
| 55 |
+
define('DB_USER', 'root'); // Your MySQL username
|
| 56 |
+
define('DB_PASS', ''); // Your MySQL password
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
3. **Set proper permissions**
|
| 60 |
+
|
| 61 |
+
Ensure the web server has read access to all PHP files.
|
| 62 |
+
|
| 63 |
+
4. **Access the application**
|
| 64 |
+
|
| 65 |
+
Navigate to `http://localhost/ARPS%20EasyPayments/index.php` in your browser.
|
| 66 |
+
|
| 67 |
+
## Usage Guide
|
| 68 |
+
|
| 69 |
+
### Step 1: Search for a Student
|
| 70 |
+
|
| 71 |
+
1. Type the student's name or student code in the search box
|
| 72 |
+
2. Select the student from the dropdown results
|
| 73 |
+
3. The page will reload showing student details and outstanding fees
|
| 74 |
+
|
| 75 |
+
### Step 2: Review Outstanding Fees
|
| 76 |
+
|
| 77 |
+
- All outstanding fees are displayed in a table, sorted from oldest to newest
|
| 78 |
+
- Fees are pre-checked by default
|
| 79 |
+
- You can uncheck any fees you don't want to settle
|
| 80 |
+
- The table shows:
|
| 81 |
+
- Fee description
|
| 82 |
+
- Academic session and term
|
| 83 |
+
- Billed amount
|
| 84 |
+
- Amount already paid
|
| 85 |
+
- Outstanding balance
|
| 86 |
+
|
| 87 |
+
### Step 3: Process Payment
|
| 88 |
+
|
| 89 |
+
1. Click the **"Process Payment"** button
|
| 90 |
+
2. Enter the **Teller Number** from the bank statement
|
| 91 |
+
3. The system will automatically:
|
| 92 |
+
- Look up the bank statement
|
| 93 |
+
- Fill in the bank narration
|
| 94 |
+
- Calculate the unreconciled amount on the teller
|
| 95 |
+
4. Enter the **Amount to Use for Fees** (must not exceed unreconciled amount)
|
| 96 |
+
5. Click **"OK PROCEED!"**
|
| 97 |
+
|
| 98 |
+
### Step 4: View Confirmation
|
| 99 |
+
|
| 100 |
+
- On success, you'll see a detailed receipt showing:
|
| 101 |
+
- Student name and receipt number
|
| 102 |
+
- Payment date and teller information
|
| 103 |
+
- All fees that were settled
|
| 104 |
+
- Remaining unreconciled amount on the teller
|
| 105 |
+
- On failure, you'll see an error message (no database changes are made)
|
| 106 |
+
|
| 107 |
+
## Payment Allocation Logic
|
| 108 |
+
|
| 109 |
+
The system automatically allocates payments using the following rules:
|
| 110 |
+
|
| 111 |
+
1. **Oldest First** - Fees are sorted by academic session (ASC), then term (ASC)
|
| 112 |
+
2. **Full Settlement Priority** - Each fee is fully settled before moving to the next
|
| 113 |
+
3. **Partial Settlement** - If the payment amount runs out, the last fee is partially settled
|
| 114 |
+
4. **Automatic Calculation** - No manual allocation required
|
| 115 |
+
|
| 116 |
+
### Example
|
| 117 |
+
|
| 118 |
+
If a student has these outstanding fees:
|
| 119 |
+
- 2024 Term 1: ₦10,000
|
| 120 |
+
- 2024 Term 2: ₦15,000
|
| 121 |
+
- 2024 Term 3: ₦12,000
|
| 122 |
+
|
| 123 |
+
And you process a payment of ₦30,000:
|
| 124 |
+
- 2024 Term 1: Fully settled (₦10,000)
|
| 125 |
+
- 2024 Term 2: Fully settled (₦15,000)
|
| 126 |
+
- 2024 Term 3: Partially settled (₦5,000)
|
| 127 |
+
|
| 128 |
+
## ID Generation Rules
|
| 129 |
+
|
| 130 |
+
The application follows strict ID generation rules as per the database schema:
|
| 131 |
+
|
| 132 |
+
- **transaction_id**: `student_id + academic_session + payment_date`
|
| 133 |
+
- **school_fee_payment_id**: `student_code + fee_id + payment_date`
|
| 134 |
+
- **receipt_no**: `student_code + payment_date`
|
| 135 |
+
- **Date format**: Always `YYYY-MM-DD`
|
| 136 |
+
|
| 137 |
+
## Error Handling
|
| 138 |
+
|
| 139 |
+
The application includes comprehensive error handling:
|
| 140 |
+
|
| 141 |
+
- **Database connection failures** - Clear error message displayed
|
| 142 |
+
- **Invalid teller numbers** - Validation before processing
|
| 143 |
+
- **Duplicate payments** - Prevented at database level
|
| 144 |
+
- **Insufficient unreconciled amount** - Client and server-side validation
|
| 145 |
+
- **Transaction failures** - Automatic rollback with error details
|
| 146 |
+
|
| 147 |
+
## Security Features
|
| 148 |
+
|
| 149 |
+
- **PDO with prepared statements** - Protection against SQL injection
|
| 150 |
+
- **Input validation** - Server-side validation of all inputs
|
| 151 |
+
- **Output sanitization** - All user data is escaped before display
|
| 152 |
+
- **Transaction integrity** - All-or-nothing database operations
|
| 153 |
+
|
| 154 |
+
## Browser Compatibility
|
| 155 |
+
|
| 156 |
+
- Chrome (recommended)
|
| 157 |
+
- Firefox
|
| 158 |
+
- Edge
|
| 159 |
+
- Safari
|
| 160 |
+
|
| 161 |
+
## Troubleshooting
|
| 162 |
+
|
| 163 |
+
### "Database connection failed"
|
| 164 |
+
- Check your database credentials in `db_config.php`
|
| 165 |
+
- Ensure MySQL server is running
|
| 166 |
+
- Verify the database name is `one_arps_aci`
|
| 167 |
+
|
| 168 |
+
### "Teller number not found"
|
| 169 |
+
- Verify the teller number exists in `tb_account_bank_statements`
|
| 170 |
+
- Check that the teller number is the last token in the description field
|
| 171 |
+
- Ensure there's unreconciled amount available
|
| 172 |
+
|
| 173 |
+
### "A payment for this student has already been registered on this date"
|
| 174 |
+
- This is a duplicate prevention feature
|
| 175 |
+
- You cannot process multiple payments for the same student on the same date
|
| 176 |
+
- Use a different payment date or check existing records
|
| 177 |
+
|
| 178 |
+
## Technical Notes
|
| 179 |
+
|
| 180 |
+
### Teller Number Extraction
|
| 181 |
+
|
| 182 |
+
The system extracts teller information from `tb_account_bank_statements.description` using this logic:
|
| 183 |
+
- **Teller Number**: Last token (rightmost word) after splitting by spaces
|
| 184 |
+
- **Teller Name**: All text before the last space
|
| 185 |
+
|
| 186 |
+
Example: `"SCHOOL FEES PAYMENT 1234567890"`
|
| 187 |
+
- Teller Number: `1234567890`
|
| 188 |
+
- Teller Name: `SCHOOL FEES PAYMENT`
|
| 189 |
+
|
| 190 |
+
### Unreconciled Amount Calculation
|
| 191 |
+
|
| 192 |
+
```sql
|
| 193 |
+
unreconciled_amount = bank_statement.amount_paid - SUM(school_fee_payments.amount_paid)
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
This shows how much of the bank deposit has not yet been allocated to student fees.
|
| 197 |
+
|
| 198 |
+
## Support
|
| 199 |
+
|
| 200 |
+
For issues or questions, please contact the system administrator.
|
| 201 |
+
|
| 202 |
+
## License
|
| 203 |
+
|
| 204 |
+
Internal use only - ARPS School Management System
|
easypay/ajax_handlers.php
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* AJAX Handlers
|
| 4 |
+
* Endpoints for student search and teller lookup
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
require_once 'db_config.php';
|
| 8 |
+
|
| 9 |
+
header('Content-Type: application/json');
|
| 10 |
+
|
| 11 |
+
$action = $_GET['action'] ?? '';
|
| 12 |
+
|
| 13 |
+
try {
|
| 14 |
+
switch ($action) {
|
| 15 |
+
case 'search_students':
|
| 16 |
+
searchStudents($pdo);
|
| 17 |
+
break;
|
| 18 |
+
|
| 19 |
+
case 'lookup_teller':
|
| 20 |
+
lookupTeller($pdo);
|
| 21 |
+
break;
|
| 22 |
+
|
| 23 |
+
default:
|
| 24 |
+
echo json_encode(['error' => 'Invalid action']);
|
| 25 |
+
}
|
| 26 |
+
} catch (Exception $e) {
|
| 27 |
+
echo json_encode(['error' => $e->getMessage()]);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Search for students by name or student code
|
| 32 |
+
*/
|
| 33 |
+
function searchStudents($pdo)
|
| 34 |
+
{
|
| 35 |
+
$search = $_GET['search'] ?? '';
|
| 36 |
+
|
| 37 |
+
if (strlen($search) < 2) {
|
| 38 |
+
echo json_encode([]);
|
| 39 |
+
return;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
$sql = "SELECT
|
| 43 |
+
id,
|
| 44 |
+
student_code,
|
| 45 |
+
CONCAT(last_name, ' ', first_name, ' ', COALESCE(other_name, '')) AS full_name
|
| 46 |
+
FROM tb_student_registrations
|
| 47 |
+
WHERE
|
| 48 |
+
admission_status = 'Active' -- New condition added here
|
| 49 |
+
AND (
|
| 50 |
+
student_code LIKE :search1
|
| 51 |
+
OR last_name LIKE :search2
|
| 52 |
+
OR first_name LIKE :search3
|
| 53 |
+
OR other_name LIKE :search4
|
| 54 |
+
OR CONCAT(last_name, ' ', first_name, ' ', COALESCE(other_name, '')) LIKE :search5
|
| 55 |
+
)
|
| 56 |
+
ORDER BY last_name, first_name
|
| 57 |
+
LIMIT 20";
|
| 58 |
+
|
| 59 |
+
$stmt = $pdo->prepare($sql);
|
| 60 |
+
$searchParam = '%' . $search . '%';
|
| 61 |
+
$stmt->execute([
|
| 62 |
+
'search1' => $searchParam,
|
| 63 |
+
'search2' => $searchParam,
|
| 64 |
+
'search3' => $searchParam,
|
| 65 |
+
'search4' => $searchParam,
|
| 66 |
+
'search5' => $searchParam
|
| 67 |
+
]);
|
| 68 |
+
|
| 69 |
+
$results = $stmt->fetchAll();
|
| 70 |
+
echo json_encode($results);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Lookup bank statement by teller number
|
| 75 |
+
*/
|
| 76 |
+
function lookupTeller($pdo)
|
| 77 |
+
{
|
| 78 |
+
$tellerNumber = $_GET['teller_number'] ?? '';
|
| 79 |
+
|
| 80 |
+
if (empty($tellerNumber)) {
|
| 81 |
+
echo json_encode(['error' => 'Teller number is required']);
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Query to find bank statement and calculate unreconciled amount
|
| 86 |
+
$sql = "SELECT
|
| 87 |
+
bs.id,
|
| 88 |
+
bs.description,
|
| 89 |
+
bs.amount_paid,
|
| 90 |
+
bs.payment_date,
|
| 91 |
+
COALESCE(fp.total_registered_fee, 0.00) AS registered_amount,
|
| 92 |
+
(bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount
|
| 93 |
+
FROM tb_account_bank_statements bs
|
| 94 |
+
LEFT JOIN (
|
| 95 |
+
SELECT teller_no, SUM(amount_paid) AS total_registered_fee
|
| 96 |
+
FROM tb_account_school_fee_payments
|
| 97 |
+
GROUP BY teller_no
|
| 98 |
+
) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no
|
| 99 |
+
WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number
|
| 100 |
+
HAVING unreconciled_amount >= 0
|
| 101 |
+
LIMIT 1";
|
| 102 |
+
|
| 103 |
+
$stmt = $pdo->prepare($sql);
|
| 104 |
+
$stmt->execute(['teller_number' => $tellerNumber]);
|
| 105 |
+
|
| 106 |
+
$result = $stmt->fetch();
|
| 107 |
+
|
| 108 |
+
if (!$result) {
|
| 109 |
+
echo json_encode(['error' => 'Teller number not found or fully reconciled']);
|
| 110 |
+
return;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Extract teller name (description without last token)
|
| 114 |
+
$descParts = explode(' ', $result['description']);
|
| 115 |
+
array_pop($descParts); // Remove teller number
|
| 116 |
+
$tellerName = implode(' ', $descParts);
|
| 117 |
+
|
| 118 |
+
$result['teller_name'] = $tellerName;
|
| 119 |
+
$result['teller_no'] = $tellerNumber;
|
| 120 |
+
|
| 121 |
+
echo json_encode($result);
|
| 122 |
+
}
|
easypay/api/.htaccess
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Payment API .htaccess
|
| 2 |
+
# Enable clean URLs for API endpoints
|
| 3 |
+
|
| 4 |
+
<IfModule mod_rewrite.c>
|
| 5 |
+
RewriteEngine On
|
| 6 |
+
RewriteBase /easypay/api/
|
| 7 |
+
|
| 8 |
+
# Route /api/payments/process to process.php
|
| 9 |
+
RewriteRule ^payments/process$ payments/process.php [L]
|
| 10 |
+
</IfModule>
|
| 11 |
+
|
| 12 |
+
# Security headers
|
| 13 |
+
<IfModule mod_headers.c>
|
| 14 |
+
# Prevent directory listing
|
| 15 |
+
Options -Indexes
|
| 16 |
+
|
| 17 |
+
# Security headers
|
| 18 |
+
Header set X-Content-Type-Options "nosniff"
|
| 19 |
+
Header set X-Frame-Options "DENY"
|
| 20 |
+
Header set X-XSS-Protection "1; mode=block"
|
| 21 |
+
</IfModule>
|
easypay/api/API_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Payment Processing API Documentation
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Payment Processing API allows external systems to initiate student fee payments securely via JSON requests. The API uses the same internal payment processing logic as the web application, ensuring consistency and reliability.
|
| 6 |
+
|
| 7 |
+
## Base URL
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
http://your-domain.com/easypay/api/
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## Authentication
|
| 14 |
+
|
| 15 |
+
### API Key (Optional)
|
| 16 |
+
|
| 17 |
+
If API authentication is enabled, include an API key in the request header:
|
| 18 |
+
|
| 19 |
+
```
|
| 20 |
+
Authorization: Bearer YOUR_API_KEY
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
To enable API authentication:
|
| 24 |
+
1. Edit `config/api_config.php`
|
| 25 |
+
2. Set `API_AUTH_ENABLED` to `true`
|
| 26 |
+
3. Add your API keys to the `API_KEYS` array
|
| 27 |
+
|
| 28 |
+
## Endpoint
|
| 29 |
+
|
| 30 |
+
### Process Payment
|
| 31 |
+
|
| 32 |
+
**POST** `/api/payments/process`
|
| 33 |
+
|
| 34 |
+
Processes a student fee payment using a bank teller number.
|
| 35 |
+
|
| 36 |
+
#### Request Headers
|
| 37 |
+
|
| 38 |
+
```
|
| 39 |
+
Content-Type: application/json
|
| 40 |
+
Authorization: Bearer YOUR_API_KEY (if authentication is enabled)
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
#### Request Body
|
| 44 |
+
|
| 45 |
+
```json
|
| 46 |
+
{
|
| 47 |
+
"student_id": "string|integer",
|
| 48 |
+
"teller_no": "string",
|
| 49 |
+
"amount": number
|
| 50 |
+
}
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
**Field Descriptions:**
|
| 54 |
+
|
| 55 |
+
| Field | Type | Required | Description |
|
| 56 |
+
|-------|------|----------|-------------|
|
| 57 |
+
| `student_id` | string/integer | Yes | The unique ID of the student from `tb_student_registrations` |
|
| 58 |
+
| `teller_no` | string | Yes | The teller number from the bank statement (must exist in `tb_account_bank_statements`) |
|
| 59 |
+
| `amount` | number | Yes | The amount to allocate to student fees (must be positive and not exceed unreconciled amount) |
|
| 60 |
+
|
| 61 |
+
#### Success Response (HTTP 201)
|
| 62 |
+
|
| 63 |
+
```json
|
| 64 |
+
{
|
| 65 |
+
"status": "success",
|
| 66 |
+
"message": "Payment processed successfully",
|
| 67 |
+
"data": {
|
| 68 |
+
"student_id": "000001234567890123",
|
| 69 |
+
"teller_no": "1234567890",
|
| 70 |
+
"amount": 50000.00,
|
| 71 |
+
"payment_id": "000001234567890123202420260109",
|
| 72 |
+
"receipt_no": "STU001202420260109",
|
| 73 |
+
"payment_date": "2026-01-09",
|
| 74 |
+
"fees_settled": [
|
| 75 |
+
{
|
| 76 |
+
"fee_description": "Tuition Fee",
|
| 77 |
+
"session": "2024",
|
| 78 |
+
"term": "1",
|
| 79 |
+
"amount": 30000.00
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"fee_description": "Development Levy",
|
| 83 |
+
"session": "2024",
|
| 84 |
+
"term": "1",
|
| 85 |
+
"amount": 20000.00
|
| 86 |
+
}
|
| 87 |
+
]
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
#### Error Responses
|
| 93 |
+
|
| 94 |
+
**Validation Error (HTTP 400)**
|
| 95 |
+
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"status": "error",
|
| 99 |
+
"message": "Validation failed",
|
| 100 |
+
"errors": {
|
| 101 |
+
"student_id": "Student ID is required",
|
| 102 |
+
"amount": "Amount must be greater than zero"
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**Student Not Found (HTTP 404)**
|
| 108 |
+
|
| 109 |
+
```json
|
| 110 |
+
{
|
| 111 |
+
"status": "error",
|
| 112 |
+
"message": "Student not found"
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
**Teller Not Found (HTTP 404)**
|
| 117 |
+
|
| 118 |
+
```json
|
| 119 |
+
{
|
| 120 |
+
"status": "error",
|
| 121 |
+
"message": "Teller number not found"
|
| 122 |
+
}
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
**Amount Exceeds Unreconciled (HTTP 400)**
|
| 126 |
+
|
| 127 |
+
```json
|
| 128 |
+
{
|
| 129 |
+
"status": "error",
|
| 130 |
+
"message": "Amount exceeds unreconciled amount on teller",
|
| 131 |
+
"errors": {
|
| 132 |
+
"amount": "Requested amount (60,000.00) exceeds available unreconciled amount (50,000.00)"
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
**Duplicate Payment (HTTP 400)**
|
| 138 |
+
|
| 139 |
+
```json
|
| 140 |
+
{
|
| 141 |
+
"status": "error",
|
| 142 |
+
"message": "Internal server error",
|
| 143 |
+
"error_detail": "A payment for this student has already been registered on this date (2026-01-09)"
|
| 144 |
+
}
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
**Unauthorized (HTTP 401)**
|
| 148 |
+
|
| 149 |
+
```json
|
| 150 |
+
{
|
| 151 |
+
"status": "error",
|
| 152 |
+
"message": "Invalid API key"
|
| 153 |
+
}
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
**Invalid Content-Type (HTTP 400)**
|
| 157 |
+
|
| 158 |
+
```json
|
| 159 |
+
{
|
| 160 |
+
"status": "error",
|
| 161 |
+
"message": "Invalid Content-Type. Expected application/json"
|
| 162 |
+
}
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
**Server Error (HTTP 500)**
|
| 166 |
+
|
| 167 |
+
```json
|
| 168 |
+
{
|
| 169 |
+
"status": "error",
|
| 170 |
+
"message": "Internal server error"
|
| 171 |
+
}
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
## Payment Allocation Logic
|
| 175 |
+
|
| 176 |
+
The API automatically allocates payments using the following rules:
|
| 177 |
+
|
| 178 |
+
1. **Automatic Fee Selection**: The system fetches all outstanding fees for the student
|
| 179 |
+
2. **Oldest First**: Fees are sorted by academic session (ascending), then term (ascending)
|
| 180 |
+
3. **Full Settlement Priority**: Each fee is fully settled before moving to the next
|
| 181 |
+
4. **Partial Settlement**: If the payment amount runs out, the last fee is partially settled
|
| 182 |
+
5. **Transaction Integrity**: All database operations are wrapped in a transaction (all-or-nothing)
|
| 183 |
+
|
| 184 |
+
### Example
|
| 185 |
+
|
| 186 |
+
If a student has these outstanding fees:
|
| 187 |
+
- 2024 Term 1 Tuition: ₦30,000
|
| 188 |
+
- 2024 Term 2 Tuition: ₦30,000
|
| 189 |
+
- 2024 Term 3 Tuition: ₦30,000
|
| 190 |
+
|
| 191 |
+
And you send a payment of ₦50,000:
|
| 192 |
+
- 2024 Term 1: Fully settled (₦30,000)
|
| 193 |
+
- 2024 Term 2: Partially settled (₦20,000)
|
| 194 |
+
- 2024 Term 3: Not settled
|
| 195 |
+
|
| 196 |
+
## Validation Rules
|
| 197 |
+
|
| 198 |
+
### Student Validation
|
| 199 |
+
- Student ID must exist in `tb_student_registrations`
|
| 200 |
+
- Student must have `admission_status = 'Active'`
|
| 201 |
+
- Student must have outstanding fees
|
| 202 |
+
|
| 203 |
+
### Teller Validation
|
| 204 |
+
- Teller number must exist in `tb_account_bank_statements`
|
| 205 |
+
- Teller must have unreconciled amount > 0
|
| 206 |
+
- The teller number is extracted as the last word in the `description` field
|
| 207 |
+
|
| 208 |
+
### Amount Validation
|
| 209 |
+
- Amount must be a positive number
|
| 210 |
+
- Amount must not exceed the unreconciled amount on the teller
|
| 211 |
+
- Amount must not be zero or negative
|
| 212 |
+
|
| 213 |
+
### Duplicate Prevention
|
| 214 |
+
- Only one payment per student per day is allowed
|
| 215 |
+
- This prevents accidental duplicate submissions
|
| 216 |
+
|
| 217 |
+
## Example Requests
|
| 218 |
+
|
| 219 |
+
### cURL
|
| 220 |
+
|
| 221 |
+
```bash
|
| 222 |
+
curl -X POST http://your-domain.com/easypay/api/payments/process \
|
| 223 |
+
-H "Content-Type: application/json" \
|
| 224 |
+
-H "Authorization: Bearer your-api-key-here" \
|
| 225 |
+
-d '{
|
| 226 |
+
"student_id": "000001234567890123",
|
| 227 |
+
"teller_no": "1234567890",
|
| 228 |
+
"amount": 50000
|
| 229 |
+
}'
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
### PHP
|
| 233 |
+
|
| 234 |
+
```php
|
| 235 |
+
<?php
|
| 236 |
+
$url = 'http://your-domain.com/easypay/api/payments/process';
|
| 237 |
+
|
| 238 |
+
$data = [
|
| 239 |
+
'student_id' => '000001234567890123',
|
| 240 |
+
'teller_no' => '1234567890',
|
| 241 |
+
'amount' => 50000
|
| 242 |
+
];
|
| 243 |
+
|
| 244 |
+
$options = [
|
| 245 |
+
'http' => [
|
| 246 |
+
'header' => [
|
| 247 |
+
'Content-Type: application/json',
|
| 248 |
+
'Authorization: Bearer your-api-key-here'
|
| 249 |
+
],
|
| 250 |
+
'method' => 'POST',
|
| 251 |
+
'content' => json_encode($data)
|
| 252 |
+
]
|
| 253 |
+
];
|
| 254 |
+
|
| 255 |
+
$context = stream_context_create($options);
|
| 256 |
+
$response = file_get_contents($url, false, $context);
|
| 257 |
+
$result = json_decode($response, true);
|
| 258 |
+
|
| 259 |
+
if ($result['status'] === 'success') {
|
| 260 |
+
echo "Payment processed: " . $result['data']['payment_id'];
|
| 261 |
+
} else {
|
| 262 |
+
echo "Error: " . $result['message'];
|
| 263 |
+
}
|
| 264 |
+
?>
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
### JavaScript (Fetch API)
|
| 268 |
+
|
| 269 |
+
```javascript
|
| 270 |
+
const url = 'http://your-domain.com/easypay/api/payments/process';
|
| 271 |
+
|
| 272 |
+
const data = {
|
| 273 |
+
student_id: '000001234567890123',
|
| 274 |
+
teller_no: '1234567890',
|
| 275 |
+
amount: 50000
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
fetch(url, {
|
| 279 |
+
method: 'POST',
|
| 280 |
+
headers: {
|
| 281 |
+
'Content-Type': 'application/json',
|
| 282 |
+
'Authorization': 'Bearer your-api-key-here'
|
| 283 |
+
},
|
| 284 |
+
body: JSON.stringify(data)
|
| 285 |
+
})
|
| 286 |
+
.then(response => response.json())
|
| 287 |
+
.then(result => {
|
| 288 |
+
if (result.status === 'success') {
|
| 289 |
+
console.log('Payment processed:', result.data.payment_id);
|
| 290 |
+
} else {
|
| 291 |
+
console.error('Error:', result.message);
|
| 292 |
+
}
|
| 293 |
+
})
|
| 294 |
+
.catch(error => console.error('Request failed:', error));
|
| 295 |
+
```
|
| 296 |
+
|
| 297 |
+
### Python
|
| 298 |
+
|
| 299 |
+
```python
|
| 300 |
+
import requests
|
| 301 |
+
import json
|
| 302 |
+
|
| 303 |
+
url = 'http://your-domain.com/easypay/api/payments/process'
|
| 304 |
+
|
| 305 |
+
headers = {
|
| 306 |
+
'Content-Type': 'application/json',
|
| 307 |
+
'Authorization': 'Bearer your-api-key-here'
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
data = {
|
| 311 |
+
'student_id': '000001234567890123',
|
| 312 |
+
'teller_no': '1234567890',
|
| 313 |
+
'amount': 50000
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
response = requests.post(url, headers=headers, json=data)
|
| 317 |
+
result = response.json()
|
| 318 |
+
|
| 319 |
+
if result['status'] == 'success':
|
| 320 |
+
print(f"Payment processed: {result['data']['payment_id']}")
|
| 321 |
+
else:
|
| 322 |
+
print(f"Error: {result['message']}")
|
| 323 |
+
```
|
| 324 |
+
|
| 325 |
+
## Database Tables Affected
|
| 326 |
+
|
| 327 |
+
The API writes to the same tables as the web application:
|
| 328 |
+
|
| 329 |
+
1. `tb_account_school_fee_payments` - Individual fee payment records
|
| 330 |
+
2. `tb_account_school_fee_sum_payments` - Aggregated payment records
|
| 331 |
+
3. `tb_account_student_payments` - Cumulative payment tracking
|
| 332 |
+
4. `tb_account_payment_registers` - Receipt records
|
| 333 |
+
5. `tb_student_logistics` - Student outstanding balance updates
|
| 334 |
+
|
| 335 |
+
## Logging
|
| 336 |
+
|
| 337 |
+
API requests are logged to `logs/api.log` (if enabled in config).
|
| 338 |
+
|
| 339 |
+
Each log entry includes:
|
| 340 |
+
- Timestamp
|
| 341 |
+
- Client IP address
|
| 342 |
+
- Request data
|
| 343 |
+
- Response data
|
| 344 |
+
- HTTP status code
|
| 345 |
+
|
| 346 |
+
To enable/disable logging, edit `config/api_config.php`:
|
| 347 |
+
|
| 348 |
+
```php
|
| 349 |
+
define('API_LOG_ENABLED', true);
|
| 350 |
+
```
|
| 351 |
+
|
| 352 |
+
## Security Considerations
|
| 353 |
+
|
| 354 |
+
1. **HTTPS Required**: Always use HTTPS in production to encrypt API keys and payment data
|
| 355 |
+
2. **API Key Management**: Store API keys securely and rotate them regularly
|
| 356 |
+
3. **IP Whitelisting**: Consider restricting API access to specific IP addresses
|
| 357 |
+
4. **Rate Limiting**: Implement rate limiting to prevent abuse (future enhancement)
|
| 358 |
+
5. **Input Validation**: All inputs are validated and sanitized before processing
|
| 359 |
+
6. **SQL Injection Protection**: All database queries use prepared statements
|
| 360 |
+
7. **Transaction Integrity**: All operations are atomic (all-or-nothing)
|
| 361 |
+
|
| 362 |
+
## Testing
|
| 363 |
+
|
| 364 |
+
### Test with API Authentication Disabled
|
| 365 |
+
|
| 366 |
+
For initial testing, you can disable API authentication:
|
| 367 |
+
|
| 368 |
+
1. Edit `config/api_config.php`
|
| 369 |
+
2. Set `API_AUTH_ENABLED` to `false`
|
| 370 |
+
3. Test without the `Authorization` header
|
| 371 |
+
|
| 372 |
+
### Test Cases
|
| 373 |
+
|
| 374 |
+
1. **Valid Payment**: Send a valid request with existing student and teller
|
| 375 |
+
2. **Invalid Student**: Send request with non-existent student ID
|
| 376 |
+
3. **Invalid Teller**: Send request with non-existent teller number
|
| 377 |
+
4. **Excessive Amount**: Send amount greater than unreconciled amount
|
| 378 |
+
5. **Duplicate Payment**: Send two payments for same student on same day
|
| 379 |
+
6. **Invalid JSON**: Send malformed JSON
|
| 380 |
+
7. **Missing Fields**: Send request with missing required fields
|
| 381 |
+
|
| 382 |
+
## Troubleshooting
|
| 383 |
+
|
| 384 |
+
### "Database connection failed"
|
| 385 |
+
- Check database credentials in `db_config.php`
|
| 386 |
+
- Ensure MySQL server is running
|
| 387 |
+
|
| 388 |
+
### "Student not found"
|
| 389 |
+
- Verify student ID exists in `tb_student_registrations`
|
| 390 |
+
- Check that student has `admission_status = 'Active'`
|
| 391 |
+
|
| 392 |
+
### "Teller number not found"
|
| 393 |
+
- Verify teller exists in `tb_account_bank_statements`
|
| 394 |
+
- Check that teller number is the last word in the description field
|
| 395 |
+
|
| 396 |
+
### "Amount exceeds unreconciled amount"
|
| 397 |
+
- Check the unreconciled amount on the teller
|
| 398 |
+
- Reduce the payment amount
|
| 399 |
+
|
| 400 |
+
### "No outstanding fees found"
|
| 401 |
+
- Verify student has unpaid fees in `tb_account_receivables`
|
| 402 |
+
|
| 403 |
+
## Support
|
| 404 |
+
|
| 405 |
+
For technical support or questions, contact your system administrator.
|
| 406 |
+
|
| 407 |
+
## Version History
|
| 408 |
+
|
| 409 |
+
- **v1.0** (2026-01-09): Initial API release
|
| 410 |
+
- Payment processing endpoint
|
| 411 |
+
- JSON request/response format
|
| 412 |
+
- Optional API key authentication
|
| 413 |
+
- Comprehensive validation
|
| 414 |
+
- Transaction logging
|
easypay/api/README.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Payment Processing API - Implementation Guide
|
| 2 |
+
|
| 3 |
+
## 🎯 Overview
|
| 4 |
+
|
| 5 |
+
This implementation extends the existing EasyPay application to support external payment initiation via a secure JSON API, without breaking or altering any existing internal payment workflows.
|
| 6 |
+
|
| 7 |
+
## ✅ What Was Implemented
|
| 8 |
+
|
| 9 |
+
### 1. **Core Architecture**
|
| 10 |
+
|
| 11 |
+
- **`includes/PaymentProcessor.php`**: Encapsulates all payment processing logic
|
| 12 |
+
- Single source of truth for payment operations
|
| 13 |
+
- Used by both web UI and API
|
| 14 |
+
- Maintains all existing business rules
|
| 15 |
+
- Atomic transactions with rollback on error
|
| 16 |
+
|
| 17 |
+
- **`includes/ApiValidator.php`**: Handles API validation and security
|
| 18 |
+
- Request validation (method, headers, content-type)
|
| 19 |
+
- API key authentication (optional)
|
| 20 |
+
- Input sanitization
|
| 21 |
+
- Business rule validation
|
| 22 |
+
|
| 23 |
+
### 2. **API Endpoint**
|
| 24 |
+
|
| 25 |
+
- **`api/payments/process.php`**: Main API endpoint
|
| 26 |
+
- Accepts JSON POST requests
|
| 27 |
+
- Validates all inputs
|
| 28 |
+
- Reuses PaymentProcessor for business logic
|
| 29 |
+
- Returns structured JSON responses
|
| 30 |
+
- Logs all API requests
|
| 31 |
+
|
| 32 |
+
### 3. **Configuration**
|
| 33 |
+
|
| 34 |
+
- **`config/api_config.php`**: API settings
|
| 35 |
+
- API key management
|
| 36 |
+
- Authentication toggle
|
| 37 |
+
- Logging configuration
|
| 38 |
+
- CORS settings
|
| 39 |
+
|
| 40 |
+
### 4. **Documentation & Testing**
|
| 41 |
+
|
| 42 |
+
- **`api/API_DOCUMENTATION.md`**: Comprehensive API documentation
|
| 43 |
+
- **`api/test.html`**: Interactive API testing tool
|
| 44 |
+
- **`api/.htaccess`**: Clean URLs and security headers
|
| 45 |
+
|
| 46 |
+
## 📁 File Structure
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
easypay/
|
| 50 |
+
├── api/
|
| 51 |
+
│ ├── payments/
|
| 52 |
+
│ │ └── process.php # Main API endpoint
|
| 53 |
+
│ ├── .htaccess # URL routing & security
|
| 54 |
+
│ ├── API_DOCUMENTATION.md # Full API docs
|
| 55 |
+
│ └── test.html # API testing tool
|
| 56 |
+
├── config/
|
| 57 |
+
│ └── api_config.php # API configuration
|
| 58 |
+
├── includes/
|
| 59 |
+
│ ├── PaymentProcessor.php # Core payment logic
|
| 60 |
+
│ └── ApiValidator.php # API validation
|
| 61 |
+
├── logs/
|
| 62 |
+
│ └── api.log # API request logs (auto-created)
|
| 63 |
+
├── db_config.php # Database connection
|
| 64 |
+
├── index.php # Web UI (unchanged)
|
| 65 |
+
├── process_payment.php # Web payment handler (unchanged)
|
| 66 |
+
└── ajax_handlers.php # AJAX endpoints (unchanged)
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## 🚀 Quick Start
|
| 70 |
+
|
| 71 |
+
### 1. Access the API
|
| 72 |
+
|
| 73 |
+
**Endpoint URL:**
|
| 74 |
+
```
|
| 75 |
+
POST http://your-domain.com/easypay/api/payments/process
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
### 2. Test the API
|
| 79 |
+
|
| 80 |
+
Open the testing tool in your browser:
|
| 81 |
+
```
|
| 82 |
+
http://your-domain.com/easypay/api/test.html
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### 3. Send a Request
|
| 86 |
+
|
| 87 |
+
**Example Request:**
|
| 88 |
+
```bash
|
| 89 |
+
curl -X POST http://localhost/easypay/api/payments/process \
|
| 90 |
+
-H "Content-Type: application/json" \
|
| 91 |
+
-d '{
|
| 92 |
+
"student_id": "000001234567890123",
|
| 93 |
+
"teller_no": "1234567890",
|
| 94 |
+
"amount": 50000
|
| 95 |
+
}'
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
**Example Response:**
|
| 99 |
+
```json
|
| 100 |
+
{
|
| 101 |
+
"status": "success",
|
| 102 |
+
"message": "Payment processed successfully",
|
| 103 |
+
"data": {
|
| 104 |
+
"student_id": "000001234567890123",
|
| 105 |
+
"teller_no": "1234567890",
|
| 106 |
+
"amount": 50000.00,
|
| 107 |
+
"payment_id": "000001234567890123202420260109",
|
| 108 |
+
"receipt_no": "STU001202420260109",
|
| 109 |
+
"payment_date": "2026-01-09",
|
| 110 |
+
"fees_settled": [...]
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## 🔐 Security Configuration
|
| 116 |
+
|
| 117 |
+
### Enable API Key Authentication
|
| 118 |
+
|
| 119 |
+
1. Edit `config/api_config.php`:
|
| 120 |
+
|
| 121 |
+
```php
|
| 122 |
+
// Enable authentication
|
| 123 |
+
define('API_AUTH_ENABLED', true);
|
| 124 |
+
|
| 125 |
+
// Add your API keys
|
| 126 |
+
define('API_KEYS', [
|
| 127 |
+
'your-secret-key-123' => 'External System 1',
|
| 128 |
+
'another-secret-key-456' => 'Mobile App'
|
| 129 |
+
]);
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
2. Include API key in requests:
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
curl -X POST http://localhost/easypay/api/payments/process \
|
| 136 |
+
-H "Content-Type: application/json" \
|
| 137 |
+
-H "Authorization: Bearer your-secret-key-123" \
|
| 138 |
+
-d '{...}'
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## ✨ Key Features
|
| 142 |
+
|
| 143 |
+
### ✅ Backward Compatibility
|
| 144 |
+
- **Zero changes** to existing web UI
|
| 145 |
+
- **Zero changes** to existing payment logic
|
| 146 |
+
- **Zero changes** to database schema
|
| 147 |
+
- Web and API use the same payment processor
|
| 148 |
+
|
| 149 |
+
### ✅ Validation & Security
|
| 150 |
+
- ✓ Student ID validation (must exist and be active)
|
| 151 |
+
- ✓ Teller number validation (must exist with unreconciled amount)
|
| 152 |
+
- ✓ Amount validation (positive, within limits)
|
| 153 |
+
- ✓ Duplicate prevention (one payment per student per day)
|
| 154 |
+
- ✓ SQL injection protection (prepared statements)
|
| 155 |
+
- ✓ Input sanitization
|
| 156 |
+
- ✓ Optional API key authentication
|
| 157 |
+
- ✓ Transaction integrity (atomic operations)
|
| 158 |
+
|
| 159 |
+
### ✅ Automatic Payment Allocation
|
| 160 |
+
- Fetches all outstanding fees for the student
|
| 161 |
+
- Sorts fees by session/term (oldest first)
|
| 162 |
+
- Allocates payment automatically
|
| 163 |
+
- Fully settles fees before moving to next
|
| 164 |
+
- Partial settlement of last fee if needed
|
| 165 |
+
|
| 166 |
+
### ✅ Comprehensive Logging
|
| 167 |
+
- All API requests logged to `logs/api.log`
|
| 168 |
+
- Includes timestamp, IP, request, response, status
|
| 169 |
+
- Can be disabled in config
|
| 170 |
+
|
| 171 |
+
### ✅ Error Handling
|
| 172 |
+
- Structured error responses
|
| 173 |
+
- Appropriate HTTP status codes
|
| 174 |
+
- Detailed validation errors
|
| 175 |
+
- Transaction rollback on failure
|
| 176 |
+
|
| 177 |
+
## 🧪 Testing Checklist
|
| 178 |
+
|
| 179 |
+
### Test Cases
|
| 180 |
+
|
| 181 |
+
1. **✓ Valid Payment**
|
| 182 |
+
- Send valid student_id, teller_no, amount
|
| 183 |
+
- Expect HTTP 201 with payment details
|
| 184 |
+
|
| 185 |
+
2. **✓ Invalid Student**
|
| 186 |
+
- Send non-existent student_id
|
| 187 |
+
- Expect HTTP 404 "Student not found"
|
| 188 |
+
|
| 189 |
+
3. **✓ Invalid Teller**
|
| 190 |
+
- Send non-existent teller_no
|
| 191 |
+
- Expect HTTP 404 "Teller number not found"
|
| 192 |
+
|
| 193 |
+
4. **✓ Excessive Amount**
|
| 194 |
+
- Send amount > unreconciled amount
|
| 195 |
+
- Expect HTTP 400 "Amount exceeds..."
|
| 196 |
+
|
| 197 |
+
5. **✓ Duplicate Payment**
|
| 198 |
+
- Send two payments for same student on same day
|
| 199 |
+
- Expect HTTP 500 with duplicate error
|
| 200 |
+
|
| 201 |
+
6. **✓ Missing Fields**
|
| 202 |
+
- Send request without required fields
|
| 203 |
+
- Expect HTTP 400 with validation errors
|
| 204 |
+
|
| 205 |
+
7. **✓ Invalid JSON**
|
| 206 |
+
- Send malformed JSON
|
| 207 |
+
- Expect HTTP 400 "Invalid JSON"
|
| 208 |
+
|
| 209 |
+
8. **✓ Wrong Content-Type**
|
| 210 |
+
- Send without application/json header
|
| 211 |
+
- Expect HTTP 400 "Invalid Content-Type"
|
| 212 |
+
|
| 213 |
+
9. **✓ Invalid API Key** (if auth enabled)
|
| 214 |
+
- Send with wrong API key
|
| 215 |
+
- Expect HTTP 401 "Invalid API key"
|
| 216 |
+
|
| 217 |
+
## 📊 Database Impact
|
| 218 |
+
|
| 219 |
+
The API writes to the same tables as the web UI:
|
| 220 |
+
|
| 221 |
+
| Table | Purpose |
|
| 222 |
+
|-------|---------|
|
| 223 |
+
| `tb_account_school_fee_payments` | Individual fee payments |
|
| 224 |
+
| `tb_account_school_fee_sum_payments` | Aggregated payments |
|
| 225 |
+
| `tb_account_student_payments` | Cumulative payment tracking |
|
| 226 |
+
| `tb_account_payment_registers` | Receipt records |
|
| 227 |
+
| `tb_student_logistics` | Outstanding balance updates |
|
| 228 |
+
|
| 229 |
+
**All operations are atomic** - if any step fails, all changes are rolled back.
|
| 230 |
+
|
| 231 |
+
## 🔍 Monitoring & Debugging
|
| 232 |
+
|
| 233 |
+
### View API Logs
|
| 234 |
+
|
| 235 |
+
```bash
|
| 236 |
+
# View last 20 API requests
|
| 237 |
+
tail -n 20 logs/api.log
|
| 238 |
+
|
| 239 |
+
# Watch logs in real-time
|
| 240 |
+
tail -f logs/api.log
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### Enable Debug Mode
|
| 244 |
+
|
| 245 |
+
In `api/payments/process.php`, change:
|
| 246 |
+
|
| 247 |
+
```php
|
| 248 |
+
error_reporting(E_ALL);
|
| 249 |
+
ini_set('display_errors', 1); // Enable for debugging
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
## 🛠️ Troubleshooting
|
| 253 |
+
|
| 254 |
+
### "Database connection failed"
|
| 255 |
+
- Check `db_config.php` credentials
|
| 256 |
+
- Ensure MySQL is running
|
| 257 |
+
- Verify network connectivity
|
| 258 |
+
|
| 259 |
+
### "Student not found"
|
| 260 |
+
- Verify student exists in `tb_student_registrations`
|
| 261 |
+
- Check `admission_status = 'Active'`
|
| 262 |
+
|
| 263 |
+
### "Teller number not found"
|
| 264 |
+
- Verify teller in `tb_account_bank_statements`
|
| 265 |
+
- Check description format (teller is last word)
|
| 266 |
+
|
| 267 |
+
### "No outstanding fees found"
|
| 268 |
+
- Check `tb_account_receivables` for student
|
| 269 |
+
- Verify fees have outstanding balance
|
| 270 |
+
|
| 271 |
+
### API returns HTML instead of JSON
|
| 272 |
+
- Check for PHP errors in the file
|
| 273 |
+
- Enable error logging to see issues
|
| 274 |
+
- Verify all required files are present
|
| 275 |
+
|
| 276 |
+
## 📝 Implementation Notes
|
| 277 |
+
|
| 278 |
+
### Design Decisions
|
| 279 |
+
|
| 280 |
+
1. **PaymentProcessor Class**: Extracted payment logic into a reusable class to ensure both web and API use identical business rules.
|
| 281 |
+
|
| 282 |
+
2. **Automatic Fee Selection**: The API automatically fetches and allocates to outstanding fees, simplifying the external system's integration.
|
| 283 |
+
|
| 284 |
+
3. **Source Tracking**: Payments are marked with `source = 'api'` for audit purposes.
|
| 285 |
+
|
| 286 |
+
4. **Current Date**: API uses current date for payments (not configurable via API for security).
|
| 287 |
+
|
| 288 |
+
5. **Optional Authentication**: API key auth is optional to allow easy testing and gradual rollout.
|
| 289 |
+
|
| 290 |
+
### Future Enhancements
|
| 291 |
+
|
| 292 |
+
- Rate limiting per API key
|
| 293 |
+
- Webhook notifications for payment status
|
| 294 |
+
- Batch payment processing
|
| 295 |
+
- Payment reversal endpoint
|
| 296 |
+
- Custom payment date (with authorization)
|
| 297 |
+
- IP whitelisting
|
| 298 |
+
|
| 299 |
+
## 🎓 Usage Examples
|
| 300 |
+
|
| 301 |
+
See `api/API_DOCUMENTATION.md` for detailed examples in:
|
| 302 |
+
- cURL
|
| 303 |
+
- PHP
|
| 304 |
+
- JavaScript (Fetch API)
|
| 305 |
+
- Python
|
| 306 |
+
|
| 307 |
+
## 📞 Support
|
| 308 |
+
|
| 309 |
+
For issues or questions:
|
| 310 |
+
1. Check the API documentation
|
| 311 |
+
2. Review the logs in `logs/api.log`
|
| 312 |
+
3. Use the test tool at `api/test.html`
|
| 313 |
+
4. Contact your system administrator
|
| 314 |
+
|
| 315 |
+
## 📜 License
|
| 316 |
+
|
| 317 |
+
Internal use only - ARPS School Management System
|
| 318 |
+
|
| 319 |
+
---
|
| 320 |
+
|
| 321 |
+
**Version:** 1.0
|
| 322 |
+
**Date:** 2026-01-09
|
| 323 |
+
**Author:** Senior PHP Backend Engineer
|
easypay/api/payments/process.php
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* Payment API Endpoint
|
| 4 |
+
*
|
| 5 |
+
* POST /api/payments/process
|
| 6 |
+
*
|
| 7 |
+
* Accepts JSON payload to process student fee payments via external systems.
|
| 8 |
+
* This endpoint uses the same internal payment logic as the web UI.
|
| 9 |
+
*
|
| 10 |
+
* Request Format:
|
| 11 |
+
* {
|
| 12 |
+
* "student_id": "string|integer",
|
| 13 |
+
* "teller_no": "string",
|
| 14 |
+
* "amount": number
|
| 15 |
+
* }
|
| 16 |
+
*
|
| 17 |
+
* Headers Required:
|
| 18 |
+
* - Content-Type: application/json
|
| 19 |
+
* - Authorization: Bearer <API_KEY> (if API_AUTH_ENABLED is true)
|
| 20 |
+
*/
|
| 21 |
+
|
| 22 |
+
// Set error reporting for production
|
| 23 |
+
error_reporting(E_ALL);
|
| 24 |
+
ini_set('display_errors', 0);
|
| 25 |
+
|
| 26 |
+
// Include required files
|
| 27 |
+
require_once __DIR__ . '/../../db_config.php';
|
| 28 |
+
require_once __DIR__ . '/../../config/api_config.php';
|
| 29 |
+
require_once __DIR__ . '/../../includes/ApiValidator.php';
|
| 30 |
+
require_once __DIR__ . '/../../includes/PaymentProcessor.php';
|
| 31 |
+
require_once __DIR__ . '/../../includes/ReceiptGenerator.php';
|
| 32 |
+
|
| 33 |
+
// Set response headers
|
| 34 |
+
header('Content-Type: application/json');
|
| 35 |
+
|
| 36 |
+
// CORS headers (if enabled)
|
| 37 |
+
if (defined('API_CORS_ENABLED') && API_CORS_ENABLED) {
|
| 38 |
+
header('Access-Control-Allow-Origin: ' . API_CORS_ORIGIN);
|
| 39 |
+
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
| 40 |
+
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
| 41 |
+
|
| 42 |
+
// Handle preflight requests
|
| 43 |
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
| 44 |
+
http_response_code(200);
|
| 45 |
+
exit;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Send JSON response
|
| 51 |
+
*/
|
| 52 |
+
function sendResponse($data, $httpCode = 200)
|
| 53 |
+
{
|
| 54 |
+
http_response_code($httpCode);
|
| 55 |
+
echo json_encode($data, JSON_PRETTY_PRINT);
|
| 56 |
+
exit;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Log API request
|
| 61 |
+
*/
|
| 62 |
+
function logApiRequest($data, $response, $httpCode)
|
| 63 |
+
{
|
| 64 |
+
if (!defined('API_LOG_ENABLED') || !API_LOG_ENABLED) {
|
| 65 |
+
return;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
$logDir = dirname(API_LOG_FILE);
|
| 69 |
+
if (!is_dir($logDir)) {
|
| 70 |
+
mkdir($logDir, 0755, true);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
$logEntry = [
|
| 74 |
+
'timestamp' => date('Y-m-d H:i:s'),
|
| 75 |
+
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
| 76 |
+
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
|
| 77 |
+
'request' => $data,
|
| 78 |
+
'response' => $response,
|
| 79 |
+
'http_code' => $httpCode
|
| 80 |
+
];
|
| 81 |
+
|
| 82 |
+
file_put_contents(
|
| 83 |
+
API_LOG_FILE,
|
| 84 |
+
json_encode($logEntry) . PHP_EOL,
|
| 85 |
+
FILE_APPEND
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Main execution
|
| 90 |
+
try {
|
| 91 |
+
// Initialize validator
|
| 92 |
+
$apiKeys = defined('API_AUTH_ENABLED') && API_AUTH_ENABLED ? array_keys(API_KEYS) : [];
|
| 93 |
+
$validator = new ApiValidator($pdo, $apiKeys);
|
| 94 |
+
|
| 95 |
+
// Step 1: Validate request (method, headers, auth)
|
| 96 |
+
$requestValidation = $validator->validateRequest();
|
| 97 |
+
if (!$requestValidation['valid']) {
|
| 98 |
+
$response = [
|
| 99 |
+
'status' => 'error',
|
| 100 |
+
'message' => $requestValidation['error']
|
| 101 |
+
];
|
| 102 |
+
logApiRequest([], $response, $requestValidation['http_code']);
|
| 103 |
+
sendResponse($response, $requestValidation['http_code']);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Step 2: Parse JSON payload
|
| 107 |
+
$payloadValidation = $validator->parseJsonPayload();
|
| 108 |
+
if (!$payloadValidation['valid']) {
|
| 109 |
+
$response = [
|
| 110 |
+
'status' => 'error',
|
| 111 |
+
'message' => $payloadValidation['error']
|
| 112 |
+
];
|
| 113 |
+
logApiRequest([], $response, $payloadValidation['http_code']);
|
| 114 |
+
sendResponse($response, $payloadValidation['http_code']);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
$requestData = $payloadValidation['data'];
|
| 118 |
+
|
| 119 |
+
// Step 3: Validate required fields
|
| 120 |
+
$fieldValidation = $validator->validatePaymentRequest($requestData);
|
| 121 |
+
if (!$fieldValidation['valid']) {
|
| 122 |
+
$response = [
|
| 123 |
+
'status' => 'error',
|
| 124 |
+
'message' => 'Validation failed',
|
| 125 |
+
'errors' => $fieldValidation['errors']
|
| 126 |
+
];
|
| 127 |
+
logApiRequest($requestData, $response, $fieldValidation['http_code']);
|
| 128 |
+
sendResponse($response, $fieldValidation['http_code']);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Sanitize input
|
| 132 |
+
$sanitizedData = $validator->sanitizeInput($requestData);
|
| 133 |
+
|
| 134 |
+
// Step 4: Validate student exists
|
| 135 |
+
$studentValidation = $validator->validateStudentExists($sanitizedData['student_id']);
|
| 136 |
+
if (!$studentValidation['valid']) {
|
| 137 |
+
$response = [
|
| 138 |
+
'status' => 'error',
|
| 139 |
+
'message' => $studentValidation['error']
|
| 140 |
+
];
|
| 141 |
+
logApiRequest($requestData, $response, $studentValidation['http_code']);
|
| 142 |
+
sendResponse($response, $studentValidation['http_code']);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
$student = $studentValidation['student'];
|
| 146 |
+
|
| 147 |
+
// Step 5: Validate teller number
|
| 148 |
+
$tellerValidation = $validator->validateTellerNumber($sanitizedData['teller_no']);
|
| 149 |
+
if (!$tellerValidation['valid']) {
|
| 150 |
+
$response = [
|
| 151 |
+
'status' => 'error',
|
| 152 |
+
'message' => $tellerValidation['error']
|
| 153 |
+
];
|
| 154 |
+
logApiRequest($requestData, $response, $tellerValidation['http_code']);
|
| 155 |
+
sendResponse($response, $tellerValidation['http_code']);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
$teller = $tellerValidation['teller'];
|
| 159 |
+
|
| 160 |
+
// Step 6: Validate amount doesn't exceed unreconciled amount
|
| 161 |
+
if ($sanitizedData['amount'] > $teller['unreconciled_amount']) {
|
| 162 |
+
$response = [
|
| 163 |
+
'status' => 'error',
|
| 164 |
+
'message' => 'Amount exceeds unreconciled amount on teller',
|
| 165 |
+
'errors' => [
|
| 166 |
+
'amount' => 'Requested amount (' . number_format($sanitizedData['amount'], 2) .
|
| 167 |
+
') exceeds available unreconciled amount (' .
|
| 168 |
+
number_format($teller['unreconciled_amount'], 2) . ')'
|
| 169 |
+
]
|
| 170 |
+
];
|
| 171 |
+
logApiRequest($requestData, $response, 400);
|
| 172 |
+
sendResponse($response, 400);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Step 7: Get outstanding fees for the student
|
| 176 |
+
$processor = new PaymentProcessor($pdo);
|
| 177 |
+
$outstandingFees = $processor->getOutstandingFees($student['id']);
|
| 178 |
+
|
| 179 |
+
if (empty($outstandingFees)) {
|
| 180 |
+
$response = [
|
| 181 |
+
'status' => 'error',
|
| 182 |
+
'message' => 'No outstanding fees found for this student'
|
| 183 |
+
];
|
| 184 |
+
logApiRequest($requestData, $response, 400);
|
| 185 |
+
sendResponse($response, 400);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// Step 8: Prepare payment parameters
|
| 189 |
+
// The API will automatically allocate the payment to outstanding fees (oldest first)
|
| 190 |
+
$paymentParams = [
|
| 191 |
+
'student_id' => $student['id'],
|
| 192 |
+
'student_code' => $student['student_code'],
|
| 193 |
+
'selected_fees' => $outstandingFees,
|
| 194 |
+
'teller_number' => $sanitizedData['teller_no'],
|
| 195 |
+
'payment_date' => $sanitizedData['payment_date'], // Use provided date
|
| 196 |
+
'amount_to_use' => $sanitizedData['amount'],
|
| 197 |
+
'source' => 'api' // Mark this payment as API-initiated
|
| 198 |
+
];
|
| 199 |
+
|
| 200 |
+
// Step 9: Process payment using the internal payment logic
|
| 201 |
+
$result = $processor->processPayment($paymentParams);
|
| 202 |
+
|
| 203 |
+
if ($result['success']) {
|
| 204 |
+
// Generate receipt
|
| 205 |
+
$result['data']['student_name'] = $student['full_name'];
|
| 206 |
+
$generator = new ReceiptGenerator();
|
| 207 |
+
$receiptBase64 = $generator->generateBase64($result['data']);
|
| 208 |
+
|
| 209 |
+
$response = [
|
| 210 |
+
'status' => 'success',
|
| 211 |
+
'message' => $result['message'],
|
| 212 |
+
'data' => [
|
| 213 |
+
'student_id' => $result['data']['student_id'],
|
| 214 |
+
'teller_no' => $result['data']['teller_no'],
|
| 215 |
+
'amount' => $result['data']['total_paid'],
|
| 216 |
+
'payment_id' => $result['data']['transaction_id'],
|
| 217 |
+
'receipt_no' => $result['data']['receipt_no'],
|
| 218 |
+
'payment_date' => $result['data']['payment_date'],
|
| 219 |
+
'receipt_image' => $receiptBase64,
|
| 220 |
+
'fees_settled' => array_map(function ($allocation) {
|
| 221 |
+
return [
|
| 222 |
+
'fee_description' => $allocation['description'],
|
| 223 |
+
'session' => $allocation['academic_session'],
|
| 224 |
+
'term' => $allocation['term_of_session'],
|
| 225 |
+
'amount' => $allocation['amount']
|
| 226 |
+
];
|
| 227 |
+
}, $result['data']['allocations'])
|
| 228 |
+
]
|
| 229 |
+
];
|
| 230 |
+
logApiRequest($requestData, $response, 201);
|
| 231 |
+
sendResponse($response, 201);
|
| 232 |
+
} else {
|
| 233 |
+
$response = [
|
| 234 |
+
'status' => 'error',
|
| 235 |
+
'message' => $result['message']
|
| 236 |
+
];
|
| 237 |
+
logApiRequest($requestData, $response, 500);
|
| 238 |
+
sendResponse($response, 500);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
} catch (Exception $e) {
|
| 242 |
+
$response = [
|
| 243 |
+
'status' => 'error',
|
| 244 |
+
'message' => 'Internal server error',
|
| 245 |
+
'error_detail' => $e->getMessage() // Remove in production
|
| 246 |
+
];
|
| 247 |
+
logApiRequest($requestData ?? [], $response, 500);
|
| 248 |
+
sendResponse($response, 500);
|
| 249 |
+
}
|
easypay/api/test.html
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Payment API Tester</title>
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 17 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.container {
|
| 23 |
+
max-width: 900px;
|
| 24 |
+
margin: 0 auto;
|
| 25 |
+
background: white;
|
| 26 |
+
border-radius: 12px;
|
| 27 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
| 28 |
+
padding: 40px;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
h1 {
|
| 32 |
+
color: #333;
|
| 33 |
+
margin-bottom: 10px;
|
| 34 |
+
text-align: center;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.subtitle {
|
| 38 |
+
text-align: center;
|
| 39 |
+
color: #666;
|
| 40 |
+
margin-bottom: 30px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.form-group {
|
| 44 |
+
margin-bottom: 20px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
label {
|
| 48 |
+
display: block;
|
| 49 |
+
margin-bottom: 8px;
|
| 50 |
+
color: #333;
|
| 51 |
+
font-weight: 600;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
input[type="text"],
|
| 55 |
+
input[type="number"] {
|
| 56 |
+
width: 100%;
|
| 57 |
+
padding: 12px;
|
| 58 |
+
border: 2px solid #ddd;
|
| 59 |
+
border-radius: 6px;
|
| 60 |
+
font-size: 16px;
|
| 61 |
+
transition: border-color 0.3s;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
input[type="text"]:focus,
|
| 65 |
+
input[type="number"]:focus {
|
| 66 |
+
outline: none;
|
| 67 |
+
border-color: #667eea;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.btn {
|
| 71 |
+
width: 100%;
|
| 72 |
+
padding: 14px;
|
| 73 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 74 |
+
color: white;
|
| 75 |
+
border: none;
|
| 76 |
+
border-radius: 6px;
|
| 77 |
+
font-size: 16px;
|
| 78 |
+
font-weight: 600;
|
| 79 |
+
cursor: pointer;
|
| 80 |
+
transition: transform 0.3s;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.btn:hover {
|
| 84 |
+
transform: translateY(-2px);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.btn:disabled {
|
| 88 |
+
opacity: 0.6;
|
| 89 |
+
cursor: not-allowed;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.response-section {
|
| 93 |
+
margin-top: 30px;
|
| 94 |
+
display: none;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.response-section.show {
|
| 98 |
+
display: block;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.response-box {
|
| 102 |
+
background: #f8f9fa;
|
| 103 |
+
border-radius: 8px;
|
| 104 |
+
padding: 20px;
|
| 105 |
+
margin-top: 15px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.response-box h3 {
|
| 109 |
+
margin-bottom: 15px;
|
| 110 |
+
color: #333;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.response-box pre {
|
| 114 |
+
background: #2d2d2d;
|
| 115 |
+
color: #f8f8f2;
|
| 116 |
+
padding: 15px;
|
| 117 |
+
border-radius: 6px;
|
| 118 |
+
overflow-x: auto;
|
| 119 |
+
font-size: 14px;
|
| 120 |
+
line-height: 1.5;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.status-badge {
|
| 124 |
+
display: inline-block;
|
| 125 |
+
padding: 6px 12px;
|
| 126 |
+
border-radius: 4px;
|
| 127 |
+
font-weight: 600;
|
| 128 |
+
margin-bottom: 10px;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.status-success {
|
| 132 |
+
background: #28a745;
|
| 133 |
+
color: white;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.status-error {
|
| 137 |
+
background: #dc3545;
|
| 138 |
+
color: white;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.info-box {
|
| 142 |
+
background: #e7f3ff;
|
| 143 |
+
border-left: 4px solid #2196F3;
|
| 144 |
+
padding: 15px;
|
| 145 |
+
margin-bottom: 20px;
|
| 146 |
+
border-radius: 4px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.info-box p {
|
| 150 |
+
margin: 5px 0;
|
| 151 |
+
color: #333;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.checkbox-group {
|
| 155 |
+
margin-bottom: 20px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.checkbox-group label {
|
| 159 |
+
display: flex;
|
| 160 |
+
align-items: center;
|
| 161 |
+
font-weight: normal;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.checkbox-group input[type="checkbox"] {
|
| 165 |
+
margin-right: 10px;
|
| 166 |
+
width: 18px;
|
| 167 |
+
height: 18px;
|
| 168 |
+
}
|
| 169 |
+
</style>
|
| 170 |
+
</head>
|
| 171 |
+
|
| 172 |
+
<body>
|
| 173 |
+
<div class="container">
|
| 174 |
+
<h1>Payment API Tester</h1>
|
| 175 |
+
<p class="subtitle">Test the Payment Processing API endpoint</p>
|
| 176 |
+
|
| 177 |
+
<div class="info-box">
|
| 178 |
+
<p><strong>API Endpoint:</strong> <code id="apiEndpoint"></code></p>
|
| 179 |
+
<p><strong>Method:</strong> POST</p>
|
| 180 |
+
<p><strong>Content-Type:</strong> application/json</p>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<form id="paymentForm">
|
| 184 |
+
<div class="form-group">
|
| 185 |
+
<label for="studentId">Student ID *</label>
|
| 186 |
+
<input type="text" id="studentId" name="student_id" required placeholder="e.g., 000001234567890123">
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div class="form-group">
|
| 190 |
+
<label for="tellerNo">Teller Number *</label>
|
| 191 |
+
<input type="text" id="tellerNo" name="teller_no" required placeholder="e.g., 1234567890">
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<div class="form-group">
|
| 195 |
+
<label for="amount">Amount *</label>
|
| 196 |
+
<input type="number" id="amount" name="amount" step="0.01" min="0.01" required
|
| 197 |
+
placeholder="e.g., 50000">
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<div class="checkbox-group">
|
| 201 |
+
<label>
|
| 202 |
+
<input type="checkbox" id="useAuth" name="use_auth">
|
| 203 |
+
Use API Key Authentication
|
| 204 |
+
</label>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<div class="form-group" id="apiKeyGroup" style="display: none;">
|
| 208 |
+
<label for="apiKey">API Key</label>
|
| 209 |
+
<input type="text" id="apiKey" name="api_key" placeholder="Enter your API key">
|
| 210 |
+
</div>
|
| 211 |
+
|
| 212 |
+
<button type="submit" class="btn" id="submitBtn">Send Request</button>
|
| 213 |
+
</form>
|
| 214 |
+
|
| 215 |
+
<div class="response-section" id="responseSection">
|
| 216 |
+
<div class="response-box">
|
| 217 |
+
<h3>Response</h3>
|
| 218 |
+
<div id="statusBadge"></div>
|
| 219 |
+
<pre id="responseBody"></pre>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<div class="response-box">
|
| 223 |
+
<h3>Request Details</h3>
|
| 224 |
+
<pre id="requestDetails"></pre>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
<script>
|
| 230 |
+
// Set API endpoint
|
| 231 |
+
const apiEndpoint = window.location.origin + '/easypay/api/payments/process';
|
| 232 |
+
document.getElementById('apiEndpoint').textContent = apiEndpoint;
|
| 233 |
+
|
| 234 |
+
// Toggle API key field
|
| 235 |
+
document.getElementById('useAuth').addEventListener('change', function () {
|
| 236 |
+
document.getElementById('apiKeyGroup').style.display = this.checked ? 'block' : 'none';
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
// Handle form submission
|
| 240 |
+
document.getElementById('paymentForm').addEventListener('submit', async function (e) {
|
| 241 |
+
e.preventDefault();
|
| 242 |
+
|
| 243 |
+
const submitBtn = document.getElementById('submitBtn');
|
| 244 |
+
submitBtn.disabled = true;
|
| 245 |
+
submitBtn.textContent = 'Sending...';
|
| 246 |
+
|
| 247 |
+
// Prepare request data
|
| 248 |
+
const requestData = {
|
| 249 |
+
student_id: document.getElementById('studentId').value,
|
| 250 |
+
teller_no: document.getElementById('tellerNo').value,
|
| 251 |
+
amount: parseFloat(document.getElementById('amount').value)
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
// Prepare headers
|
| 255 |
+
const headers = {
|
| 256 |
+
'Content-Type': 'application/json'
|
| 257 |
+
};
|
| 258 |
+
|
| 259 |
+
if (document.getElementById('useAuth').checked) {
|
| 260 |
+
const apiKey = document.getElementById('apiKey').value;
|
| 261 |
+
if (apiKey) {
|
| 262 |
+
headers['Authorization'] = 'Bearer ' + apiKey;
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
try {
|
| 267 |
+
// Send request
|
| 268 |
+
const response = await fetch(apiEndpoint, {
|
| 269 |
+
method: 'POST',
|
| 270 |
+
headers: headers,
|
| 271 |
+
body: JSON.stringify(requestData)
|
| 272 |
+
});
|
| 273 |
+
|
| 274 |
+
const responseData = await response.json();
|
| 275 |
+
|
| 276 |
+
// Display response
|
| 277 |
+
displayResponse(response.status, responseData, requestData, headers);
|
| 278 |
+
|
| 279 |
+
} catch (error) {
|
| 280 |
+
displayError(error.message, requestData, headers);
|
| 281 |
+
} finally {
|
| 282 |
+
submitBtn.disabled = false;
|
| 283 |
+
submitBtn.textContent = 'Send Request';
|
| 284 |
+
}
|
| 285 |
+
});
|
| 286 |
+
|
| 287 |
+
function displayResponse(status, data, request, headers) {
|
| 288 |
+
const responseSection = document.getElementById('responseSection');
|
| 289 |
+
const statusBadge = document.getElementById('statusBadge');
|
| 290 |
+
const responseBody = document.getElementById('responseBody');
|
| 291 |
+
const requestDetails = document.getElementById('requestDetails');
|
| 292 |
+
|
| 293 |
+
// Show response section
|
| 294 |
+
responseSection.classList.add('show');
|
| 295 |
+
|
| 296 |
+
// Set status badge
|
| 297 |
+
const isSuccess = status >= 200 && status < 300;
|
| 298 |
+
statusBadge.className = 'status-badge ' + (isSuccess ? 'status-success' : 'status-error');
|
| 299 |
+
statusBadge.textContent = 'HTTP ' + status + ' - ' + (isSuccess ? 'Success' : 'Error');
|
| 300 |
+
|
| 301 |
+
// Display response body
|
| 302 |
+
responseBody.textContent = JSON.stringify(data, null, 2);
|
| 303 |
+
|
| 304 |
+
// Display request details
|
| 305 |
+
const requestInfo = {
|
| 306 |
+
url: apiEndpoint,
|
| 307 |
+
method: 'POST',
|
| 308 |
+
headers: headers,
|
| 309 |
+
body: request
|
| 310 |
+
};
|
| 311 |
+
requestDetails.textContent = JSON.stringify(requestInfo, null, 2);
|
| 312 |
+
|
| 313 |
+
// Scroll to response
|
| 314 |
+
responseSection.scrollIntoView({ behavior: 'smooth' });
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function displayError(message, request, headers) {
|
| 318 |
+
const responseSection = document.getElementById('responseSection');
|
| 319 |
+
const statusBadge = document.getElementById('statusBadge');
|
| 320 |
+
const responseBody = document.getElementById('responseBody');
|
| 321 |
+
const requestDetails = document.getElementById('requestDetails');
|
| 322 |
+
|
| 323 |
+
// Show response section
|
| 324 |
+
responseSection.classList.add('show');
|
| 325 |
+
|
| 326 |
+
// Set status badge
|
| 327 |
+
statusBadge.className = 'status-badge status-error';
|
| 328 |
+
statusBadge.textContent = 'Request Failed';
|
| 329 |
+
|
| 330 |
+
// Display error
|
| 331 |
+
responseBody.textContent = JSON.stringify({
|
| 332 |
+
error: message
|
| 333 |
+
}, null, 2);
|
| 334 |
+
|
| 335 |
+
// Display request details
|
| 336 |
+
const requestInfo = {
|
| 337 |
+
url: apiEndpoint,
|
| 338 |
+
method: 'POST',
|
| 339 |
+
headers: headers,
|
| 340 |
+
body: request
|
| 341 |
+
};
|
| 342 |
+
requestDetails.textContent = JSON.stringify(requestInfo, null, 2);
|
| 343 |
+
|
| 344 |
+
// Scroll to response
|
| 345 |
+
responseSection.scrollIntoView({ behavior: 'smooth' });
|
| 346 |
+
}
|
| 347 |
+
</script>
|
| 348 |
+
</body>
|
| 349 |
+
|
| 350 |
+
</html>
|
easypay/assets/logo.png
ADDED
|
easypay/config/api_config.php
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* API Configuration
|
| 4 |
+
*
|
| 5 |
+
* Configuration settings for the Payment API
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
// API Keys for authentication
|
| 9 |
+
// Add your API keys here. Leave empty to disable API key authentication.
|
| 10 |
+
// Example: ['key1' => 'My App', 'key2' => 'External System']
|
| 11 |
+
define('API_KEYS', [
|
| 12 |
+
// 'your-secret-api-key-here' => 'System Name',
|
| 13 |
+
// Add more API keys as needed
|
| 14 |
+
]);
|
| 15 |
+
|
| 16 |
+
// Enable/Disable API key authentication
|
| 17 |
+
// Set to false for testing, true for production
|
| 18 |
+
define('API_AUTH_ENABLED', false);
|
| 19 |
+
|
| 20 |
+
// API logging
|
| 21 |
+
define('API_LOG_ENABLED', true);
|
| 22 |
+
define('API_LOG_FILE', __DIR__ . '/../logs/api.log');
|
| 23 |
+
|
| 24 |
+
// CORS settings (if needed for external systems)
|
| 25 |
+
define('API_CORS_ENABLED', false);
|
| 26 |
+
define('API_CORS_ORIGIN', '*'); // Use specific domain in production
|
| 27 |
+
|
| 28 |
+
// Rate limiting (future implementation)
|
| 29 |
+
define('API_RATE_LIMIT_ENABLED', false);
|
| 30 |
+
define('API_RATE_LIMIT_REQUESTS', 100); // requests per minute
|
easypay/db_config.php
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* Database Configuration
|
| 4 |
+
* PDO connection for one_arps_aci database
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
// Database credentials
|
| 8 |
+
define('DB_HOST', 'o2.service.oyster.cloud76.cc:3306');
|
| 9 |
+
define('DB_NAME', 'one_arps_aci');
|
| 10 |
+
define('DB_USER', 'root');
|
| 11 |
+
define('DB_PASS', 'password');
|
| 12 |
+
define('DB_CHARSET', 'utf8mb4');
|
| 13 |
+
|
| 14 |
+
// PDO options - D55-system-2-beasts-jungle-sky-birth cs_admin
|
| 15 |
+
$options = [
|
| 16 |
+
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
| 17 |
+
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
| 18 |
+
PDO::ATTR_EMULATE_PREPARES => false,
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
// Create PDO instance
|
| 22 |
+
try {
|
| 23 |
+
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
|
| 24 |
+
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
| 25 |
+
} catch (PDOException $e) {
|
| 26 |
+
die("Database connection failed: " . $e->getMessage());
|
| 27 |
+
}
|
easypay/download_receipt.php
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
require_once 'db_config.php';
|
| 3 |
+
require_once 'includes/ReceiptGenerator.php';
|
| 4 |
+
|
| 5 |
+
// Validate input
|
| 6 |
+
$receiptNo = $_GET['receipt_no'] ?? '';
|
| 7 |
+
if (empty($receiptNo)) {
|
| 8 |
+
die("Receipt number is required.");
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
try {
|
| 12 |
+
// 1. Fetch main payment records grouped by receipt
|
| 13 |
+
// We join with student registrations to get name info
|
| 14 |
+
// We join with school fees to get description
|
| 15 |
+
$sql = "SELECT pr.*,
|
| 16 |
+
sf.description as fee_description,
|
| 17 |
+
sr.last_name, sr.first_name, sr.other_name, sr.student_code
|
| 18 |
+
FROM tb_account_payment_registers pr
|
| 19 |
+
JOIN tb_account_school_fees sf ON pr.fee_id = sf.id
|
| 20 |
+
JOIN tb_student_registrations sr ON pr.student_id = sr.id
|
| 21 |
+
WHERE pr.receipt_no = :receipt_no";
|
| 22 |
+
|
| 23 |
+
$stmt = $pdo->prepare($sql);
|
| 24 |
+
$stmt->execute(['receipt_no' => $receiptNo]);
|
| 25 |
+
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
| 26 |
+
|
| 27 |
+
if (empty($rows)) {
|
| 28 |
+
die("Receipt not found.");
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// 2. Prepare data structure for generator
|
| 32 |
+
$firstRow = $rows[0];
|
| 33 |
+
$data = [
|
| 34 |
+
'receipt_no' => $receiptNo,
|
| 35 |
+
'student_name' => trim($firstRow['last_name'] . ' ' . $firstRow['first_name'] . ' ' . ($firstRow['other_name'] ?? '')),
|
| 36 |
+
'student_code' => $firstRow['student_code'],
|
| 37 |
+
'payment_date' => $firstRow['payment_date'],
|
| 38 |
+
'total_paid' => 0,
|
| 39 |
+
'allocations' => []
|
| 40 |
+
];
|
| 41 |
+
|
| 42 |
+
// 3. Process each fee allocation to get billed/balance context
|
| 43 |
+
foreach ($rows as $row) {
|
| 44 |
+
$amountPaidHere = floatval($row['amount_paid']);
|
| 45 |
+
$data['total_paid'] += $amountPaidHere;
|
| 46 |
+
|
| 47 |
+
// Fetch Billed Amount (Actual Value) from Receivables
|
| 48 |
+
$sqlBilled = "SELECT actual_value
|
| 49 |
+
FROM tb_account_receivables
|
| 50 |
+
WHERE student_id = :sid
|
| 51 |
+
AND fee_id = :fid
|
| 52 |
+
AND academic_session = :as
|
| 53 |
+
AND term_of_session = :ts
|
| 54 |
+
LIMIT 1";
|
| 55 |
+
$stmtBilled = $pdo->prepare($sqlBilled);
|
| 56 |
+
$stmtBilled->execute([
|
| 57 |
+
'sid' => $row['student_id'],
|
| 58 |
+
'fid' => $row['fee_id'],
|
| 59 |
+
'as' => $row['academic_session'],
|
| 60 |
+
'ts' => $row['term_of_session']
|
| 61 |
+
]);
|
| 62 |
+
$billedRes = $stmtBilled->fetch(PDO::FETCH_ASSOC);
|
| 63 |
+
$amountBilled = floatval($billedRes['actual_value'] ?? 0);
|
| 64 |
+
|
| 65 |
+
// Fetch Total Paid To Date (inclusive of this payment's date)
|
| 66 |
+
// We sum all payments for this fee/student/session/term up to this date
|
| 67 |
+
// Note: This matches the state "at the time of receipt" roughly
|
| 68 |
+
$sqlPaid = "SELECT SUM(amount_paid) as total_paid
|
| 69 |
+
FROM tb_account_payment_registers
|
| 70 |
+
WHERE student_id = :sid
|
| 71 |
+
AND fee_id = :fid
|
| 72 |
+
AND academic_session = :as
|
| 73 |
+
AND term_of_session = :ts
|
| 74 |
+
AND payment_date <= :pd";
|
| 75 |
+
$stmtPaid = $pdo->prepare($sqlPaid);
|
| 76 |
+
$stmtPaid->execute([
|
| 77 |
+
'sid' => $row['student_id'],
|
| 78 |
+
'fid' => $row['fee_id'],
|
| 79 |
+
'as' => $row['academic_session'],
|
| 80 |
+
'ts' => $row['term_of_session'],
|
| 81 |
+
'pd' => $row['payment_date']
|
| 82 |
+
]);
|
| 83 |
+
$paidRes = $stmtPaid->fetch(PDO::FETCH_ASSOC);
|
| 84 |
+
$totalPaidToDate = floatval($paidRes['total_paid'] ?? 0);
|
| 85 |
+
|
| 86 |
+
$balance = $amountBilled - $totalPaidToDate;
|
| 87 |
+
|
| 88 |
+
$data['allocations'][] = [
|
| 89 |
+
'description' => $row['fee_description'],
|
| 90 |
+
'academic_session' => $row['academic_session'],
|
| 91 |
+
'term_of_session' => $row['term_of_session'],
|
| 92 |
+
'amount' => $amountPaidHere,
|
| 93 |
+
'amount_billed' => $amountBilled,
|
| 94 |
+
'total_paid_to_date' => $totalPaidToDate,
|
| 95 |
+
'balance' => $balance
|
| 96 |
+
];
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// 4. Generate Image
|
| 100 |
+
$generator = new ReceiptGenerator();
|
| 101 |
+
$imageData = $generator->generate($data);
|
| 102 |
+
|
| 103 |
+
// 5. Output
|
| 104 |
+
header('Content-Type: image/png');
|
| 105 |
+
header('Content-Disposition: attachment; filename="receipt_' . $receiptNo . '.png"');
|
| 106 |
+
header('Content-Length: ' . strlen($imageData));
|
| 107 |
+
echo $imageData;
|
| 108 |
+
|
| 109 |
+
} catch (Exception $e) {
|
| 110 |
+
die("Error generating receipt: " . $e->getMessage());
|
| 111 |
+
}
|
easypay/includes/ApiValidator.php
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* ApiValidator Class
|
| 4 |
+
*
|
| 5 |
+
* Handles API request validation, authentication, and input sanitization
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
class ApiValidator
|
| 9 |
+
{
|
| 10 |
+
private $pdo;
|
| 11 |
+
private $apiKeys;
|
| 12 |
+
|
| 13 |
+
public function __construct($pdo, $apiKeys = [])
|
| 14 |
+
{
|
| 15 |
+
$this->pdo = $pdo;
|
| 16 |
+
$this->apiKeys = $apiKeys;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* Validate API request
|
| 21 |
+
* Checks Content-Type and optionally validates API key
|
| 22 |
+
*
|
| 23 |
+
* @return array Validation result
|
| 24 |
+
*/
|
| 25 |
+
public function validateRequest()
|
| 26 |
+
{
|
| 27 |
+
// Check request method
|
| 28 |
+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
| 29 |
+
return [
|
| 30 |
+
'valid' => false,
|
| 31 |
+
'error' => 'Invalid request method. Only POST is allowed.',
|
| 32 |
+
'http_code' => 405
|
| 33 |
+
];
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Check Content-Type header
|
| 37 |
+
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
| 38 |
+
if (stripos($contentType, 'application/json') === false) {
|
| 39 |
+
return [
|
| 40 |
+
'valid' => false,
|
| 41 |
+
'error' => 'Invalid Content-Type. Expected application/json',
|
| 42 |
+
'http_code' => 400
|
| 43 |
+
];
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Validate API key if configured
|
| 47 |
+
if (!empty($this->apiKeys)) {
|
| 48 |
+
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
| 49 |
+
|
| 50 |
+
if (empty($authHeader)) {
|
| 51 |
+
return [
|
| 52 |
+
'valid' => false,
|
| 53 |
+
'error' => 'Missing Authorization header',
|
| 54 |
+
'http_code' => 401
|
| 55 |
+
];
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Extract Bearer token
|
| 59 |
+
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
|
| 60 |
+
return [
|
| 61 |
+
'valid' => false,
|
| 62 |
+
'error' => 'Invalid Authorization header format. Expected: Bearer <API_KEY>',
|
| 63 |
+
'http_code' => 401
|
| 64 |
+
];
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
$apiKey = $matches[1];
|
| 68 |
+
|
| 69 |
+
if (!in_array($apiKey, $this->apiKeys)) {
|
| 70 |
+
return [
|
| 71 |
+
'valid' => false,
|
| 72 |
+
'error' => 'Invalid API key',
|
| 73 |
+
'http_code' => 401
|
| 74 |
+
];
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
return ['valid' => true];
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/**
|
| 82 |
+
* Parse and validate JSON payload
|
| 83 |
+
*
|
| 84 |
+
* @return array Parsed data or error
|
| 85 |
+
*/
|
| 86 |
+
public function parseJsonPayload()
|
| 87 |
+
{
|
| 88 |
+
$rawInput = file_get_contents('php://input');
|
| 89 |
+
|
| 90 |
+
if (empty($rawInput)) {
|
| 91 |
+
return [
|
| 92 |
+
'valid' => false,
|
| 93 |
+
'error' => 'Empty request body',
|
| 94 |
+
'http_code' => 400
|
| 95 |
+
];
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
$data = json_decode($rawInput, true);
|
| 99 |
+
|
| 100 |
+
if (json_last_error() !== JSON_ERROR_NONE) {
|
| 101 |
+
return [
|
| 102 |
+
'valid' => false,
|
| 103 |
+
'error' => 'Invalid JSON: ' . json_last_error_msg(),
|
| 104 |
+
'http_code' => 400
|
| 105 |
+
];
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return [
|
| 109 |
+
'valid' => true,
|
| 110 |
+
'data' => $data
|
| 111 |
+
];
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Validate payment request fields
|
| 116 |
+
*
|
| 117 |
+
* @param array $data Request data
|
| 118 |
+
* @return array Validation result with errors
|
| 119 |
+
*/
|
| 120 |
+
public function validatePaymentRequest($data)
|
| 121 |
+
{
|
| 122 |
+
$errors = [];
|
| 123 |
+
|
| 124 |
+
// Validate student_id
|
| 125 |
+
if (empty($data['student_id'])) {
|
| 126 |
+
$errors['student_id'] = 'Student ID is required';
|
| 127 |
+
} elseif (!is_string($data['student_id']) && !is_numeric($data['student_id'])) {
|
| 128 |
+
$errors['student_id'] = 'Student ID must be a string or number';
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Validate teller_no
|
| 132 |
+
if (empty($data['teller_no'])) {
|
| 133 |
+
$errors['teller_no'] = 'Teller number is required';
|
| 134 |
+
} elseif (!is_string($data['teller_no']) && !is_numeric($data['teller_no'])) {
|
| 135 |
+
$errors['teller_no'] = 'Teller number must be a string or number';
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Validate amount
|
| 139 |
+
if (!isset($data['amount'])) {
|
| 140 |
+
$errors['amount'] = 'Amount is required';
|
| 141 |
+
} elseif (!is_numeric($data['amount'])) {
|
| 142 |
+
$errors['amount'] = 'Amount must be a number';
|
| 143 |
+
} elseif (floatval($data['amount']) <= 0) {
|
| 144 |
+
$errors['amount'] = 'Amount must be greater than zero';
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Validate payment_date
|
| 148 |
+
if (empty($data['payment_date'])) {
|
| 149 |
+
$errors['payment_date'] = 'Payment date is required';
|
| 150 |
+
} elseif (!strtotime($data['payment_date'])) {
|
| 151 |
+
$errors['payment_date'] = 'Invalid payment date format';
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
if (!empty($errors)) {
|
| 155 |
+
return [
|
| 156 |
+
'valid' => false,
|
| 157 |
+
'errors' => $errors,
|
| 158 |
+
'http_code' => 400
|
| 159 |
+
];
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
return ['valid' => true];
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/**
|
| 166 |
+
* Validate student exists in database
|
| 167 |
+
*
|
| 168 |
+
* @param string $studentId Student ID
|
| 169 |
+
* @return array Validation result with student data
|
| 170 |
+
*/
|
| 171 |
+
public function validateStudentExists($studentId)
|
| 172 |
+
{
|
| 173 |
+
$sql = "SELECT id, student_code,
|
| 174 |
+
CONCAT(last_name, ' ', first_name, ' ', COALESCE(other_name, '')) AS full_name,
|
| 175 |
+
admission_status
|
| 176 |
+
FROM tb_student_registrations
|
| 177 |
+
WHERE id = :student_id";
|
| 178 |
+
|
| 179 |
+
$stmt = $this->pdo->prepare($sql);
|
| 180 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 181 |
+
$student = $stmt->fetch();
|
| 182 |
+
|
| 183 |
+
if (!$student) {
|
| 184 |
+
return [
|
| 185 |
+
'valid' => false,
|
| 186 |
+
'error' => 'Student not found',
|
| 187 |
+
'http_code' => 404
|
| 188 |
+
];
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
if ($student['admission_status'] !== 'Active') {
|
| 192 |
+
return [
|
| 193 |
+
'valid' => false,
|
| 194 |
+
'error' => 'Student is not active',
|
| 195 |
+
'http_code' => 400
|
| 196 |
+
];
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
return [
|
| 200 |
+
'valid' => true,
|
| 201 |
+
'student' => $student
|
| 202 |
+
];
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/**
|
| 206 |
+
* Validate teller number exists and has unreconciled amount
|
| 207 |
+
*
|
| 208 |
+
* @param string $tellerNo Teller number
|
| 209 |
+
* @return array Validation result with teller data
|
| 210 |
+
*/
|
| 211 |
+
public function validateTellerNumber($tellerNo)
|
| 212 |
+
{
|
| 213 |
+
$sql = "SELECT
|
| 214 |
+
bs.id,
|
| 215 |
+
bs.description,
|
| 216 |
+
bs.amount_paid,
|
| 217 |
+
bs.payment_date,
|
| 218 |
+
COALESCE(fp.total_registered_fee, 0.00) AS registered_amount,
|
| 219 |
+
(bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount
|
| 220 |
+
FROM tb_account_bank_statements bs
|
| 221 |
+
LEFT JOIN (
|
| 222 |
+
SELECT teller_no, SUM(amount_paid) AS total_registered_fee
|
| 223 |
+
FROM tb_account_school_fee_payments
|
| 224 |
+
GROUP BY teller_no
|
| 225 |
+
) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no
|
| 226 |
+
WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number
|
| 227 |
+
LIMIT 1";
|
| 228 |
+
|
| 229 |
+
$stmt = $this->pdo->prepare($sql);
|
| 230 |
+
$stmt->execute(['teller_number' => $tellerNo]);
|
| 231 |
+
$teller = $stmt->fetch();
|
| 232 |
+
|
| 233 |
+
if (!$teller) {
|
| 234 |
+
return [
|
| 235 |
+
'valid' => false,
|
| 236 |
+
'error' => 'Teller number not found',
|
| 237 |
+
'http_code' => 404
|
| 238 |
+
];
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
if ($teller['unreconciled_amount'] <= 0) {
|
| 242 |
+
return [
|
| 243 |
+
'valid' => false,
|
| 244 |
+
'error' => 'Teller number has no unreconciled amount',
|
| 245 |
+
'http_code' => 400
|
| 246 |
+
];
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return [
|
| 250 |
+
'valid' => true,
|
| 251 |
+
'teller' => $teller
|
| 252 |
+
];
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/**
|
| 256 |
+
* Check for duplicate teller number usage
|
| 257 |
+
* Prevents the same teller number from being used multiple times
|
| 258 |
+
*
|
| 259 |
+
* @param string $tellerNo Teller number
|
| 260 |
+
* @param string $studentId Student ID (optional, for better error message)
|
| 261 |
+
* @return array Validation result
|
| 262 |
+
*/
|
| 263 |
+
public function checkTellerDuplicate($tellerNo, $studentId = null)
|
| 264 |
+
{
|
| 265 |
+
// Note: The system allows the same teller to be used for different students
|
| 266 |
+
// This check is to ensure the teller has available unreconciled amount
|
| 267 |
+
// The actual duplicate check is handled by the validateTellerNumber method
|
| 268 |
+
|
| 269 |
+
return ['valid' => true];
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/**
|
| 273 |
+
* Sanitize input data
|
| 274 |
+
*
|
| 275 |
+
* @param array $data Input data
|
| 276 |
+
* @return array Sanitized data
|
| 277 |
+
*/
|
| 278 |
+
public function sanitizeInput($data)
|
| 279 |
+
{
|
| 280 |
+
return [
|
| 281 |
+
'student_id' => trim($data['student_id'] ?? ''),
|
| 282 |
+
'teller_no' => trim($data['teller_no'] ?? ''),
|
| 283 |
+
'amount' => floatval($data['amount'] ?? 0),
|
| 284 |
+
'payment_date' => trim($data['payment_date'] ?? '')
|
| 285 |
+
];
|
| 286 |
+
}
|
| 287 |
+
}
|
easypay/includes/PaymentProcessor.php
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* PaymentProcessor Class
|
| 4 |
+
*
|
| 5 |
+
* Encapsulates the core payment processing logic.
|
| 6 |
+
* This is the single source of truth for payment operations.
|
| 7 |
+
* Used by both the web UI and the API.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
class PaymentProcessor
|
| 11 |
+
{
|
| 12 |
+
private $pdo;
|
| 13 |
+
|
| 14 |
+
// Constants for payment processing
|
| 15 |
+
private const CREDIT_BANK_ID = '000001373634585148';
|
| 16 |
+
private const DEBIT_BANK_ID = '514297805530965017';
|
| 17 |
+
private const PAYMODE_ID = '000001373901891416';
|
| 18 |
+
private const RECIPIENT_ID = 'SS0011441283890434';
|
| 19 |
+
private const PAYMENT_BY = 'SS0011441283890434';
|
| 20 |
+
private const CREATED_BY = 'SS0011441283890434';
|
| 21 |
+
private const CREATED_AS = 'school';
|
| 22 |
+
private const PAYMENT_STATUS = 'Approved';
|
| 23 |
+
private const PAYMODE_CATEGORY = 'BANK';
|
| 24 |
+
|
| 25 |
+
public function __construct($pdo)
|
| 26 |
+
{
|
| 27 |
+
$this->pdo = $pdo;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Process a payment transaction
|
| 32 |
+
*
|
| 33 |
+
* @param array $params Payment parameters
|
| 34 |
+
* @return array Result with success status, message, and payment details
|
| 35 |
+
* @throws Exception on validation or processing errors
|
| 36 |
+
*/
|
| 37 |
+
public function processPayment(array $params)
|
| 38 |
+
{
|
| 39 |
+
// Extract and validate parameters
|
| 40 |
+
$studentId = $params['student_id'] ?? '';
|
| 41 |
+
$studentCode = $params['student_code'] ?? '';
|
| 42 |
+
$selectedFees = $params['selected_fees'] ?? [];
|
| 43 |
+
$tellerNumber = $params['teller_number'] ?? '';
|
| 44 |
+
$paymentDate = $params['payment_date'] ?? '';
|
| 45 |
+
$amountToUse = floatval($params['amount_to_use'] ?? 0);
|
| 46 |
+
$source = $params['source'] ?? 'web'; // Track payment source (web/api)
|
| 47 |
+
|
| 48 |
+
// Validate required fields
|
| 49 |
+
if (
|
| 50 |
+
empty($studentId) || empty($studentCode) || empty($selectedFees) ||
|
| 51 |
+
empty($tellerNumber) || empty($paymentDate) || $amountToUse <= 0
|
| 52 |
+
) {
|
| 53 |
+
throw new Exception('Missing required fields');
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Validate selected fees is an array
|
| 57 |
+
if (!is_array($selectedFees) || count($selectedFees) === 0) {
|
| 58 |
+
throw new Exception('No fees selected');
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Sort fees by academic_session ASC, term_of_session ASC (oldest to newest)
|
| 62 |
+
usort($selectedFees, function ($a, $b) {
|
| 63 |
+
if ($a['academic_session'] != $b['academic_session']) {
|
| 64 |
+
return $a['academic_session'] - $b['academic_session'];
|
| 65 |
+
}
|
| 66 |
+
return $a['term_of_session'] - $b['term_of_session'];
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
// Re-fetch bank statement to verify
|
| 70 |
+
$bankStatement = $this->getBankStatement($tellerNumber);
|
| 71 |
+
|
| 72 |
+
if (!$bankStatement) {
|
| 73 |
+
throw new Exception('Bank statement not found for teller number: ' . $tellerNumber);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Verify unreconciled amount
|
| 77 |
+
if ($amountToUse > $bankStatement['unreconciled_amount']) {
|
| 78 |
+
throw new Exception('Amount exceeds unreconciled amount on teller');
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Extract teller name and number from description
|
| 82 |
+
$descParts = explode(' ', $bankStatement['description']);
|
| 83 |
+
$tellerNo = array_pop($descParts);
|
| 84 |
+
$tellerName = implode(' ', $descParts);
|
| 85 |
+
|
| 86 |
+
// Use the oldest fee's session/term for transaction_id
|
| 87 |
+
$dominantSession = $selectedFees[0]['academic_session'];
|
| 88 |
+
$dominantTerm = $selectedFees[0]['term_of_session'];
|
| 89 |
+
|
| 90 |
+
// Generate transaction_id
|
| 91 |
+
$transactionId = $studentId . $dominantSession . $paymentDate;
|
| 92 |
+
|
| 93 |
+
// STEP 1: Guard check - prevent duplicate payments on same date
|
| 94 |
+
$this->checkDuplicatePayment($studentId, $paymentDate);
|
| 95 |
+
|
| 96 |
+
// STEP 2: Allocate payment across fees (oldest to newest)
|
| 97 |
+
$feeAllocations = $this->allocatePaymentToFees($selectedFees, $amountToUse);
|
| 98 |
+
|
| 99 |
+
if (count($feeAllocations) === 0) {
|
| 100 |
+
throw new Exception('No fees could be allocated');
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Calculate total paid
|
| 104 |
+
$totalPaid = array_sum(array_column($feeAllocations, 'amount'));
|
| 105 |
+
|
| 106 |
+
// Generate receipt_no (used across all fee records)
|
| 107 |
+
$receiptNo = $studentCode . $paymentDate;
|
| 108 |
+
|
| 109 |
+
// STEP 3: Execute database transaction
|
| 110 |
+
$this->pdo->beginTransaction();
|
| 111 |
+
|
| 112 |
+
try {
|
| 113 |
+
// 3a) INSERT into tb_account_school_fee_payments (per fee)
|
| 114 |
+
$this->insertSchoolFeePayments(
|
| 115 |
+
$feeAllocations,
|
| 116 |
+
$studentId,
|
| 117 |
+
$studentCode,
|
| 118 |
+
$transactionId,
|
| 119 |
+
$tellerNo,
|
| 120 |
+
$tellerName,
|
| 121 |
+
$paymentDate
|
| 122 |
+
);
|
| 123 |
+
|
| 124 |
+
// 3b) INSERT into tb_account_school_fee_sum_payments (single record)
|
| 125 |
+
$this->insertSumPayment(
|
| 126 |
+
$studentId,
|
| 127 |
+
$totalPaid,
|
| 128 |
+
$paymentDate,
|
| 129 |
+
$transactionId,
|
| 130 |
+
$dominantSession,
|
| 131 |
+
$dominantTerm
|
| 132 |
+
);
|
| 133 |
+
|
| 134 |
+
// 3c) INSERT into tb_account_student_payments (per fee)
|
| 135 |
+
$this->insertStudentPayments(
|
| 136 |
+
$feeAllocations,
|
| 137 |
+
$studentId,
|
| 138 |
+
$studentCode,
|
| 139 |
+
$transactionId,
|
| 140 |
+
$paymentDate
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
// 3d) INSERT into tb_account_payment_registers (per fee)
|
| 144 |
+
$this->insertPaymentRegisters(
|
| 145 |
+
$feeAllocations,
|
| 146 |
+
$studentId,
|
| 147 |
+
$studentCode,
|
| 148 |
+
$receiptNo,
|
| 149 |
+
$paymentDate,
|
| 150 |
+
$transactionId
|
| 151 |
+
);
|
| 152 |
+
|
| 153 |
+
// 3e) UPDATE tb_student_logistics
|
| 154 |
+
$this->updateStudentLogistics($feeAllocations, $studentId);
|
| 155 |
+
|
| 156 |
+
// Commit transaction
|
| 157 |
+
$this->pdo->commit();
|
| 158 |
+
|
| 159 |
+
// Fetch fee descriptions for display
|
| 160 |
+
$feeDescriptions = $this->getFeeDescriptions($feeAllocations);
|
| 161 |
+
|
| 162 |
+
// Prepare payment details for display
|
| 163 |
+
foreach ($feeAllocations as $key => $allocation) {
|
| 164 |
+
$feeAllocations[$key]['description'] = $feeDescriptions[$allocation['fee_id']] ?? 'Unknown Fee';
|
| 165 |
+
$feeAllocations[$key]['total_paid_to_date'] = $allocation['previous_paid'] + $allocation['amount'];
|
| 166 |
+
$feeAllocations[$key]['balance'] = $allocation['amount_billed'] - $feeAllocations[$key]['total_paid_to_date'];
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
return [
|
| 170 |
+
'success' => true,
|
| 171 |
+
'message' => 'Payment processed successfully',
|
| 172 |
+
'data' => [
|
| 173 |
+
'student_id' => $studentId,
|
| 174 |
+
'student_code' => $studentCode,
|
| 175 |
+
'payment_date' => $paymentDate,
|
| 176 |
+
'teller_no' => $tellerNo,
|
| 177 |
+
'teller_name' => $tellerName,
|
| 178 |
+
'total_paid' => $totalPaid,
|
| 179 |
+
'receipt_no' => $receiptNo,
|
| 180 |
+
'transaction_id' => $transactionId,
|
| 181 |
+
'allocations' => $feeAllocations,
|
| 182 |
+
'remaining_unreconciled' => $bankStatement['unreconciled_amount'] - $totalPaid,
|
| 183 |
+
'source' => $source
|
| 184 |
+
]
|
| 185 |
+
];
|
| 186 |
+
|
| 187 |
+
} catch (Exception $e) {
|
| 188 |
+
$this->pdo->rollBack();
|
| 189 |
+
throw new Exception('Transaction failed: ' . $e->getMessage());
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/**
|
| 194 |
+
* Get bank statement by teller number
|
| 195 |
+
*/
|
| 196 |
+
private function getBankStatement($tellerNumber)
|
| 197 |
+
{
|
| 198 |
+
$sql = "SELECT
|
| 199 |
+
bs.id,
|
| 200 |
+
bs.description,
|
| 201 |
+
bs.amount_paid,
|
| 202 |
+
bs.payment_date,
|
| 203 |
+
COALESCE(fp.total_registered_fee, 0.00) AS registered_amount,
|
| 204 |
+
(bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount
|
| 205 |
+
FROM tb_account_bank_statements bs
|
| 206 |
+
LEFT JOIN (
|
| 207 |
+
SELECT teller_no, SUM(amount_paid) AS total_registered_fee
|
| 208 |
+
FROM tb_account_school_fee_payments
|
| 209 |
+
GROUP BY teller_no
|
| 210 |
+
) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no
|
| 211 |
+
WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number
|
| 212 |
+
LIMIT 1";
|
| 213 |
+
|
| 214 |
+
$stmt = $this->pdo->prepare($sql);
|
| 215 |
+
$stmt->execute(['teller_number' => $tellerNumber]);
|
| 216 |
+
return $stmt->fetch();
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
/**
|
| 220 |
+
* Check for duplicate payment on the same date
|
| 221 |
+
*/
|
| 222 |
+
private function checkDuplicatePayment($studentId, $paymentDate)
|
| 223 |
+
{
|
| 224 |
+
$sql = "SELECT COUNT(*) AS cnt
|
| 225 |
+
FROM tb_account_school_fee_sum_payments
|
| 226 |
+
WHERE student_id = :student_id
|
| 227 |
+
AND payment_date = :payment_date";
|
| 228 |
+
|
| 229 |
+
$stmt = $this->pdo->prepare($sql);
|
| 230 |
+
$stmt->execute([
|
| 231 |
+
'student_id' => $studentId,
|
| 232 |
+
'payment_date' => $paymentDate
|
| 233 |
+
]);
|
| 234 |
+
|
| 235 |
+
$duplicateCheck = $stmt->fetch();
|
| 236 |
+
if ($duplicateCheck['cnt'] > 0) {
|
| 237 |
+
throw new Exception('A payment for this student has already been registered on this date (' . $paymentDate . ')');
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/**
|
| 242 |
+
* Allocate payment amount across fees (oldest to newest)
|
| 243 |
+
*/
|
| 244 |
+
private function allocatePaymentToFees($selectedFees, $amountToUse)
|
| 245 |
+
{
|
| 246 |
+
$feeAllocations = [];
|
| 247 |
+
$remainingAmount = $amountToUse;
|
| 248 |
+
|
| 249 |
+
foreach ($selectedFees as $fee) {
|
| 250 |
+
if ($remainingAmount <= 0) {
|
| 251 |
+
break;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
$outstandingAmount = floatval($fee['outstanding_amount']);
|
| 255 |
+
|
| 256 |
+
if ($remainingAmount >= $outstandingAmount) {
|
| 257 |
+
// Fully settle this fee
|
| 258 |
+
$amountForThisFee = $outstandingAmount;
|
| 259 |
+
$remainingAmount -= $outstandingAmount;
|
| 260 |
+
} else {
|
| 261 |
+
// Partially settle this fee
|
| 262 |
+
$amountForThisFee = $remainingAmount;
|
| 263 |
+
$remainingAmount = 0;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
$feeAllocations[] = [
|
| 267 |
+
'fee_id' => $fee['fee_id'],
|
| 268 |
+
'academic_session' => $fee['academic_session'],
|
| 269 |
+
'term_of_session' => $fee['term_of_session'],
|
| 270 |
+
'amount' => $amountForThisFee,
|
| 271 |
+
'amount_billed' => floatval($fee['amount_billed'] ?? 0),
|
| 272 |
+
'previous_paid' => floatval($fee['amount_paid'] ?? 0)
|
| 273 |
+
];
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
return $feeAllocations;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/**
|
| 280 |
+
* Insert records into tb_account_school_fee_payments
|
| 281 |
+
*/
|
| 282 |
+
private function insertSchoolFeePayments(
|
| 283 |
+
$feeAllocations,
|
| 284 |
+
$studentId,
|
| 285 |
+
$studentCode,
|
| 286 |
+
$transactionId,
|
| 287 |
+
$tellerNo,
|
| 288 |
+
$tellerName,
|
| 289 |
+
$paymentDate
|
| 290 |
+
) {
|
| 291 |
+
foreach ($feeAllocations as $allocation) {
|
| 292 |
+
$schoolFeePaymentId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 293 |
+
|
| 294 |
+
$sql = "INSERT INTO tb_account_school_fee_payments (
|
| 295 |
+
id, fee_id, student_id, transaction_id, amount_paid,
|
| 296 |
+
teller_no, teller_name, credit_bank_id, debit_bank_id,
|
| 297 |
+
paymode_id, payment_status, payment_date, recipient_id,
|
| 298 |
+
academic_session, term_of_session, payment_by, payment_on
|
| 299 |
+
) VALUES (
|
| 300 |
+
:id, :fee_id, :student_id, :transaction_id, :amount_paid,
|
| 301 |
+
:teller_no, :teller_name, :credit_bank_id, :debit_bank_id,
|
| 302 |
+
:paymode_id, :payment_status, :payment_date, :recipient_id,
|
| 303 |
+
:academic_session, :term_of_session, :payment_by, NOW()
|
| 304 |
+
)";
|
| 305 |
+
|
| 306 |
+
$stmt = $this->pdo->prepare($sql);
|
| 307 |
+
$stmt->execute([
|
| 308 |
+
'id' => $schoolFeePaymentId,
|
| 309 |
+
'fee_id' => $allocation['fee_id'],
|
| 310 |
+
'student_id' => $studentId,
|
| 311 |
+
'transaction_id' => $transactionId,
|
| 312 |
+
'amount_paid' => $allocation['amount'],
|
| 313 |
+
'teller_no' => $tellerNo,
|
| 314 |
+
'teller_name' => $tellerName,
|
| 315 |
+
'credit_bank_id' => self::CREDIT_BANK_ID,
|
| 316 |
+
'debit_bank_id' => self::DEBIT_BANK_ID,
|
| 317 |
+
'paymode_id' => self::PAYMODE_ID,
|
| 318 |
+
'payment_status' => self::PAYMENT_STATUS,
|
| 319 |
+
'payment_date' => $paymentDate,
|
| 320 |
+
'recipient_id' => self::RECIPIENT_ID,
|
| 321 |
+
'academic_session' => $allocation['academic_session'],
|
| 322 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 323 |
+
'payment_by' => self::PAYMENT_BY
|
| 324 |
+
]);
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
/**
|
| 329 |
+
* Insert record into tb_account_school_fee_sum_payments
|
| 330 |
+
*/
|
| 331 |
+
private function insertSumPayment(
|
| 332 |
+
$studentId,
|
| 333 |
+
$totalPaid,
|
| 334 |
+
$paymentDate,
|
| 335 |
+
$transactionId,
|
| 336 |
+
$dominantSession,
|
| 337 |
+
$dominantTerm
|
| 338 |
+
) {
|
| 339 |
+
$sumPaymentsId = $studentId . $dominantSession . $paymentDate;
|
| 340 |
+
|
| 341 |
+
$sql = "INSERT INTO tb_account_school_fee_sum_payments (
|
| 342 |
+
id, student_id, total_paid, paymode_id, payment_date,
|
| 343 |
+
credit_bank_id, debit_bank_id, transaction_id, status,
|
| 344 |
+
academic_session, term_of_session, registered_by, registered_on
|
| 345 |
+
) VALUES (
|
| 346 |
+
:id, :student_id, :total_paid, :paymode_id, :payment_date,
|
| 347 |
+
:credit_bank_id, :debit_bank_id, :transaction_id, :status,
|
| 348 |
+
:academic_session, :term_of_session, :registered_by, NOW()
|
| 349 |
+
)";
|
| 350 |
+
|
| 351 |
+
$stmt = $this->pdo->prepare($sql);
|
| 352 |
+
$stmt->execute([
|
| 353 |
+
'id' => $sumPaymentsId,
|
| 354 |
+
'student_id' => $studentId,
|
| 355 |
+
'total_paid' => $totalPaid,
|
| 356 |
+
'paymode_id' => self::PAYMODE_ID,
|
| 357 |
+
'payment_date' => $paymentDate,
|
| 358 |
+
'credit_bank_id' => self::CREDIT_BANK_ID,
|
| 359 |
+
'debit_bank_id' => self::DEBIT_BANK_ID,
|
| 360 |
+
'transaction_id' => $transactionId,
|
| 361 |
+
'status' => self::PAYMENT_STATUS,
|
| 362 |
+
'academic_session' => $dominantSession,
|
| 363 |
+
'term_of_session' => $dominantTerm,
|
| 364 |
+
'registered_by' => self::CREATED_BY
|
| 365 |
+
]);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
/**
|
| 369 |
+
* Insert records into tb_account_student_payments
|
| 370 |
+
*/
|
| 371 |
+
private function insertStudentPayments(
|
| 372 |
+
$feeAllocations,
|
| 373 |
+
$studentId,
|
| 374 |
+
$studentCode,
|
| 375 |
+
$transactionId,
|
| 376 |
+
$paymentDate
|
| 377 |
+
) {
|
| 378 |
+
foreach ($feeAllocations as $allocation) {
|
| 379 |
+
// Get current payment_to_date (sum of all previous payments for this fee/session/term)
|
| 380 |
+
$sql = "SELECT SUM(payment_to_date) AS current_total
|
| 381 |
+
FROM tb_account_student_payments
|
| 382 |
+
WHERE student_id = :student_id
|
| 383 |
+
AND fee_id = :fee_id
|
| 384 |
+
AND academic_session = :academic_session
|
| 385 |
+
AND term_of_session = :term_of_session";
|
| 386 |
+
|
| 387 |
+
$stmt = $this->pdo->prepare($sql);
|
| 388 |
+
$stmt->execute([
|
| 389 |
+
'student_id' => $studentId,
|
| 390 |
+
'fee_id' => $allocation['fee_id'],
|
| 391 |
+
'academic_session' => $allocation['academic_session'],
|
| 392 |
+
'term_of_session' => $allocation['term_of_session']
|
| 393 |
+
]);
|
| 394 |
+
|
| 395 |
+
$currentPayment = $stmt->fetch();
|
| 396 |
+
$newPaymentToDate = $allocation['amount'];
|
| 397 |
+
|
| 398 |
+
$studentPaymentId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 399 |
+
|
| 400 |
+
$sql = "INSERT INTO tb_account_student_payments (
|
| 401 |
+
id, fee_id, student_id, payment_to_date, transaction_id,
|
| 402 |
+
academic_session, term_of_session, created_by, created_as, created_on
|
| 403 |
+
) VALUES (
|
| 404 |
+
:id, :fee_id, :student_id, :payment_to_date, :transaction_id,
|
| 405 |
+
:academic_session, :term_of_session, :created_by, :created_as, NOW()
|
| 406 |
+
)";
|
| 407 |
+
|
| 408 |
+
$stmt = $this->pdo->prepare($sql);
|
| 409 |
+
$stmt->execute([
|
| 410 |
+
'id' => $studentPaymentId,
|
| 411 |
+
'fee_id' => $allocation['fee_id'],
|
| 412 |
+
'student_id' => $studentId,
|
| 413 |
+
'payment_to_date' => $newPaymentToDate,
|
| 414 |
+
'transaction_id' => $transactionId,
|
| 415 |
+
'academic_session' => $allocation['academic_session'],
|
| 416 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 417 |
+
'created_by' => self::CREATED_BY,
|
| 418 |
+
'created_as' => self::CREATED_AS
|
| 419 |
+
]);
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
/**
|
| 424 |
+
* Insert records into tb_account_payment_registers
|
| 425 |
+
*/
|
| 426 |
+
private function insertPaymentRegisters(
|
| 427 |
+
$feeAllocations,
|
| 428 |
+
$studentId,
|
| 429 |
+
$studentCode,
|
| 430 |
+
$receiptNo,
|
| 431 |
+
$paymentDate,
|
| 432 |
+
$transactionId
|
| 433 |
+
) {
|
| 434 |
+
foreach ($feeAllocations as $allocation) {
|
| 435 |
+
$paymentRegisterId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 436 |
+
|
| 437 |
+
$sql = "INSERT INTO tb_account_payment_registers (
|
| 438 |
+
id, fee_id, student_id, amount_paid, amount_due,
|
| 439 |
+
receipt_no, recipient_id, payment_date, paymode_category,
|
| 440 |
+
transaction_id, academic_session, term_of_session,
|
| 441 |
+
created_by, created_as, created_on
|
| 442 |
+
) VALUES (
|
| 443 |
+
:id, :fee_id, :student_id, :amount_paid, :amount_due,
|
| 444 |
+
:receipt_no, :recipient_id, :payment_date, :paymode_category,
|
| 445 |
+
:transaction_id, :academic_session, :term_of_session,
|
| 446 |
+
:created_by, :created_as, NOW()
|
| 447 |
+
)";
|
| 448 |
+
|
| 449 |
+
$stmt = $this->pdo->prepare($sql);
|
| 450 |
+
$stmt->execute([
|
| 451 |
+
'id' => $paymentRegisterId,
|
| 452 |
+
'fee_id' => $allocation['fee_id'],
|
| 453 |
+
'student_id' => $studentId,
|
| 454 |
+
'amount_paid' => $allocation['amount'],
|
| 455 |
+
'amount_due' => $allocation['amount'],
|
| 456 |
+
'receipt_no' => $receiptNo,
|
| 457 |
+
'recipient_id' => self::RECIPIENT_ID,
|
| 458 |
+
'payment_date' => $paymentDate,
|
| 459 |
+
'paymode_category' => self::PAYMODE_CATEGORY,
|
| 460 |
+
'transaction_id' => $transactionId,
|
| 461 |
+
'academic_session' => $allocation['academic_session'],
|
| 462 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 463 |
+
'created_by' => self::CREATED_BY,
|
| 464 |
+
'created_as' => self::CREATED_AS
|
| 465 |
+
]);
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
/**
|
| 470 |
+
* Update tb_student_logistics to reduce outstanding fees
|
| 471 |
+
*/
|
| 472 |
+
private function updateStudentLogistics($feeAllocations, $studentId)
|
| 473 |
+
{
|
| 474 |
+
// Group allocations by session/term to update specific records
|
| 475 |
+
$sessionTermTotals = [];
|
| 476 |
+
foreach ($feeAllocations as $allocation) {
|
| 477 |
+
$key = $allocation['academic_session'] . '-' . $allocation['term_of_session'];
|
| 478 |
+
if (!isset($sessionTermTotals[$key])) {
|
| 479 |
+
$sessionTermTotals[$key] = [
|
| 480 |
+
'academic_session' => $allocation['academic_session'],
|
| 481 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 482 |
+
'total' => 0
|
| 483 |
+
];
|
| 484 |
+
}
|
| 485 |
+
$sessionTermTotals[$key]['total'] += $allocation['amount'];
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
// Update each session/term record
|
| 489 |
+
foreach ($sessionTermTotals as $st) {
|
| 490 |
+
$sql = "UPDATE tb_student_logistics
|
| 491 |
+
SET fees_outstanding = GREATEST(0, fees_outstanding - :total_paid)
|
| 492 |
+
WHERE student_id = :student_id
|
| 493 |
+
AND academic_session = :academic_session
|
| 494 |
+
AND term_of_session = :term_of_session";
|
| 495 |
+
|
| 496 |
+
$stmt = $this->pdo->prepare($sql);
|
| 497 |
+
$stmt->execute([
|
| 498 |
+
'total_paid' => $st['total'],
|
| 499 |
+
'student_id' => $studentId,
|
| 500 |
+
'academic_session' => $st['academic_session'],
|
| 501 |
+
'term_of_session' => $st['term_of_session']
|
| 502 |
+
]);
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
/**
|
| 507 |
+
* Get fee descriptions for display
|
| 508 |
+
*/
|
| 509 |
+
private function getFeeDescriptions($feeAllocations)
|
| 510 |
+
{
|
| 511 |
+
$feeIds = array_column($feeAllocations, 'fee_id');
|
| 512 |
+
$placeholders = implode(',', array_fill(0, count($feeIds), '?'));
|
| 513 |
+
|
| 514 |
+
$sql = "SELECT id, description FROM tb_account_school_fees WHERE id IN ($placeholders)";
|
| 515 |
+
$stmt = $this->pdo->prepare($sql);
|
| 516 |
+
$stmt->execute($feeIds);
|
| 517 |
+
return $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
/**
|
| 521 |
+
* Validate student exists
|
| 522 |
+
*/
|
| 523 |
+
public function validateStudent($studentId)
|
| 524 |
+
{
|
| 525 |
+
$sql = "SELECT id, student_code,
|
| 526 |
+
CONCAT(last_name, ' ', first_name, ' ', COALESCE(other_name, '')) AS full_name
|
| 527 |
+
FROM tb_student_registrations
|
| 528 |
+
WHERE id = :student_id AND admission_status = 'Active'";
|
| 529 |
+
|
| 530 |
+
$stmt = $this->pdo->prepare($sql);
|
| 531 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 532 |
+
return $stmt->fetch();
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
/**
|
| 536 |
+
* Get outstanding fees for a student
|
| 537 |
+
*/
|
| 538 |
+
public function getOutstandingFees($studentId)
|
| 539 |
+
{
|
| 540 |
+
$sql = "SELECT
|
| 541 |
+
ar.fee_id,
|
| 542 |
+
sf.description AS fee_description,
|
| 543 |
+
ar.academic_session,
|
| 544 |
+
ar.term_of_session,
|
| 545 |
+
ar.actual_value AS amount_billed,
|
| 546 |
+
COALESCE(SUM(sp.payment_to_date), 0) AS amount_paid,
|
| 547 |
+
(ar.actual_value - COALESCE(SUM(sp.payment_to_date), 0)) AS outstanding_amount
|
| 548 |
+
FROM tb_account_receivables ar
|
| 549 |
+
INNER JOIN tb_account_school_fees sf ON ar.fee_id = sf.id
|
| 550 |
+
LEFT JOIN tb_account_student_payments sp ON
|
| 551 |
+
ar.student_id = sp.student_id AND
|
| 552 |
+
ar.fee_id = sp.fee_id AND
|
| 553 |
+
ar.academic_session = sp.academic_session AND
|
| 554 |
+
ar.term_of_session = sp.term_of_session
|
| 555 |
+
WHERE ar.student_id = :student_id
|
| 556 |
+
GROUP BY ar.fee_id, ar.academic_session, ar.term_of_session, sf.description, ar.actual_value
|
| 557 |
+
HAVING outstanding_amount > 0
|
| 558 |
+
ORDER BY ar.academic_session ASC, ar.term_of_session ASC";
|
| 559 |
+
|
| 560 |
+
$stmt = $this->pdo->prepare($sql);
|
| 561 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 562 |
+
return $stmt->fetchAll();
|
| 563 |
+
}
|
| 564 |
+
}
|
easypay/includes/ReceiptGenerator.php
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
class ReceiptGenerator
|
| 4 |
+
{
|
| 5 |
+
private $fontPath;
|
| 6 |
+
private $logoPath;
|
| 7 |
+
|
| 8 |
+
public function __construct()
|
| 9 |
+
{
|
| 10 |
+
// Use Arial from Windows Fonts generally available on Windows
|
| 11 |
+
$this->fontPath = 'C:/Windows/Fonts/arial.ttf';
|
| 12 |
+
$this->logoPath = __DIR__ . '/../assets/logo.png';
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
public function generate($data)
|
| 16 |
+
{
|
| 17 |
+
$width = 800;
|
| 18 |
+
// Calculate dynamic height
|
| 19 |
+
$baseHeight = 450;
|
| 20 |
+
$rowHeight = 40;
|
| 21 |
+
$numRows = count($data['allocations'] ?? []);
|
| 22 |
+
$height = $baseHeight + ($numRows * $rowHeight) + 150;
|
| 23 |
+
|
| 24 |
+
$image = imagecreatetruecolor($width, $height);
|
| 25 |
+
|
| 26 |
+
// Colors
|
| 27 |
+
$white = imagecolorallocate($image, 255, 255, 255);
|
| 28 |
+
$black = imagecolorallocate($image, 0, 0, 0);
|
| 29 |
+
$grey = imagecolorallocate($image, 100, 100, 100);
|
| 30 |
+
$lightGrey = imagecolorallocate($image, 240, 240, 240);
|
| 31 |
+
|
| 32 |
+
imagefilledrectangle($image, 0, 0, $width, $height, $white);
|
| 33 |
+
|
| 34 |
+
// Logo
|
| 35 |
+
if (file_exists($this->logoPath)) {
|
| 36 |
+
$logo = @imagecreatefrompng($this->logoPath);
|
| 37 |
+
if ($logo) {
|
| 38 |
+
$logoW = imagesx($logo);
|
| 39 |
+
$logoH = imagesy($logo);
|
| 40 |
+
$targetW = 100;
|
| 41 |
+
$targetH = ($targetW / $logoW) * $logoH;
|
| 42 |
+
|
| 43 |
+
imagecopyresampled($image, $logo, 40, 30, 0, 0, $targetW, $targetH, $logoW, $logoH);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Header
|
| 48 |
+
$this->centerText($image, 18, 50, "ACE JUNIOR COLLEGE", $black);
|
| 49 |
+
$this->centerText($image, 10, 75, "...education for excellence", $grey);
|
| 50 |
+
$this->centerText($image, 10, 100, "1, Ebute - Igbogbo Road, Ipakodo, Ikorodu, Lagos, Nigeria", $black);
|
| 51 |
+
$this->centerText($image, 10, 120, "Phone No(s): 08027449739, 08077275777", $black);
|
| 52 |
+
$this->centerText($image, 10, 140, "mailto:info@acecollege.com.ng https://www.acecollege.com.ng", $black);
|
| 53 |
+
|
| 54 |
+
// Receipt Title
|
| 55 |
+
$titleY = 180;
|
| 56 |
+
$receiptNo = $data['receipt_no'] ?? 'N/A';
|
| 57 |
+
$this->centerText($image, 12, $titleY, "SCHOOL FEES RECEIPT - REFERENCE NO: $receiptNo", $black);
|
| 58 |
+
|
| 59 |
+
// Session Info
|
| 60 |
+
$session = $data['allocations'][0]['academic_session'] ?? '----';
|
| 61 |
+
$term = $data['allocations'][0]['term_of_session'] ?? '-';
|
| 62 |
+
$nextSession = is_numeric($session) ? ($session + 1) : '';
|
| 63 |
+
$sessionStr = "$session/$nextSession ACADEMIC SESSION, TERM $term";
|
| 64 |
+
$this->centerText($image, 10, $titleY + 25, $sessionStr, $grey);
|
| 65 |
+
|
| 66 |
+
// Student Box
|
| 67 |
+
$boxY = 230;
|
| 68 |
+
imagerectangle($image, 40, $boxY, $width - 40, $boxY + 40, $grey);
|
| 69 |
+
// We expect student_name to be passed in $data. If not, fallback.
|
| 70 |
+
$studentName = isset($data['student_name']) ? strtoupper($data['student_name']) : (isset($data['student_code']) ? "STUDENT CODE: " . $data['student_code'] : 'UNKNOWN STUDENT');
|
| 71 |
+
$this->centerText($image, 14, $boxY + 28, $studentName, $grey);
|
| 72 |
+
|
| 73 |
+
// Table Header
|
| 74 |
+
$tableY = 300;
|
| 75 |
+
$cols = [40, 350, 480, 590, 700]; // x positions
|
| 76 |
+
|
| 77 |
+
imageline($image, 40, $tableY, $width - 40, $tableY, $black);
|
| 78 |
+
imageline($image, 40, $tableY + 30, $width - 40, $tableY + 30, $black);
|
| 79 |
+
|
| 80 |
+
$font = $this->fontPath;
|
| 81 |
+
imagettftext($image, 9, 0, $cols[0], $tableY + 20, $black, $font, "FEE DESCRIPTION");
|
| 82 |
+
imagettftext($image, 9, 0, $cols[1], $tableY + 20, $black, $font, "AMOUNT BILLED");
|
| 83 |
+
imagettftext($image, 9, 0, $cols[2], $tableY + 20, $black, $font, "AMOUNT PAID");
|
| 84 |
+
imagettftext($image, 9, 0, $cols[3], $tableY + 20, $black, $font, "PAID TO DATE");
|
| 85 |
+
imagettftext($image, 9, 0, $cols[4], $tableY + 20, $black, $font, "TO BALANCE");
|
| 86 |
+
|
| 87 |
+
// Rows
|
| 88 |
+
$currY = $tableY + 30;
|
| 89 |
+
if (isset($data['allocations']) && is_array($data['allocations'])) {
|
| 90 |
+
foreach ($data['allocations'] as $alloc) {
|
| 91 |
+
$desc = $alloc['description'];
|
| 92 |
+
$billed = number_format($alloc['amount_billed'] ?? 0, 2);
|
| 93 |
+
$paid = number_format($alloc['amount'] ?? 0, 2);
|
| 94 |
+
$paidToDate = number_format($alloc['total_paid_to_date'] ?? 0, 2);
|
| 95 |
+
$balance = number_format($alloc['balance'] ?? 0, 2);
|
| 96 |
+
|
| 97 |
+
$currY += 30;
|
| 98 |
+
imagettftext($image, 9, 0, $cols[0], $currY, $black, $font, substr($desc, 0, 40)); // Truncate if too long
|
| 99 |
+
|
| 100 |
+
$this->rightAlignText($image, 9, $cols[2] - 20, $currY, $billed, $black);
|
| 101 |
+
$this->rightAlignText($image, 9, $cols[3] - 20, $currY, $paid, $black);
|
| 102 |
+
$this->rightAlignText($image, 9, $cols[4] - 20, $currY, $paidToDate, $black);
|
| 103 |
+
$this->rightAlignText($image, 9, $width - 40, $currY, $balance, $black);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Total Row
|
| 108 |
+
$currY += 20;
|
| 109 |
+
imageline($image, 40, $currY, $width - 40, $currY, $black);
|
| 110 |
+
$currY += 30;
|
| 111 |
+
|
| 112 |
+
$totalPaid = number_format($data['total_paid'] ?? 0, 2);
|
| 113 |
+
|
| 114 |
+
// Show total under Amount Paid column
|
| 115 |
+
$this->rightAlignText($image, 10, $cols[3] - 20, $currY, $totalPaid, $grey);
|
| 116 |
+
|
| 117 |
+
imageline($image, 40, $currY + 10, $width - 40, $currY + 10, $black);
|
| 118 |
+
|
| 119 |
+
// Footer
|
| 120 |
+
$footerY = $currY + 50;
|
| 121 |
+
imagettftext($image, 9, 0, 40, $footerY, $black, $font, "RECEIPT DULY VERIFIED BY");
|
| 122 |
+
imagettftext($image, 9, 0, 40, $footerY + 20, $black, $font, "BURSAR'S OFFICE");
|
| 123 |
+
|
| 124 |
+
$receivedY = $footerY;
|
| 125 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY, "RECEIVED IN", $black);
|
| 126 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY + 15, "BANK", $black);
|
| 127 |
+
|
| 128 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY + 40, "RECEIVED ON", $black);
|
| 129 |
+
$dateStr = isset($data['payment_date']) ? strtoupper(date("F d, Y", strtotime($data['payment_date']))) : date("F d, Y");
|
| 130 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY + 55, $dateStr, $black);
|
| 131 |
+
|
| 132 |
+
ob_start();
|
| 133 |
+
imagepng($image);
|
| 134 |
+
$content = ob_get_clean();
|
| 135 |
+
imagedestroy($image);
|
| 136 |
+
|
| 137 |
+
return $content;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
public function generateBase64($data)
|
| 141 |
+
{
|
| 142 |
+
$content = $this->generate($data);
|
| 143 |
+
return base64_encode($content);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
private function centerText($image, $size, $y, $text, $color)
|
| 147 |
+
{
|
| 148 |
+
try {
|
| 149 |
+
$box = imagettfbbox($size, 0, $this->fontPath, $text);
|
| 150 |
+
$textW = $box[2] - $box[0];
|
| 151 |
+
$x = (imagesx($image) - $textW) / 2;
|
| 152 |
+
imagettftext($image, $size, 0, $x, $y, $color, $this->fontPath, $text);
|
| 153 |
+
} catch (Exception $e) {
|
| 154 |
+
// Fallback if font fails (though unlikely on verified system)
|
| 155 |
+
imagestring($image, 2, (imagesx($image) / 2) - (strlen($text) * 3), $y, $text, $color);
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
private function rightAlignText($image, $size, $rightX, $y, $text, $color)
|
| 160 |
+
{
|
| 161 |
+
try {
|
| 162 |
+
$box = imagettfbbox($size, 0, $this->fontPath, $text);
|
| 163 |
+
$textW = $box[2] - $box[0];
|
| 164 |
+
$x = $rightX - $textW;
|
| 165 |
+
imagettftext($image, $size, 0, $x, $y, $color, $this->fontPath, $text);
|
| 166 |
+
} catch (Exception $e) {
|
| 167 |
+
imagestring($image, 2, $rightX - (strlen($text) * 6), $y, $text, $color);
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
easypay/index.php
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* Student Fee Payment Registration - Main Page
|
| 4 |
+
* Search for students and display outstanding fees
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
require_once 'db_config.php';
|
| 8 |
+
|
| 9 |
+
$studentData = null;
|
| 10 |
+
$outstandingFees = [];
|
| 11 |
+
$studentId = $_GET['student_id'] ?? '';
|
| 12 |
+
|
| 13 |
+
// If student is selected, fetch their data and outstanding fees
|
| 14 |
+
if (!empty($studentId)) {
|
| 15 |
+
try {
|
| 16 |
+
// Fetch student details
|
| 17 |
+
$sql = "SELECT
|
| 18 |
+
sr.id,
|
| 19 |
+
sr.student_code,
|
| 20 |
+
CONCAT(sr.last_name, ' ', sr.first_name, ' ', COALESCE(sr.other_name, '')) AS full_name,
|
| 21 |
+
al.level_name
|
| 22 |
+
FROM tb_student_registrations sr
|
| 23 |
+
LEFT JOIN tb_academic_levels al ON al.id = sr.level_id
|
| 24 |
+
WHERE sr.id = :student_id";
|
| 25 |
+
|
| 26 |
+
$stmt = $pdo->prepare($sql);
|
| 27 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 28 |
+
$studentData = $stmt->fetch();
|
| 29 |
+
|
| 30 |
+
if ($studentData) {
|
| 31 |
+
// Fetch outstanding fees per fee_id
|
| 32 |
+
$sql = "SELECT
|
| 33 |
+
ar.id AS receivable_id,
|
| 34 |
+
ar.fee_id,
|
| 35 |
+
asf.description AS fee_description,
|
| 36 |
+
ar.academic_session,
|
| 37 |
+
ar.term_of_session,
|
| 38 |
+
ar.actual_value AS billed_amount,
|
| 39 |
+
COALESCE(asp.total_paid_for_period, 0) AS total_paid, -- Renamed column for clarity
|
| 40 |
+
(ar.actual_value - COALESCE(asp.total_paid_for_period, 0)) AS outstanding_amount,
|
| 41 |
+
ar.created_on
|
| 42 |
+
FROM
|
| 43 |
+
tb_account_receivables ar
|
| 44 |
+
JOIN
|
| 45 |
+
tb_account_school_fees asf ON asf.id = ar.fee_id
|
| 46 |
+
LEFT JOIN (
|
| 47 |
+
-- Subquery now calculates total payments specific to a session, term, and fee
|
| 48 |
+
SELECT
|
| 49 |
+
fee_id,
|
| 50 |
+
student_id,
|
| 51 |
+
academic_session,
|
| 52 |
+
term_of_session,
|
| 53 |
+
SUM(payment_to_date) AS total_paid_for_period
|
| 54 |
+
FROM
|
| 55 |
+
tb_account_student_payments
|
| 56 |
+
GROUP BY
|
| 57 |
+
fee_id,
|
| 58 |
+
student_id,
|
| 59 |
+
academic_session,
|
| 60 |
+
term_of_session
|
| 61 |
+
) asp ON asp.fee_id = ar.fee_id
|
| 62 |
+
AND asp.student_id = ar.student_id
|
| 63 |
+
AND asp.academic_session = ar.academic_session
|
| 64 |
+
AND asp.term_of_session = ar.term_of_session
|
| 65 |
+
WHERE
|
| 66 |
+
ar.student_id = :student_id
|
| 67 |
+
AND ar.academic_session > 2023
|
| 68 |
+
-- Only show records where the calculated outstanding amount is greater than zero
|
| 69 |
+
AND (ar.actual_value - COALESCE(asp.total_paid_for_period, 0)) > 0
|
| 70 |
+
ORDER BY
|
| 71 |
+
ar.academic_session ASC, ar.term_of_session ASC, ar.created_on ASC";
|
| 72 |
+
|
| 73 |
+
$stmt = $pdo->prepare($sql);
|
| 74 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 75 |
+
$outstandingFees = $stmt->fetchAll();
|
| 76 |
+
}
|
| 77 |
+
} catch (PDOException $e) {
|
| 78 |
+
$error = "Error fetching student data: " . $e->getMessage();
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
?>
|
| 82 |
+
<!DOCTYPE html>
|
| 83 |
+
<html lang="en">
|
| 84 |
+
|
| 85 |
+
<head>
|
| 86 |
+
<meta charset="UTF-8">
|
| 87 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 88 |
+
<title>Student Fee Payment Registration</title>
|
| 89 |
+
<style>
|
| 90 |
+
* {
|
| 91 |
+
margin: 0;
|
| 92 |
+
padding: 0;
|
| 93 |
+
box-sizing: border-box;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
body {
|
| 97 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 98 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 99 |
+
min-height: 100vh;
|
| 100 |
+
padding: 20px;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.container {
|
| 104 |
+
max-width: 1200px;
|
| 105 |
+
margin: 0 auto;
|
| 106 |
+
background: white;
|
| 107 |
+
border-radius: 12px;
|
| 108 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
| 109 |
+
padding: 30px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
h1 {
|
| 113 |
+
color: #333;
|
| 114 |
+
margin-bottom: 30px;
|
| 115 |
+
text-align: center;
|
| 116 |
+
font-size: 28px;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.search-section {
|
| 120 |
+
margin-bottom: 30px;
|
| 121 |
+
padding: 20px;
|
| 122 |
+
background: #f8f9fa;
|
| 123 |
+
border-radius: 8px;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.search-box {
|
| 127 |
+
position: relative;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.search-box label {
|
| 131 |
+
display: block;
|
| 132 |
+
margin-bottom: 8px;
|
| 133 |
+
font-weight: 600;
|
| 134 |
+
color: #555;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.search-box input {
|
| 138 |
+
width: 100%;
|
| 139 |
+
padding: 12px 15px;
|
| 140 |
+
border: 2px solid #ddd;
|
| 141 |
+
border-radius: 6px;
|
| 142 |
+
font-size: 16px;
|
| 143 |
+
transition: border-color 0.3s;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.search-box input:focus {
|
| 147 |
+
outline: none;
|
| 148 |
+
border-color: #667eea;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.search-results {
|
| 152 |
+
position: absolute;
|
| 153 |
+
top: 100%;
|
| 154 |
+
left: 0;
|
| 155 |
+
right: 0;
|
| 156 |
+
background: white;
|
| 157 |
+
border: 2px solid #667eea;
|
| 158 |
+
border-top: none;
|
| 159 |
+
border-radius: 0 0 6px 6px;
|
| 160 |
+
max-height: 300px;
|
| 161 |
+
overflow-y: auto;
|
| 162 |
+
display: none;
|
| 163 |
+
z-index: 1000;
|
| 164 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.search-results.active {
|
| 168 |
+
display: block;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.search-result-item {
|
| 172 |
+
padding: 12px 15px;
|
| 173 |
+
cursor: pointer;
|
| 174 |
+
border-bottom: 1px solid #eee;
|
| 175 |
+
transition: background-color 0.2s;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.search-result-item:hover {
|
| 179 |
+
background-color: #f0f0f0;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.search-result-item:last-child {
|
| 183 |
+
border-bottom: none;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.student-code {
|
| 187 |
+
color: #667eea;
|
| 188 |
+
font-weight: 600;
|
| 189 |
+
margin-right: 10px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.student-details {
|
| 193 |
+
margin-bottom: 30px;
|
| 194 |
+
padding: 20px;
|
| 195 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 196 |
+
color: white;
|
| 197 |
+
border-radius: 8px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.student-details h2 {
|
| 201 |
+
margin-bottom: 10px;
|
| 202 |
+
font-size: 24px;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.student-details p {
|
| 206 |
+
font-size: 16px;
|
| 207 |
+
margin-bottom: 5px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.fees-section {
|
| 211 |
+
margin-bottom: 30px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.fees-section h3 {
|
| 215 |
+
margin-bottom: 15px;
|
| 216 |
+
color: #333;
|
| 217 |
+
font-size: 20px;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
table {
|
| 221 |
+
width: 100%;
|
| 222 |
+
border-collapse: collapse;
|
| 223 |
+
margin-bottom: 20px;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
th,
|
| 227 |
+
td {
|
| 228 |
+
padding: 12px;
|
| 229 |
+
text-align: left;
|
| 230 |
+
border-bottom: 1px solid #ddd;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
th {
|
| 234 |
+
background-color: #667eea;
|
| 235 |
+
color: white;
|
| 236 |
+
font-weight: 600;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
tr:hover {
|
| 240 |
+
background-color: #f8f9fa;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.amount {
|
| 244 |
+
text-align: right;
|
| 245 |
+
font-family: 'Courier New', monospace;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.btn {
|
| 249 |
+
padding: 12px 30px;
|
| 250 |
+
border: none;
|
| 251 |
+
border-radius: 6px;
|
| 252 |
+
font-size: 16px;
|
| 253 |
+
font-weight: 600;
|
| 254 |
+
cursor: pointer;
|
| 255 |
+
transition: all 0.3s;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.btn-primary {
|
| 259 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 260 |
+
color: white;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.btn-primary:hover {
|
| 264 |
+
transform: translateY(-2px);
|
| 265 |
+
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.btn-primary:disabled {
|
| 269 |
+
background: #ccc;
|
| 270 |
+
cursor: not-allowed;
|
| 271 |
+
transform: none;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.modal {
|
| 275 |
+
display: none;
|
| 276 |
+
position: fixed;
|
| 277 |
+
top: 0;
|
| 278 |
+
left: 0;
|
| 279 |
+
width: 100%;
|
| 280 |
+
height: 100%;
|
| 281 |
+
background: rgba(0, 0, 0, 0.5);
|
| 282 |
+
z-index: 2000;
|
| 283 |
+
align-items: center;
|
| 284 |
+
justify-content: center;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.modal.active {
|
| 288 |
+
display: flex;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.modal-content {
|
| 292 |
+
background: white;
|
| 293 |
+
padding: 30px;
|
| 294 |
+
border-radius: 12px;
|
| 295 |
+
max-width: 600px;
|
| 296 |
+
width: 90%;
|
| 297 |
+
max-height: 90vh;
|
| 298 |
+
overflow-y: auto;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.modal-header {
|
| 302 |
+
margin-bottom: 20px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.modal-header h2 {
|
| 306 |
+
color: #333;
|
| 307 |
+
font-size: 22px;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.form-group {
|
| 311 |
+
margin-bottom: 20px;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.form-group label {
|
| 315 |
+
display: block;
|
| 316 |
+
margin-bottom: 8px;
|
| 317 |
+
font-weight: 600;
|
| 318 |
+
color: #555;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.form-group input,
|
| 322 |
+
.form-group textarea {
|
| 323 |
+
width: 100%;
|
| 324 |
+
padding: 10px 12px;
|
| 325 |
+
border: 2px solid #ddd;
|
| 326 |
+
border-radius: 6px;
|
| 327 |
+
font-size: 14px;
|
| 328 |
+
font-family: inherit;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.form-group input:focus,
|
| 332 |
+
.form-group textarea:focus {
|
| 333 |
+
outline: none;
|
| 334 |
+
border-color: #667eea;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.form-group input[readonly] {
|
| 338 |
+
background-color: #f0f0f0;
|
| 339 |
+
cursor: not-allowed;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.modal-actions {
|
| 343 |
+
display: flex;
|
| 344 |
+
gap: 10px;
|
| 345 |
+
justify-content: flex-end;
|
| 346 |
+
margin-top: 25px;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.btn-secondary {
|
| 350 |
+
background: #6c757d;
|
| 351 |
+
color: white;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.btn-secondary:hover {
|
| 355 |
+
background: #5a6268;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.error {
|
| 359 |
+
color: #dc3545;
|
| 360 |
+
font-size: 14px;
|
| 361 |
+
margin-top: 5px;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.success {
|
| 365 |
+
color: #28a745;
|
| 366 |
+
font-size: 14px;
|
| 367 |
+
margin-top: 5px;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.alert {
|
| 371 |
+
padding: 15px;
|
| 372 |
+
border-radius: 6px;
|
| 373 |
+
margin-bottom: 20px;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.alert-error {
|
| 377 |
+
background-color: #f8d7da;
|
| 378 |
+
color: #721c24;
|
| 379 |
+
border: 1px solid #f5c6cb;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.total-row {
|
| 383 |
+
font-weight: bold;
|
| 384 |
+
background-color: #f0f0f0;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.loading {
|
| 388 |
+
display: inline-block;
|
| 389 |
+
width: 16px;
|
| 390 |
+
height: 16px;
|
| 391 |
+
border: 3px solid #f3f3f3;
|
| 392 |
+
border-top: 3px solid #667eea;
|
| 393 |
+
border-radius: 50%;
|
| 394 |
+
animation: spin 1s linear infinite;
|
| 395 |
+
margin-left: 10px;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
@keyframes spin {
|
| 399 |
+
0% {
|
| 400 |
+
transform: rotate(0deg);
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
100% {
|
| 404 |
+
transform: rotate(360deg);
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
</style>
|
| 408 |
+
</head>
|
| 409 |
+
|
| 410 |
+
<body>
|
| 411 |
+
<div class="container">
|
| 412 |
+
<h1>Student Fee Payment Registration</h1>
|
| 413 |
+
|
| 414 |
+
<?php if (isset($error)): ?>
|
| 415 |
+
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
| 416 |
+
<?php endif; ?>
|
| 417 |
+
|
| 418 |
+
<!-- Student Search Section -->
|
| 419 |
+
<div class="search-section">
|
| 420 |
+
<div class="search-box">
|
| 421 |
+
<label for="studentSearch">Search Student (by name or student code)</label>
|
| 422 |
+
<input type="text" id="studentSearch" placeholder="Type to search..." autocomplete="off">
|
| 423 |
+
<div class="search-results" id="searchResults"></div>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
<?php if ($studentData): ?>
|
| 428 |
+
<!-- Student Details -->
|
| 429 |
+
<div class="student-details">
|
| 430 |
+
<h2><?php echo htmlspecialchars($studentData['full_name']); ?></h2>
|
| 431 |
+
<p><strong>Student Code:</strong> <?php echo htmlspecialchars($studentData['student_code']); ?></p>
|
| 432 |
+
<p><strong>Academic Level:</strong> <?php echo htmlspecialchars($studentData['level_name'] ?? 'N/A'); ?></p>
|
| 433 |
+
</div>
|
| 434 |
+
|
| 435 |
+
<?php if (count($outstandingFees) > 0): ?>
|
| 436 |
+
<!-- Outstanding Fees Section -->
|
| 437 |
+
<div class="fees-section">
|
| 438 |
+
<h3>Outstanding Fees</h3>
|
| 439 |
+
<form id="paymentForm">
|
| 440 |
+
<input type="hidden" name="student_id" value="<?php echo htmlspecialchars($studentData['id']); ?>">
|
| 441 |
+
<input type="hidden" name="student_code"
|
| 442 |
+
value="<?php echo htmlspecialchars($studentData['student_code']); ?>">
|
| 443 |
+
|
| 444 |
+
<table>
|
| 445 |
+
<thead>
|
| 446 |
+
<tr>
|
| 447 |
+
<th width="50">Select</th>
|
| 448 |
+
<th>Fee Description</th>
|
| 449 |
+
<th width="100">Session</th>
|
| 450 |
+
<th width="80">Term</th>
|
| 451 |
+
<th width="120" class="amount">Billed</th>
|
| 452 |
+
<th width="120" class="amount">Paid</th>
|
| 453 |
+
<th width="120" class="amount">Outstanding</th>
|
| 454 |
+
</tr>
|
| 455 |
+
</thead>
|
| 456 |
+
<tbody>
|
| 457 |
+
<?php
|
| 458 |
+
$totalOutstanding = 0;
|
| 459 |
+
foreach ($outstandingFees as $fee):
|
| 460 |
+
$totalOutstanding += $fee['outstanding_amount'];
|
| 461 |
+
?>
|
| 462 |
+
<tr>
|
| 463 |
+
<td>
|
| 464 |
+
<input type="checkbox" class="fee-checkbox" name="selected_fees[]" value="<?php echo htmlspecialchars(json_encode([
|
| 465 |
+
'receivable_id' => $fee['receivable_id'],
|
| 466 |
+
'fee_id' => $fee['fee_id'],
|
| 467 |
+
'academic_session' => $fee['academic_session'],
|
| 468 |
+
'term_of_session' => $fee['term_of_session'],
|
| 469 |
+
'outstanding_amount' => $fee['outstanding_amount']
|
| 470 |
+
])); ?>" checked>
|
| 471 |
+
</td>
|
| 472 |
+
<td><?php echo htmlspecialchars($fee['fee_description']); ?></td>
|
| 473 |
+
<td><?php echo htmlspecialchars($fee['academic_session']); ?></td>
|
| 474 |
+
<td><?php echo htmlspecialchars($fee['term_of_session']); ?></td>
|
| 475 |
+
<td class="amount">₦<?php echo number_format($fee['billed_amount'], 2); ?></td>
|
| 476 |
+
<td class="amount">₦<?php echo number_format($fee['total_paid'], 2); ?></td>
|
| 477 |
+
<td class="amount">₦<?php echo number_format($fee['outstanding_amount'], 2); ?></td>
|
| 478 |
+
</tr>
|
| 479 |
+
<?php endforeach; ?>
|
| 480 |
+
<tr class="total-row">
|
| 481 |
+
<td colspan="6" style="text-align: right;">Total Outstanding:</td>
|
| 482 |
+
<td class="amount">₦<?php echo number_format($totalOutstanding, 2); ?></td>
|
| 483 |
+
</tr>
|
| 484 |
+
</tbody>
|
| 485 |
+
</table>
|
| 486 |
+
|
| 487 |
+
<button type="button" class="btn btn-primary" id="processPaymentBtn">Process Payment</button>
|
| 488 |
+
</form>
|
| 489 |
+
</div>
|
| 490 |
+
<?php else: ?>
|
| 491 |
+
<div class="alert alert-error">No outstanding fees found for this student.</div>
|
| 492 |
+
<?php endif; ?>
|
| 493 |
+
<?php endif; ?>
|
| 494 |
+
</div>
|
| 495 |
+
|
| 496 |
+
<!-- Payment Modal -->
|
| 497 |
+
<div class="modal" id="paymentModal">
|
| 498 |
+
<div class="modal-content">
|
| 499 |
+
<div class="modal-header">
|
| 500 |
+
<h2>Process Payment</h2>
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
<form id="paymentDetailsForm" method="POST" action="process_payment.php">
|
| 504 |
+
<input type="hidden" name="student_id" id="modal_student_id">
|
| 505 |
+
<input type="hidden" name="student_code" id="modal_student_code">
|
| 506 |
+
<input type="hidden" name="selected_fees" id="modal_selected_fees">
|
| 507 |
+
<input type="hidden" name="payment_date" id="modal_payment_date">
|
| 508 |
+
<input type="hidden" name="bank_description" id="modal_bank_description">
|
| 509 |
+
|
| 510 |
+
<div class="form-group">
|
| 511 |
+
<label for="teller_number">Teller Number *</label>
|
| 512 |
+
<input type="text" id="teller_number" name="teller_number" required>
|
| 513 |
+
<span class="loading" id="tellerLoading" style="display:none;"></span>
|
| 514 |
+
<div class="error" id="tellerError"></div>
|
| 515 |
+
</div>
|
| 516 |
+
|
| 517 |
+
<div class="form-group">
|
| 518 |
+
<label for="bank_narration">Bank Narration</label>
|
| 519 |
+
<textarea id="bank_narration" name="bank_narration" rows="3" readonly></textarea>
|
| 520 |
+
</div>
|
| 521 |
+
|
| 522 |
+
<div class="form-group">
|
| 523 |
+
<label for="unreconciled_amount">Unreconciled Amount on Teller</label>
|
| 524 |
+
<input type="text" id="unreconciled_amount" readonly>
|
| 525 |
+
</div>
|
| 526 |
+
|
| 527 |
+
<div class="form-group">
|
| 528 |
+
<label for="amount_to_use">Amount to Use for Fees *</label>
|
| 529 |
+
<input type="number" id="amount_to_use" name="amount_to_use" step="0.01" min="0" required>
|
| 530 |
+
<div class="error" id="amountError"></div>
|
| 531 |
+
</div>
|
| 532 |
+
|
| 533 |
+
<div class="modal-actions">
|
| 534 |
+
<button type="button" class="btn btn-secondary" id="cancelBtn">Cancel</button>
|
| 535 |
+
<button type="submit" class="btn btn-primary" id="proceedBtn" disabled>OK PROCEED!</button>
|
| 536 |
+
</div>
|
| 537 |
+
</form>
|
| 538 |
+
</div>
|
| 539 |
+
</div>
|
| 540 |
+
|
| 541 |
+
<script>
|
| 542 |
+
// Student search functionality
|
| 543 |
+
const searchInput = document.getElementById('studentSearch');
|
| 544 |
+
const searchResults = document.getElementById('searchResults');
|
| 545 |
+
let searchTimeout;
|
| 546 |
+
|
| 547 |
+
searchInput.addEventListener('input', function () {
|
| 548 |
+
clearTimeout(searchTimeout);
|
| 549 |
+
const searchTerm = this.value.trim();
|
| 550 |
+
|
| 551 |
+
if (searchTerm.length < 2) {
|
| 552 |
+
searchResults.classList.remove('active');
|
| 553 |
+
searchResults.innerHTML = '';
|
| 554 |
+
return;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
searchTimeout = setTimeout(() => {
|
| 558 |
+
fetch(`ajax_handlers.php?action=search_students&search=${encodeURIComponent(searchTerm)}`)
|
| 559 |
+
.then(response => response.json())
|
| 560 |
+
.then(data => {
|
| 561 |
+
if (data.error) {
|
| 562 |
+
searchResults.innerHTML = `<div class="search-result-item">${data.error}</div>`;
|
| 563 |
+
} else if (data.length === 0) {
|
| 564 |
+
searchResults.innerHTML = '<div class="search-result-item">No students found</div>';
|
| 565 |
+
} else {
|
| 566 |
+
searchResults.innerHTML = data.map(student =>
|
| 567 |
+
`<div class="search-result-item" data-id="${student.id}">
|
| 568 |
+
<span class="student-code">${student.student_code}</span>
|
| 569 |
+
<span>${student.full_name}</span>
|
| 570 |
+
</div>`
|
| 571 |
+
).join('');
|
| 572 |
+
|
| 573 |
+
// Add click handlers
|
| 574 |
+
document.querySelectorAll('.search-result-item').forEach(item => {
|
| 575 |
+
item.addEventListener('click', function () {
|
| 576 |
+
const studentId = this.dataset.id;
|
| 577 |
+
window.location.href = `?student_id=${studentId}`;
|
| 578 |
+
});
|
| 579 |
+
});
|
| 580 |
+
}
|
| 581 |
+
searchResults.classList.add('active');
|
| 582 |
+
})
|
| 583 |
+
.catch(error => {
|
| 584 |
+
console.error('Search error:', error);
|
| 585 |
+
searchResults.innerHTML = '<div class="search-result-item">Error searching students</div>';
|
| 586 |
+
searchResults.classList.add('active');
|
| 587 |
+
});
|
| 588 |
+
}, 300);
|
| 589 |
+
});
|
| 590 |
+
|
| 591 |
+
// Close search results when clicking outside
|
| 592 |
+
document.addEventListener('click', function (e) {
|
| 593 |
+
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
|
| 594 |
+
searchResults.classList.remove('active');
|
| 595 |
+
}
|
| 596 |
+
});
|
| 597 |
+
|
| 598 |
+
// Payment modal functionality
|
| 599 |
+
const modal = document.getElementById('paymentModal');
|
| 600 |
+
const processPaymentBtn = document.getElementById('processPaymentBtn');
|
| 601 |
+
const cancelBtn = document.getElementById('cancelBtn');
|
| 602 |
+
const tellerInput = document.getElementById('teller_number');
|
| 603 |
+
const tellerLoading = document.getElementById('tellerLoading');
|
| 604 |
+
const tellerError = document.getElementById('tellerError');
|
| 605 |
+
const amountInput = document.getElementById('amount_to_use');
|
| 606 |
+
const amountError = document.getElementById('amountError');
|
| 607 |
+
const proceedBtn = document.getElementById('proceedBtn');
|
| 608 |
+
|
| 609 |
+
let unreconciledAmount = 0;
|
| 610 |
+
|
| 611 |
+
processPaymentBtn?.addEventListener('click', function () {
|
| 612 |
+
const checkedFees = document.querySelectorAll('.fee-checkbox:checked');
|
| 613 |
+
|
| 614 |
+
if (checkedFees.length === 0) {
|
| 615 |
+
alert('Please select at least one fee to process payment.');
|
| 616 |
+
return;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
// Collect selected fees
|
| 620 |
+
const selectedFees = Array.from(checkedFees).map(cb => JSON.parse(cb.value));
|
| 621 |
+
|
| 622 |
+
// Populate modal
|
| 623 |
+
document.getElementById('modal_student_id').value = document.querySelector('input[name="student_id"]').value;
|
| 624 |
+
document.getElementById('modal_student_code').value = document.querySelector('input[name="student_code"]').value;
|
| 625 |
+
document.getElementById('modal_selected_fees').value = JSON.stringify(selectedFees);
|
| 626 |
+
|
| 627 |
+
// Reset form
|
| 628 |
+
document.getElementById('paymentDetailsForm').reset();
|
| 629 |
+
tellerError.textContent = '';
|
| 630 |
+
amountError.textContent = '';
|
| 631 |
+
proceedBtn.disabled = true;
|
| 632 |
+
unreconciledAmount = 0;
|
| 633 |
+
|
| 634 |
+
modal.classList.add('active');
|
| 635 |
+
});
|
| 636 |
+
|
| 637 |
+
cancelBtn.addEventListener('click', function () {
|
| 638 |
+
modal.classList.remove('active');
|
| 639 |
+
});
|
| 640 |
+
|
| 641 |
+
// Teller lookup on blur
|
| 642 |
+
tellerInput.addEventListener('blur', function () {
|
| 643 |
+
const tellerNumber = this.value.trim();
|
| 644 |
+
|
| 645 |
+
if (!tellerNumber) {
|
| 646 |
+
return;
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
tellerLoading.style.display = 'inline-block';
|
| 650 |
+
tellerError.textContent = '';
|
| 651 |
+
|
| 652 |
+
fetch(`ajax_handlers.php?action=lookup_teller&teller_number=${encodeURIComponent(tellerNumber)}`)
|
| 653 |
+
.then(response => response.json())
|
| 654 |
+
.then(data => {
|
| 655 |
+
tellerLoading.style.display = 'none';
|
| 656 |
+
|
| 657 |
+
if (data.error) {
|
| 658 |
+
tellerError.textContent = data.error;
|
| 659 |
+
document.getElementById('bank_narration').value = '';
|
| 660 |
+
document.getElementById('unreconciled_amount').value = '';
|
| 661 |
+
unreconciledAmount = 0;
|
| 662 |
+
proceedBtn.disabled = true;
|
| 663 |
+
} else {
|
| 664 |
+
document.getElementById('bank_narration').value = data.teller_name;
|
| 665 |
+
document.getElementById('unreconciled_amount').value = '₦' + parseFloat(data.unreconciled_amount).toFixed(2);
|
| 666 |
+
document.getElementById('modal_payment_date').value = data.payment_date;
|
| 667 |
+
document.getElementById('modal_bank_description').value = data.description;
|
| 668 |
+
unreconciledAmount = parseFloat(data.unreconciled_amount);
|
| 669 |
+
|
| 670 |
+
// Enable proceed button if amount is valid
|
| 671 |
+
validateAmount();
|
| 672 |
+
}
|
| 673 |
+
})
|
| 674 |
+
.catch(error => {
|
| 675 |
+
tellerLoading.style.display = 'none';
|
| 676 |
+
tellerError.textContent = 'Error looking up teller number';
|
| 677 |
+
console.error('Teller lookup error:', error);
|
| 678 |
+
});
|
| 679 |
+
});
|
| 680 |
+
|
| 681 |
+
// Amount validation
|
| 682 |
+
amountInput.addEventListener('input', validateAmount);
|
| 683 |
+
|
| 684 |
+
function validateAmount() {
|
| 685 |
+
const amount = parseFloat(amountInput.value);
|
| 686 |
+
|
| 687 |
+
if (isNaN(amount) || amount <= 0) {
|
| 688 |
+
amountError.textContent = 'Amount must be greater than zero';
|
| 689 |
+
proceedBtn.disabled = true;
|
| 690 |
+
return;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
if (unreconciledAmount === 0) {
|
| 694 |
+
amountError.textContent = 'Please enter a valid teller number first';
|
| 695 |
+
proceedBtn.disabled = true;
|
| 696 |
+
return;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
if (amount > unreconciledAmount) {
|
| 700 |
+
amountError.textContent = `Amount cannot exceed unreconciled amount (₦${unreconciledAmount.toFixed(2)})`;
|
| 701 |
+
proceedBtn.disabled = true;
|
| 702 |
+
return;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
amountError.textContent = '';
|
| 706 |
+
proceedBtn.disabled = false;
|
| 707 |
+
}
|
| 708 |
+
</script>
|
| 709 |
+
</body>
|
| 710 |
+
|
| 711 |
+
</html>
|
easypay/logs/api.log
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"timestamp":"2026-01-09 12:44:46","ip":"::1","method":"POST","request":{"student_id":"0000012025JSR26549","teller_no":"S59500769","amount":87500},"response":{"status":"error","message":"Internal server error","error_detail":"SQLSTATE[42S22]: Column not found: 1054 Unknown column 'ar.amount_billed' in 'field list'"},"http_code":500}
|
| 2 |
+
{"timestamp":"2026-01-09 13:42:34","ip":"::1","method":"POST","request":{"student_id":"0000012025JSR26549","teller_no":"S59500769","amount":87500},"response":{"status":"error","message":"Internal server error","error_detail":"SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #5 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'one_arps_aci.ar.actual_value' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by"},"http_code":500}
|
| 3 |
+
{"timestamp":"2026-01-09 13:46:52","ip":"::1","method":"POST","request":{"student_id":"0000012025JSR26549","teller_no":"S59500769","amount":87500},"response":{"status":"success","message":"Payment processed successfully","data":{"student_id":"0000012025JSR26549","teller_no":"S59500769","amount":87500,"payment_id":"0000012025JSR2654920252026-01-09","receipt_no":"2025R265492026-01-09","payment_date":"2026-01-09","fees_settled":[{"fee_description":"Tuition [RJ]","session":2025,"term":2,"amount":80000},{"fee_description":"P.T.A Levy","session":2025,"term":2,"amount":500},{"fee_description":"Inter-House Sport","session":2025,"term":2,"amount":7000}]}},"http_code":201}
|
easypay/process_payment - v0.php
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* Process Payment
|
| 4 |
+
* Handle payment allocation and database transactions
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
require_once 'db_config.php';
|
| 8 |
+
|
| 9 |
+
// Initialize response
|
| 10 |
+
$success = false;
|
| 11 |
+
$message = '';
|
| 12 |
+
$paymentDetails = [];
|
| 13 |
+
|
| 14 |
+
try {
|
| 15 |
+
// Validate POST data
|
| 16 |
+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
| 17 |
+
throw new Exception('Invalid request method');
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
$studentId = $_POST['student_id'] ?? '';
|
| 21 |
+
$studentCode = $_POST['student_code'] ?? '';
|
| 22 |
+
$selectedFeesJson = $_POST['selected_fees'] ?? '';
|
| 23 |
+
$tellerNumber = $_POST['teller_number'] ?? '';
|
| 24 |
+
$bankDescription = $_POST['bank_description'] ?? '';
|
| 25 |
+
$paymentDate = $_POST['payment_date'] ?? '';
|
| 26 |
+
$amountToUse = floatval($_POST['amount_to_use'] ?? 0);
|
| 27 |
+
|
| 28 |
+
// Validate required fields
|
| 29 |
+
if (
|
| 30 |
+
empty($studentId) || empty($studentCode) || empty($selectedFeesJson) ||
|
| 31 |
+
empty($tellerNumber) || empty($paymentDate) || $amountToUse <= 0
|
| 32 |
+
) {
|
| 33 |
+
throw new Exception('Missing required fields');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Parse selected fees
|
| 37 |
+
$selectedFees = json_decode($selectedFeesJson, true);
|
| 38 |
+
if (!is_array($selectedFees) || count($selectedFees) === 0) {
|
| 39 |
+
throw new Exception('No fees selected');
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Sort fees by academic_session ASC, term_of_session ASC (oldest to newest)
|
| 43 |
+
usort($selectedFees, function ($a, $b) {
|
| 44 |
+
if ($a['academic_session'] != $b['academic_session']) {
|
| 45 |
+
return $a['academic_session'] - $b['academic_session'];
|
| 46 |
+
}
|
| 47 |
+
return $a['term_of_session'] - $b['term_of_session'];
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
// Re-fetch bank statement to verify
|
| 51 |
+
$sql = "SELECT
|
| 52 |
+
bs.id,
|
| 53 |
+
bs.description,
|
| 54 |
+
bs.amount_paid,
|
| 55 |
+
bs.payment_date,
|
| 56 |
+
COALESCE(fp.total_registered_fee, 0.00) AS registered_amount,
|
| 57 |
+
(bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount
|
| 58 |
+
FROM tb_account_bank_statements bs
|
| 59 |
+
LEFT JOIN (
|
| 60 |
+
SELECT teller_no, SUM(amount_paid) AS total_registered_fee
|
| 61 |
+
FROM tb_account_school_fee_payments
|
| 62 |
+
GROUP BY teller_no
|
| 63 |
+
) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no
|
| 64 |
+
WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number
|
| 65 |
+
LIMIT 1";
|
| 66 |
+
|
| 67 |
+
$stmt = $pdo->prepare($sql);
|
| 68 |
+
$stmt->execute(['teller_number' => $tellerNumber]);
|
| 69 |
+
$bankStatement = $stmt->fetch();
|
| 70 |
+
|
| 71 |
+
if (!$bankStatement) {
|
| 72 |
+
throw new Exception('Bank statement not found for teller number: ' . $tellerNumber);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Verify unreconciled amount
|
| 76 |
+
if ($amountToUse > $bankStatement['unreconciled_amount']) {
|
| 77 |
+
throw new Exception('Amount exceeds unreconciled amount on teller');
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Extract teller name and number from description
|
| 81 |
+
$descParts = explode(' ', $bankStatement['description']);
|
| 82 |
+
$tellerNo = array_pop($descParts);
|
| 83 |
+
$tellerName = implode(' ', $descParts);
|
| 84 |
+
|
| 85 |
+
// Use the oldest fee's session/term for transaction_id
|
| 86 |
+
$dominantSession = $selectedFees[0]['academic_session'];
|
| 87 |
+
$dominantTerm = $selectedFees[0]['term_of_session'];
|
| 88 |
+
|
| 89 |
+
// Generate transaction_id
|
| 90 |
+
$transactionId = $studentId . $dominantSession . $paymentDate;
|
| 91 |
+
|
| 92 |
+
// STEP 1: Guard check - prevent duplicate payments on same date
|
| 93 |
+
$sql = "SELECT COUNT(*) AS cnt
|
| 94 |
+
FROM tb_account_school_fee_sum_payments
|
| 95 |
+
WHERE student_id = :student_id
|
| 96 |
+
AND payment_date = :payment_date";
|
| 97 |
+
|
| 98 |
+
$stmt = $pdo->prepare($sql);
|
| 99 |
+
$stmt->execute([
|
| 100 |
+
'student_id' => $studentId,
|
| 101 |
+
'payment_date' => $paymentDate
|
| 102 |
+
]);
|
| 103 |
+
|
| 104 |
+
$duplicateCheck = $stmt->fetch();
|
| 105 |
+
if ($duplicateCheck['cnt'] > 0) {
|
| 106 |
+
throw new Exception('A payment for this student has already been registered on this date (' . $paymentDate . ')');
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// STEP 2: Allocate payment across fees (oldest to newest)
|
| 110 |
+
$feeAllocations = [];
|
| 111 |
+
$remainingAmount = $amountToUse;
|
| 112 |
+
|
| 113 |
+
foreach ($selectedFees as $fee) {
|
| 114 |
+
if ($remainingAmount <= 0) {
|
| 115 |
+
break;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
$outstandingAmount = floatval($fee['outstanding_amount']);
|
| 119 |
+
|
| 120 |
+
if ($remainingAmount >= $outstandingAmount) {
|
| 121 |
+
// Fully settle this fee
|
| 122 |
+
$amountForThisFee = $outstandingAmount;
|
| 123 |
+
$remainingAmount -= $outstandingAmount;
|
| 124 |
+
} else {
|
| 125 |
+
// Partially settle this fee
|
| 126 |
+
$amountForThisFee = $remainingAmount;
|
| 127 |
+
$remainingAmount = 0;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
$feeAllocations[] = [
|
| 131 |
+
'fee_id' => $fee['fee_id'],
|
| 132 |
+
'academic_session' => $fee['academic_session'],
|
| 133 |
+
'term_of_session' => $fee['term_of_session'],
|
| 134 |
+
'amount' => $amountForThisFee
|
| 135 |
+
];
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if (count($feeAllocations) === 0) {
|
| 139 |
+
throw new Exception('No fees could be allocated');
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Calculate total paid
|
| 143 |
+
$totalPaid = array_sum(array_column($feeAllocations, 'amount'));
|
| 144 |
+
|
| 145 |
+
// STEP 3: Begin database transaction
|
| 146 |
+
$pdo->beginTransaction();
|
| 147 |
+
|
| 148 |
+
try {
|
| 149 |
+
// Constants
|
| 150 |
+
$creditBankId = '000001373634585148';
|
| 151 |
+
$debitBankId = '514297805530965017';
|
| 152 |
+
$paymodeId = '000001373901891416';
|
| 153 |
+
$recipientId = 'SS0011441283890434';
|
| 154 |
+
$paymentBy = 'SS0011441283890434';
|
| 155 |
+
$createdBy = 'SS0011441283890434';
|
| 156 |
+
$createdAs = 'school';
|
| 157 |
+
$paymentStatus = 'Approved';
|
| 158 |
+
$paymodeCategory = 'BANK';
|
| 159 |
+
|
| 160 |
+
// Generate receipt_no (used across all fee records)
|
| 161 |
+
$receiptNo = $studentCode . $paymentDate;
|
| 162 |
+
|
| 163 |
+
// 3a) INSERT into tb_account_school_fee_payments (per fee)
|
| 164 |
+
foreach ($feeAllocations as $allocation) {
|
| 165 |
+
$schoolFeePaymentId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 166 |
+
|
| 167 |
+
$sql = "INSERT INTO tb_account_school_fee_payments (
|
| 168 |
+
id, fee_id, student_id, transaction_id, amount_paid,
|
| 169 |
+
teller_no, teller_name, credit_bank_id, debit_bank_id,
|
| 170 |
+
paymode_id, payment_status, payment_date, recipient_id,
|
| 171 |
+
academic_session, term_of_session, payment_by, payment_on
|
| 172 |
+
) VALUES (
|
| 173 |
+
:id, :fee_id, :student_id, :transaction_id, :amount_paid,
|
| 174 |
+
:teller_no, :teller_name, :credit_bank_id, :debit_bank_id,
|
| 175 |
+
:paymode_id, :payment_status, :payment_date, :recipient_id,
|
| 176 |
+
:academic_session, :term_of_session, :payment_by, NOW()
|
| 177 |
+
)";
|
| 178 |
+
|
| 179 |
+
$stmt = $pdo->prepare($sql);
|
| 180 |
+
$stmt->execute([
|
| 181 |
+
'id' => $schoolFeePaymentId,
|
| 182 |
+
'fee_id' => $allocation['fee_id'],
|
| 183 |
+
'student_id' => $studentId,
|
| 184 |
+
'transaction_id' => $transactionId,
|
| 185 |
+
'amount_paid' => $allocation['amount'],
|
| 186 |
+
'teller_no' => $tellerNo,
|
| 187 |
+
'teller_name' => $tellerName,
|
| 188 |
+
'credit_bank_id' => $creditBankId,
|
| 189 |
+
'debit_bank_id' => $debitBankId,
|
| 190 |
+
'paymode_id' => $paymodeId,
|
| 191 |
+
'payment_status' => $paymentStatus,
|
| 192 |
+
'payment_date' => $paymentDate,
|
| 193 |
+
'recipient_id' => $recipientId,
|
| 194 |
+
'academic_session' => $allocation['academic_session'],
|
| 195 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 196 |
+
'payment_by' => $paymentBy
|
| 197 |
+
]);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
// 3b) INSERT into tb_account_school_fee_sum_payments (single record)
|
| 201 |
+
$sumPaymentsId = $studentId . $dominantSession . $paymentDate;
|
| 202 |
+
|
| 203 |
+
$sql = "INSERT INTO tb_account_school_fee_sum_payments (
|
| 204 |
+
id, student_id, total_paid, paymode_id, payment_date,
|
| 205 |
+
credit_bank_id, debit_bank_id, transaction_id, status,
|
| 206 |
+
academic_session, term_of_session, registered_by, registered_on
|
| 207 |
+
) VALUES (
|
| 208 |
+
:id, :student_id, :total_paid, :paymode_id, :payment_date,
|
| 209 |
+
:credit_bank_id, :debit_bank_id, :transaction_id, :status,
|
| 210 |
+
:academic_session, :term_of_session, :registered_by, NOW()
|
| 211 |
+
)";
|
| 212 |
+
|
| 213 |
+
$stmt = $pdo->prepare($sql);
|
| 214 |
+
$stmt->execute([
|
| 215 |
+
'id' => $sumPaymentsId,
|
| 216 |
+
'student_id' => $studentId,
|
| 217 |
+
'total_paid' => $totalPaid,
|
| 218 |
+
'paymode_id' => $paymodeId,
|
| 219 |
+
'payment_date' => $paymentDate,
|
| 220 |
+
'credit_bank_id' => $creditBankId,
|
| 221 |
+
'debit_bank_id' => $debitBankId,
|
| 222 |
+
'transaction_id' => $transactionId,
|
| 223 |
+
'status' => $paymentStatus,
|
| 224 |
+
'academic_session' => $dominantSession,
|
| 225 |
+
'term_of_session' => $dominantTerm,
|
| 226 |
+
'registered_by' => $createdBy
|
| 227 |
+
]);
|
| 228 |
+
|
| 229 |
+
// 3c) INSERT into tb_account_student_payments (per fee)
|
| 230 |
+
foreach ($feeAllocations as $allocation) {
|
| 231 |
+
// Get current payment_to_date (sum of all previous payments for this fee/session/term)
|
| 232 |
+
$sql = "SELECT SUM(payment_to_date) AS current_total
|
| 233 |
+
FROM tb_account_student_payments
|
| 234 |
+
WHERE student_id = :student_id
|
| 235 |
+
AND fee_id = :fee_id
|
| 236 |
+
AND academic_session = :academic_session
|
| 237 |
+
AND term_of_session = :term_of_session";
|
| 238 |
+
|
| 239 |
+
$stmt = $pdo->prepare($sql);
|
| 240 |
+
$stmt->execute([
|
| 241 |
+
'student_id' => $studentId,
|
| 242 |
+
'fee_id' => $allocation['fee_id'],
|
| 243 |
+
'academic_session' => $allocation['academic_session'],
|
| 244 |
+
'term_of_session' => $allocation['term_of_session']
|
| 245 |
+
]);
|
| 246 |
+
|
| 247 |
+
$currentPayment = $stmt->fetch();
|
| 248 |
+
$currentTotal = floatval($currentPayment['current_total'] ?? 0);
|
| 249 |
+
$newPaymentToDate = $allocation['amount'];
|
| 250 |
+
//$newPaymentToDate = $currentTotal + $allocation['amount']; --Old code that added new payment amount to current total (total before payment)
|
| 251 |
+
|
| 252 |
+
$studentPaymentId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 253 |
+
|
| 254 |
+
$sql = "INSERT INTO tb_account_student_payments (
|
| 255 |
+
id, fee_id, student_id, payment_to_date, transaction_id,
|
| 256 |
+
academic_session, term_of_session, created_by, created_as, created_on
|
| 257 |
+
) VALUES (
|
| 258 |
+
:id, :fee_id, :student_id, :payment_to_date, :transaction_id,
|
| 259 |
+
:academic_session, :term_of_session, :created_by, :created_as, NOW()
|
| 260 |
+
)";
|
| 261 |
+
|
| 262 |
+
$stmt = $pdo->prepare($sql);
|
| 263 |
+
$stmt->execute([
|
| 264 |
+
'id' => $studentPaymentId,
|
| 265 |
+
'fee_id' => $allocation['fee_id'],
|
| 266 |
+
'student_id' => $studentId,
|
| 267 |
+
'payment_to_date' => $newPaymentToDate,
|
| 268 |
+
'transaction_id' => $transactionId,
|
| 269 |
+
'academic_session' => $allocation['academic_session'],
|
| 270 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 271 |
+
'created_by' => $createdBy,
|
| 272 |
+
'created_as' => $createdAs
|
| 273 |
+
]);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
// 3d) INSERT into tb_account_payment_registers (per fee)
|
| 277 |
+
foreach ($feeAllocations as $allocation) {
|
| 278 |
+
$paymentRegisterId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 279 |
+
|
| 280 |
+
$sql = "INSERT INTO tb_account_payment_registers (
|
| 281 |
+
id, fee_id, student_id, amount_paid, amount_due,
|
| 282 |
+
receipt_no, recipient_id, payment_date, paymode_category,
|
| 283 |
+
transaction_id, academic_session, term_of_session,
|
| 284 |
+
created_by, created_as, created_on
|
| 285 |
+
) VALUES (
|
| 286 |
+
:id, :fee_id, :student_id, :amount_paid, :amount_due,
|
| 287 |
+
:receipt_no, :recipient_id, :payment_date, :paymode_category,
|
| 288 |
+
:transaction_id, :academic_session, :term_of_session,
|
| 289 |
+
:created_by, :created_as, NOW()
|
| 290 |
+
)";
|
| 291 |
+
|
| 292 |
+
$stmt = $pdo->prepare($sql);
|
| 293 |
+
$stmt->execute([
|
| 294 |
+
'id' => $paymentRegisterId,
|
| 295 |
+
'fee_id' => $allocation['fee_id'],
|
| 296 |
+
'student_id' => $studentId,
|
| 297 |
+
'amount_paid' => $allocation['amount'],
|
| 298 |
+
'amount_due' => $allocation['amount'],
|
| 299 |
+
'receipt_no' => $receiptNo,
|
| 300 |
+
'recipient_id' => $recipientId,
|
| 301 |
+
'payment_date' => $paymentDate,
|
| 302 |
+
'paymode_category' => $paymodeCategory,
|
| 303 |
+
'transaction_id' => $transactionId,
|
| 304 |
+
'academic_session' => $allocation['academic_session'],
|
| 305 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 306 |
+
'created_by' => $createdBy,
|
| 307 |
+
'created_as' => $createdAs
|
| 308 |
+
]);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
// 3e) UPDATE tb_student_logistics
|
| 312 |
+
// Group allocations by session/term to update specific records
|
| 313 |
+
$sessionTermTotals = [];
|
| 314 |
+
foreach ($feeAllocations as $allocation) {
|
| 315 |
+
$key = $allocation['academic_session'] . '-' . $allocation['term_of_session'];
|
| 316 |
+
if (!isset($sessionTermTotals[$key])) {
|
| 317 |
+
$sessionTermTotals[$key] = [
|
| 318 |
+
'academic_session' => $allocation['academic_session'],
|
| 319 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 320 |
+
'total' => 0
|
| 321 |
+
];
|
| 322 |
+
}
|
| 323 |
+
$sessionTermTotals[$key]['total'] += $allocation['amount'];
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Update each session/term record
|
| 327 |
+
foreach ($sessionTermTotals as $st) {
|
| 328 |
+
$sql = "UPDATE tb_student_logistics
|
| 329 |
+
SET fees_outstanding = GREATEST(0, fees_outstanding - :total_paid)
|
| 330 |
+
WHERE student_id = :student_id
|
| 331 |
+
AND academic_session = :academic_session
|
| 332 |
+
AND term_of_session = :term_of_session";
|
| 333 |
+
|
| 334 |
+
$stmt = $pdo->prepare($sql);
|
| 335 |
+
$stmt->execute([
|
| 336 |
+
'total_paid' => $st['total'],
|
| 337 |
+
'student_id' => $studentId,
|
| 338 |
+
'academic_session' => $st['academic_session'],
|
| 339 |
+
'term_of_session' => $st['term_of_session']
|
| 340 |
+
]);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// Commit transaction
|
| 344 |
+
$pdo->commit();
|
| 345 |
+
|
| 346 |
+
$success = true;
|
| 347 |
+
$message = 'Payment processed successfully!';
|
| 348 |
+
|
| 349 |
+
// Fetch fee descriptions for display
|
| 350 |
+
$feeIds = array_column($feeAllocations, 'fee_id');
|
| 351 |
+
$placeholders = implode(',', array_fill(0, count($feeIds), '?'));
|
| 352 |
+
|
| 353 |
+
$sql = "SELECT id, description FROM tb_account_school_fees WHERE id IN ($placeholders)";
|
| 354 |
+
$stmt = $pdo->prepare($sql);
|
| 355 |
+
$stmt->execute($feeIds);
|
| 356 |
+
$feeDescriptions = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
| 357 |
+
|
| 358 |
+
// Prepare payment details for display
|
| 359 |
+
foreach ($feeAllocations as &$allocation) {
|
| 360 |
+
$allocation['description'] = $feeDescriptions[$allocation['fee_id']] ?? 'Unknown Fee';
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
$paymentDetails = [
|
| 364 |
+
'student_code' => $studentCode,
|
| 365 |
+
'payment_date' => $paymentDate,
|
| 366 |
+
'teller_no' => $tellerNo,
|
| 367 |
+
'teller_name' => $tellerName,
|
| 368 |
+
'total_paid' => $totalPaid,
|
| 369 |
+
'receipt_no' => $receiptNo,
|
| 370 |
+
'transaction_id' => $transactionId,
|
| 371 |
+
'allocations' => $feeAllocations,
|
| 372 |
+
'remaining_unreconciled' => $bankStatement['unreconciled_amount'] - $totalPaid
|
| 373 |
+
];
|
| 374 |
+
|
| 375 |
+
} catch (Exception $e) {
|
| 376 |
+
$pdo->rollBack();
|
| 377 |
+
throw new Exception('Transaction failed: ' . $e->getMessage());
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
} catch (Exception $e) {
|
| 381 |
+
$success = false;
|
| 382 |
+
$message = $e->getMessage();
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// Fetch student name for display
|
| 386 |
+
$studentName = '';
|
| 387 |
+
if (!empty($studentId)) {
|
| 388 |
+
try {
|
| 389 |
+
$sql = "SELECT CONCAT(last_name, ' ', first_name, ' ', COALESCE(other_name, '')) AS full_name
|
| 390 |
+
FROM tb_student_registrations
|
| 391 |
+
WHERE id = :student_id";
|
| 392 |
+
$stmt = $pdo->prepare($sql);
|
| 393 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 394 |
+
$result = $stmt->fetch();
|
| 395 |
+
$studentName = $result['full_name'] ?? 'Unknown Student';
|
| 396 |
+
} catch (Exception $e) {
|
| 397 |
+
$studentName = 'Unknown Student';
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
?>
|
| 401 |
+
<!DOCTYPE html>
|
| 402 |
+
<html lang="en">
|
| 403 |
+
|
| 404 |
+
<head>
|
| 405 |
+
<meta charset="UTF-8">
|
| 406 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 407 |
+
<title>Payment Processing Result</title>
|
| 408 |
+
<style>
|
| 409 |
+
* {
|
| 410 |
+
margin: 0;
|
| 411 |
+
padding: 0;
|
| 412 |
+
box-sizing: border-box;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
body {
|
| 416 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 417 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 418 |
+
min-height: 100vh;
|
| 419 |
+
padding: 20px;
|
| 420 |
+
display: flex;
|
| 421 |
+
align-items: center;
|
| 422 |
+
justify-content: center;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.container {
|
| 426 |
+
max-width: 800px;
|
| 427 |
+
width: 100%;
|
| 428 |
+
background: white;
|
| 429 |
+
border-radius: 12px;
|
| 430 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
| 431 |
+
padding: 40px;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.success-icon {
|
| 435 |
+
width: 80px;
|
| 436 |
+
height: 80px;
|
| 437 |
+
margin: 0 auto 20px;
|
| 438 |
+
background: #28a745;
|
| 439 |
+
border-radius: 50%;
|
| 440 |
+
display: flex;
|
| 441 |
+
align-items: center;
|
| 442 |
+
justify-content: center;
|
| 443 |
+
color: white;
|
| 444 |
+
font-size: 48px;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.error-icon {
|
| 448 |
+
width: 80px;
|
| 449 |
+
height: 80px;
|
| 450 |
+
margin: 0 auto 20px;
|
| 451 |
+
background: #dc3545;
|
| 452 |
+
border-radius: 50%;
|
| 453 |
+
display: flex;
|
| 454 |
+
align-items: center;
|
| 455 |
+
justify-content: center;
|
| 456 |
+
color: white;
|
| 457 |
+
font-size: 48px;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
h1 {
|
| 461 |
+
text-align: center;
|
| 462 |
+
color: #333;
|
| 463 |
+
margin-bottom: 10px;
|
| 464 |
+
font-size: 28px;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.message {
|
| 468 |
+
text-align: center;
|
| 469 |
+
color: #666;
|
| 470 |
+
margin-bottom: 30px;
|
| 471 |
+
font-size: 16px;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.details-section {
|
| 475 |
+
background: #f8f9fa;
|
| 476 |
+
padding: 20px;
|
| 477 |
+
border-radius: 8px;
|
| 478 |
+
margin-bottom: 20px;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.details-section h2 {
|
| 482 |
+
color: #333;
|
| 483 |
+
margin-bottom: 15px;
|
| 484 |
+
font-size: 20px;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.detail-row {
|
| 488 |
+
display: flex;
|
| 489 |
+
justify-content: space-between;
|
| 490 |
+
padding: 10px 0;
|
| 491 |
+
border-bottom: 1px solid #ddd;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.detail-row:last-child {
|
| 495 |
+
border-bottom: none;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.detail-label {
|
| 499 |
+
font-weight: 600;
|
| 500 |
+
color: #555;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.detail-value {
|
| 504 |
+
color: #333;
|
| 505 |
+
text-align: right;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
table {
|
| 509 |
+
width: 100%;
|
| 510 |
+
border-collapse: collapse;
|
| 511 |
+
margin-top: 15px;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
th,
|
| 515 |
+
td {
|
| 516 |
+
padding: 12px;
|
| 517 |
+
text-align: left;
|
| 518 |
+
border-bottom: 1px solid #ddd;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
th {
|
| 522 |
+
background-color: #667eea;
|
| 523 |
+
color: white;
|
| 524 |
+
font-weight: 600;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.amount {
|
| 528 |
+
text-align: right;
|
| 529 |
+
font-family: 'Courier New', monospace;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.total-row {
|
| 533 |
+
font-weight: bold;
|
| 534 |
+
background-color: #f0f0f0;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.btn {
|
| 538 |
+
display: inline-block;
|
| 539 |
+
padding: 12px 30px;
|
| 540 |
+
border: none;
|
| 541 |
+
border-radius: 6px;
|
| 542 |
+
font-size: 16px;
|
| 543 |
+
font-weight: 600;
|
| 544 |
+
cursor: pointer;
|
| 545 |
+
text-decoration: none;
|
| 546 |
+
transition: all 0.3s;
|
| 547 |
+
text-align: center;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.btn-primary {
|
| 551 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 552 |
+
color: white;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.btn-primary:hover {
|
| 556 |
+
transform: translateY(-2px);
|
| 557 |
+
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.actions {
|
| 561 |
+
text-align: center;
|
| 562 |
+
margin-top: 30px;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.highlight {
|
| 566 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 567 |
+
color: white;
|
| 568 |
+
padding: 15px;
|
| 569 |
+
border-radius: 8px;
|
| 570 |
+
margin-bottom: 20px;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.highlight p {
|
| 574 |
+
margin: 5px 0;
|
| 575 |
+
}
|
| 576 |
+
</style>
|
| 577 |
+
</head>
|
| 578 |
+
|
| 579 |
+
<body>
|
| 580 |
+
<div class="container">
|
| 581 |
+
<?php if ($success): ?>
|
| 582 |
+
<div class="success-icon">✓</div>
|
| 583 |
+
<h1>Payment Processed Successfully!</h1>
|
| 584 |
+
<p class="message"><?php echo htmlspecialchars($message); ?></p>
|
| 585 |
+
|
| 586 |
+
<div class="highlight">
|
| 587 |
+
<p><strong>Student:</strong> <?php echo htmlspecialchars($studentName); ?></p>
|
| 588 |
+
<p><strong>Receipt No:</strong> <?php echo htmlspecialchars($paymentDetails['receipt_no']); ?></p>
|
| 589 |
+
<p><strong>Transaction ID:</strong> <?php echo htmlspecialchars($paymentDetails['transaction_id']); ?></p>
|
| 590 |
+
</div>
|
| 591 |
+
|
| 592 |
+
<div class="details-section">
|
| 593 |
+
<h2>Payment Details</h2>
|
| 594 |
+
<div class="detail-row">
|
| 595 |
+
<span class="detail-label">Payment Date:</span>
|
| 596 |
+
<span class="detail-value"><?php echo htmlspecialchars($paymentDetails['payment_date']); ?></span>
|
| 597 |
+
</div>
|
| 598 |
+
<div class="detail-row">
|
| 599 |
+
<span class="detail-label">Teller Number:</span>
|
| 600 |
+
<span class="detail-value"><?php echo htmlspecialchars($paymentDetails['teller_no']); ?></span>
|
| 601 |
+
</div>
|
| 602 |
+
<div class="detail-row">
|
| 603 |
+
<span class="detail-label">Bank Narration:</span>
|
| 604 |
+
<span class="detail-value"><?php echo htmlspecialchars($paymentDetails['teller_name']); ?></span>
|
| 605 |
+
</div>
|
| 606 |
+
<div class="detail-row">
|
| 607 |
+
<span class="detail-label">Total Amount Used:</span>
|
| 608 |
+
<span class="detail-value">₦<?php echo number_format($paymentDetails['total_paid'], 2); ?></span>
|
| 609 |
+
</div>
|
| 610 |
+
<div class="detail-row">
|
| 611 |
+
<span class="detail-label">Remaining Unreconciled on Teller:</span>
|
| 612 |
+
<span
|
| 613 |
+
class="detail-value">₦<?php echo number_format($paymentDetails['remaining_unreconciled'], 2); ?></span>
|
| 614 |
+
</div>
|
| 615 |
+
</div>
|
| 616 |
+
|
| 617 |
+
<div class="details-section">
|
| 618 |
+
<h2>Fees Settled</h2>
|
| 619 |
+
<table>
|
| 620 |
+
<thead>
|
| 621 |
+
<tr>
|
| 622 |
+
<th>Fee Description</th>
|
| 623 |
+
<th width="100">Session</th>
|
| 624 |
+
<th width="80">Term</th>
|
| 625 |
+
<th width="120" class="amount">Amount Paid</th>
|
| 626 |
+
</tr>
|
| 627 |
+
</thead>
|
| 628 |
+
<tbody>
|
| 629 |
+
<?php foreach ($paymentDetails['allocations'] as $allocation): ?>
|
| 630 |
+
<tr>
|
| 631 |
+
<td><?php echo htmlspecialchars($allocation['description']); ?></td>
|
| 632 |
+
<td><?php echo htmlspecialchars($allocation['academic_session']); ?></td>
|
| 633 |
+
<td><?php echo htmlspecialchars($allocation['term_of_session']); ?></td>
|
| 634 |
+
<td class="amount">₦<?php echo number_format($allocation['amount'], 2); ?></td>
|
| 635 |
+
</tr>
|
| 636 |
+
<?php endforeach; ?>
|
| 637 |
+
<tr class="total-row">
|
| 638 |
+
<td colspan="3" style="text-align: right;">Total:</td>
|
| 639 |
+
<td class="amount">₦<?php echo number_format($paymentDetails['total_paid'], 2); ?></td>
|
| 640 |
+
</tr>
|
| 641 |
+
</tbody>
|
| 642 |
+
</table>
|
| 643 |
+
</div>
|
| 644 |
+
|
| 645 |
+
<?php else: ?>
|
| 646 |
+
<div class="error-icon">✗</div>
|
| 647 |
+
<h1>Payment Processing Failed</h1>
|
| 648 |
+
<p class="message" style="color: #dc3545;"><?php echo htmlspecialchars($message); ?></p>
|
| 649 |
+
|
| 650 |
+
<div class="details-section">
|
| 651 |
+
<h2>Error Details</h2>
|
| 652 |
+
<p>The payment could not be processed. No changes were made to the database.</p>
|
| 653 |
+
<p style="margin-top: 10px;"><strong>Error:</strong> <?php echo htmlspecialchars($message); ?></p>
|
| 654 |
+
</div>
|
| 655 |
+
<?php endif; ?>
|
| 656 |
+
|
| 657 |
+
<div class="actions">
|
| 658 |
+
<a href="index.php" class="btn btn-primary">Return to Main Page</a>
|
| 659 |
+
</div>
|
| 660 |
+
</div>
|
| 661 |
+
</body>
|
| 662 |
+
|
| 663 |
+
</html>
|
easypay/process_payment.php
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* Process Payment
|
| 4 |
+
* Handle payment allocation and database transactions
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
require_once 'db_config.php';
|
| 8 |
+
|
| 9 |
+
// Initialize response
|
| 10 |
+
$success = false;
|
| 11 |
+
$message = '';
|
| 12 |
+
$paymentDetails = [];
|
| 13 |
+
|
| 14 |
+
try {
|
| 15 |
+
// Validate POST data
|
| 16 |
+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
| 17 |
+
throw new Exception('Invalid request method');
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
$studentId = $_POST['student_id'] ?? '';
|
| 21 |
+
$studentCode = $_POST['student_code'] ?? '';
|
| 22 |
+
$selectedFeesJson = $_POST['selected_fees'] ?? '';
|
| 23 |
+
$tellerNumber = $_POST['teller_number'] ?? '';
|
| 24 |
+
$bankDescription = $_POST['bank_description'] ?? '';
|
| 25 |
+
$paymentDate = $_POST['payment_date'] ?? '';
|
| 26 |
+
$amountToUse = floatval($_POST['amount_to_use'] ?? 0);
|
| 27 |
+
|
| 28 |
+
// Validate required fields
|
| 29 |
+
if (
|
| 30 |
+
empty($studentId) || empty($studentCode) || empty($selectedFeesJson) ||
|
| 31 |
+
empty($tellerNumber) || empty($paymentDate) || $amountToUse <= 0
|
| 32 |
+
) {
|
| 33 |
+
throw new Exception('Missing required fields');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Parse selected fees
|
| 37 |
+
$selectedFees = json_decode($selectedFeesJson, true);
|
| 38 |
+
if (!is_array($selectedFees) || count($selectedFees) === 0) {
|
| 39 |
+
throw new Exception('No fees selected');
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Sort fees by academic_session ASC, term_of_session ASC (oldest to newest)
|
| 43 |
+
usort($selectedFees, function ($a, $b) {
|
| 44 |
+
if ($a['academic_session'] != $b['academic_session']) {
|
| 45 |
+
return $a['academic_session'] - $b['academic_session'];
|
| 46 |
+
}
|
| 47 |
+
return $a['term_of_session'] - $b['term_of_session'];
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
// Re-fetch bank statement to verify
|
| 51 |
+
$sql = "SELECT
|
| 52 |
+
bs.id,
|
| 53 |
+
bs.description,
|
| 54 |
+
bs.amount_paid,
|
| 55 |
+
bs.payment_date,
|
| 56 |
+
COALESCE(fp.total_registered_fee, 0.00) AS registered_amount,
|
| 57 |
+
(bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount
|
| 58 |
+
FROM tb_account_bank_statements bs
|
| 59 |
+
LEFT JOIN (
|
| 60 |
+
SELECT teller_no, SUM(amount_paid) AS total_registered_fee
|
| 61 |
+
FROM tb_account_school_fee_payments
|
| 62 |
+
GROUP BY teller_no
|
| 63 |
+
) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no
|
| 64 |
+
WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number
|
| 65 |
+
LIMIT 1";
|
| 66 |
+
|
| 67 |
+
$stmt = $pdo->prepare($sql);
|
| 68 |
+
$stmt->execute(['teller_number' => $tellerNumber]);
|
| 69 |
+
$bankStatement = $stmt->fetch();
|
| 70 |
+
|
| 71 |
+
if (!$bankStatement) {
|
| 72 |
+
throw new Exception('Bank statement not found for teller number: ' . $tellerNumber);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Verify unreconciled amount
|
| 76 |
+
if ($amountToUse > $bankStatement['unreconciled_amount']) {
|
| 77 |
+
throw new Exception('Amount exceeds unreconciled amount on teller');
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Extract teller name and number from description
|
| 81 |
+
$descParts = explode(' ', $bankStatement['description']);
|
| 82 |
+
$tellerNo = array_pop($descParts);
|
| 83 |
+
$tellerName = implode(' ', $descParts);
|
| 84 |
+
|
| 85 |
+
// Use the oldest fee's session/term for transaction_id
|
| 86 |
+
$dominantSession = $selectedFees[0]['academic_session'];
|
| 87 |
+
$dominantTerm = $selectedFees[0]['term_of_session'];
|
| 88 |
+
|
| 89 |
+
// Generate transaction_id
|
| 90 |
+
$transactionId = $studentId . $dominantSession . $paymentDate;
|
| 91 |
+
|
| 92 |
+
// STEP 1: Guard check - prevent duplicate payments on same date
|
| 93 |
+
$sql = "SELECT COUNT(*) AS cnt
|
| 94 |
+
FROM tb_account_school_fee_sum_payments
|
| 95 |
+
WHERE student_id = :student_id
|
| 96 |
+
AND payment_date = :payment_date";
|
| 97 |
+
|
| 98 |
+
$stmt = $pdo->prepare($sql);
|
| 99 |
+
$stmt->execute([
|
| 100 |
+
'student_id' => $studentId,
|
| 101 |
+
'payment_date' => $paymentDate
|
| 102 |
+
]);
|
| 103 |
+
|
| 104 |
+
$duplicateCheck = $stmt->fetch();
|
| 105 |
+
if ($duplicateCheck['cnt'] > 0) {
|
| 106 |
+
throw new Exception('A payment for this student has already been registered on this date (' . $paymentDate . ')');
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// STEP 2: Allocate payment across fees (oldest to newest)
|
| 110 |
+
$feeAllocations = [];
|
| 111 |
+
$remainingAmount = $amountToUse;
|
| 112 |
+
|
| 113 |
+
foreach ($selectedFees as $fee) {
|
| 114 |
+
if ($remainingAmount <= 0) {
|
| 115 |
+
break;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
$outstandingAmount = floatval($fee['outstanding_amount']);
|
| 119 |
+
|
| 120 |
+
if ($remainingAmount >= $outstandingAmount) {
|
| 121 |
+
// Fully settle this fee
|
| 122 |
+
$amountForThisFee = $outstandingAmount;
|
| 123 |
+
$remainingAmount -= $outstandingAmount;
|
| 124 |
+
} else {
|
| 125 |
+
// Partially settle this fee
|
| 126 |
+
$amountForThisFee = $remainingAmount;
|
| 127 |
+
$remainingAmount = 0;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
$feeAllocations[] = [
|
| 131 |
+
'fee_id' => $fee['fee_id'],
|
| 132 |
+
'academic_session' => $fee['academic_session'],
|
| 133 |
+
'term_of_session' => $fee['term_of_session'],
|
| 134 |
+
'amount' => $amountForThisFee
|
| 135 |
+
];
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if (count($feeAllocations) === 0) {
|
| 139 |
+
throw new Exception('No fees could be allocated');
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Calculate total paid
|
| 143 |
+
$totalPaid = array_sum(array_column($feeAllocations, 'amount'));
|
| 144 |
+
|
| 145 |
+
// STEP 3: Begin database transaction
|
| 146 |
+
$pdo->beginTransaction();
|
| 147 |
+
|
| 148 |
+
try {
|
| 149 |
+
// Constants
|
| 150 |
+
$creditBankId = '000001373634585148';
|
| 151 |
+
$debitBankId = '514297805530965017';
|
| 152 |
+
$paymodeId = '000001373901891416';
|
| 153 |
+
$recipientId = 'SS0011441283890434';
|
| 154 |
+
$paymentBy = 'SS0011441283890434';
|
| 155 |
+
$createdBy = 'SS0011441283890434';
|
| 156 |
+
$createdAs = 'school';
|
| 157 |
+
$paymentStatus = 'Approved';
|
| 158 |
+
$paymodeCategory = 'BANK';
|
| 159 |
+
|
| 160 |
+
// Generate receipt_no (used across all fee records)
|
| 161 |
+
$receiptNo = $studentCode . $paymentDate;
|
| 162 |
+
|
| 163 |
+
// 3a) INSERT into tb_account_school_fee_payments (per fee)
|
| 164 |
+
foreach ($feeAllocations as $allocation) {
|
| 165 |
+
$schoolFeePaymentId = $studentCode . $allocation['fee_id'] . $paymentDate; //original code
|
| 166 |
+
|
| 167 |
+
//$schoolFeePaymentId = $allocation['fee_id'] . $paymentDate; //Is used to reduce length of id to solve duplicate entry error.
|
| 168 |
+
|
| 169 |
+
$sql = "INSERT INTO tb_account_school_fee_payments (
|
| 170 |
+
id, fee_id, student_id, transaction_id, amount_paid,
|
| 171 |
+
teller_no, teller_name, credit_bank_id, debit_bank_id,
|
| 172 |
+
paymode_id, payment_status, payment_date, recipient_id,
|
| 173 |
+
academic_session, term_of_session, payment_by, payment_on
|
| 174 |
+
) VALUES (
|
| 175 |
+
:id, :fee_id, :student_id, :transaction_id, :amount_paid,
|
| 176 |
+
:teller_no, :teller_name, :credit_bank_id, :debit_bank_id,
|
| 177 |
+
:paymode_id, :payment_status, :payment_date, :recipient_id,
|
| 178 |
+
:academic_session, :term_of_session, :payment_by, NOW()
|
| 179 |
+
)";
|
| 180 |
+
|
| 181 |
+
$stmt = $pdo->prepare($sql);
|
| 182 |
+
$stmt->execute([
|
| 183 |
+
'id' => $schoolFeePaymentId,
|
| 184 |
+
'fee_id' => $allocation['fee_id'],
|
| 185 |
+
'student_id' => $studentId,
|
| 186 |
+
'transaction_id' => $transactionId,
|
| 187 |
+
'amount_paid' => $allocation['amount'],
|
| 188 |
+
'teller_no' => $tellerNo,
|
| 189 |
+
'teller_name' => $tellerName,
|
| 190 |
+
'credit_bank_id' => $creditBankId,
|
| 191 |
+
'debit_bank_id' => $debitBankId,
|
| 192 |
+
'paymode_id' => $paymodeId,
|
| 193 |
+
'payment_status' => $paymentStatus,
|
| 194 |
+
'payment_date' => $paymentDate,
|
| 195 |
+
'recipient_id' => $recipientId,
|
| 196 |
+
'academic_session' => $allocation['academic_session'],
|
| 197 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 198 |
+
'payment_by' => $paymentBy
|
| 199 |
+
]);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
// 3b) INSERT into tb_account_school_fee_sum_payments (single record)
|
| 203 |
+
$sumPaymentsId = $studentId . $dominantSession . $paymentDate;
|
| 204 |
+
|
| 205 |
+
$sql = "INSERT INTO tb_account_school_fee_sum_payments (
|
| 206 |
+
id, student_id, total_paid, paymode_id, payment_date,
|
| 207 |
+
credit_bank_id, debit_bank_id, transaction_id, status,
|
| 208 |
+
academic_session, term_of_session, registered_by, registered_on
|
| 209 |
+
) VALUES (
|
| 210 |
+
:id, :student_id, :total_paid, :paymode_id, :payment_date,
|
| 211 |
+
:credit_bank_id, :debit_bank_id, :transaction_id, :status,
|
| 212 |
+
:academic_session, :term_of_session, :registered_by, NOW()
|
| 213 |
+
)";
|
| 214 |
+
|
| 215 |
+
$stmt = $pdo->prepare($sql);
|
| 216 |
+
$stmt->execute([
|
| 217 |
+
'id' => $sumPaymentsId,
|
| 218 |
+
'student_id' => $studentId,
|
| 219 |
+
'total_paid' => $totalPaid,
|
| 220 |
+
'paymode_id' => $paymodeId,
|
| 221 |
+
'payment_date' => $paymentDate,
|
| 222 |
+
'credit_bank_id' => $creditBankId,
|
| 223 |
+
'debit_bank_id' => $debitBankId,
|
| 224 |
+
'transaction_id' => $transactionId,
|
| 225 |
+
'status' => $paymentStatus,
|
| 226 |
+
'academic_session' => $dominantSession,
|
| 227 |
+
'term_of_session' => $dominantTerm,
|
| 228 |
+
'registered_by' => $createdBy
|
| 229 |
+
]);
|
| 230 |
+
|
| 231 |
+
// 3c) INSERT into tb_account_student_payments (per fee)
|
| 232 |
+
foreach ($feeAllocations as $allocation) {
|
| 233 |
+
// Get current payment_to_date (sum of all previous payments for this fee/session/term)
|
| 234 |
+
$sql = "SELECT SUM(payment_to_date) AS current_total
|
| 235 |
+
FROM tb_account_student_payments
|
| 236 |
+
WHERE student_id = :student_id
|
| 237 |
+
AND fee_id = :fee_id
|
| 238 |
+
AND academic_session = :academic_session
|
| 239 |
+
AND term_of_session = :term_of_session";
|
| 240 |
+
|
| 241 |
+
$stmt = $pdo->prepare($sql);
|
| 242 |
+
$stmt->execute([
|
| 243 |
+
'student_id' => $studentId,
|
| 244 |
+
'fee_id' => $allocation['fee_id'],
|
| 245 |
+
'academic_session' => $allocation['academic_session'],
|
| 246 |
+
'term_of_session' => $allocation['term_of_session']
|
| 247 |
+
]);
|
| 248 |
+
|
| 249 |
+
$currentPayment = $stmt->fetch();
|
| 250 |
+
$currentTotal = floatval($currentPayment['current_total'] ?? 0);
|
| 251 |
+
$newPaymentToDate = $allocation['amount'];
|
| 252 |
+
//$newPaymentToDate = $currentTotal + $allocation['amount']; --Old code that added new payment amount to current total (total before payment)
|
| 253 |
+
|
| 254 |
+
$studentPaymentId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 255 |
+
|
| 256 |
+
$sql = "INSERT INTO tb_account_student_payments (
|
| 257 |
+
id, fee_id, student_id, payment_to_date, transaction_id,
|
| 258 |
+
academic_session, term_of_session, created_by, created_as, created_on
|
| 259 |
+
) VALUES (
|
| 260 |
+
:id, :fee_id, :student_id, :payment_to_date, :transaction_id,
|
| 261 |
+
:academic_session, :term_of_session, :created_by, :created_as, NOW()
|
| 262 |
+
)";
|
| 263 |
+
|
| 264 |
+
$stmt = $pdo->prepare($sql);
|
| 265 |
+
$stmt->execute([
|
| 266 |
+
'id' => $studentPaymentId,
|
| 267 |
+
'fee_id' => $allocation['fee_id'],
|
| 268 |
+
'student_id' => $studentId,
|
| 269 |
+
'payment_to_date' => $newPaymentToDate,
|
| 270 |
+
'transaction_id' => $transactionId,
|
| 271 |
+
'academic_session' => $allocation['academic_session'],
|
| 272 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 273 |
+
'created_by' => $createdBy,
|
| 274 |
+
'created_as' => $createdAs
|
| 275 |
+
]);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// 3d) INSERT into tb_account_payment_registers (per fee)
|
| 279 |
+
foreach ($feeAllocations as $allocation) {
|
| 280 |
+
$paymentRegisterId = $studentCode . $allocation['fee_id'] . $paymentDate;
|
| 281 |
+
|
| 282 |
+
$sql = "INSERT INTO tb_account_payment_registers (
|
| 283 |
+
id, fee_id, student_id, amount_paid, amount_due,
|
| 284 |
+
receipt_no, recipient_id, payment_date, paymode_category,
|
| 285 |
+
transaction_id, academic_session, term_of_session,
|
| 286 |
+
created_by, created_as, created_on
|
| 287 |
+
) VALUES (
|
| 288 |
+
:id, :fee_id, :student_id, :amount_paid, :amount_due,
|
| 289 |
+
:receipt_no, :recipient_id, :payment_date, :paymode_category,
|
| 290 |
+
:transaction_id, :academic_session, :term_of_session,
|
| 291 |
+
:created_by, :created_as, NOW()
|
| 292 |
+
)";
|
| 293 |
+
|
| 294 |
+
$stmt = $pdo->prepare($sql);
|
| 295 |
+
$stmt->execute([
|
| 296 |
+
'id' => $paymentRegisterId,
|
| 297 |
+
'fee_id' => $allocation['fee_id'],
|
| 298 |
+
'student_id' => $studentId,
|
| 299 |
+
'amount_paid' => $allocation['amount'],
|
| 300 |
+
'amount_due' => $allocation['amount'],
|
| 301 |
+
'receipt_no' => $receiptNo,
|
| 302 |
+
'recipient_id' => $recipientId,
|
| 303 |
+
'payment_date' => $paymentDate,
|
| 304 |
+
'paymode_category' => $paymodeCategory,
|
| 305 |
+
'transaction_id' => $transactionId,
|
| 306 |
+
'academic_session' => $allocation['academic_session'],
|
| 307 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 308 |
+
'created_by' => $createdBy,
|
| 309 |
+
'created_as' => $createdAs
|
| 310 |
+
]);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
// 3e) UPDATE tb_student_logistics
|
| 314 |
+
// Group allocations by session/term to update specific records
|
| 315 |
+
$sessionTermTotals = [];
|
| 316 |
+
foreach ($feeAllocations as $allocation) {
|
| 317 |
+
$key = $allocation['academic_session'] . '-' . $allocation['term_of_session'];
|
| 318 |
+
if (!isset($sessionTermTotals[$key])) {
|
| 319 |
+
$sessionTermTotals[$key] = [
|
| 320 |
+
'academic_session' => $allocation['academic_session'],
|
| 321 |
+
'term_of_session' => $allocation['term_of_session'],
|
| 322 |
+
'total' => 0
|
| 323 |
+
];
|
| 324 |
+
}
|
| 325 |
+
$sessionTermTotals[$key]['total'] += $allocation['amount'];
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
// Update each session/term record
|
| 329 |
+
foreach ($sessionTermTotals as $st) {
|
| 330 |
+
$sql = "UPDATE tb_student_logistics
|
| 331 |
+
SET fees_outstanding = GREATEST(0, fees_outstanding - :total_paid)
|
| 332 |
+
WHERE student_id = :student_id
|
| 333 |
+
AND academic_session = :academic_session
|
| 334 |
+
AND term_of_session = :term_of_session";
|
| 335 |
+
|
| 336 |
+
$stmt = $pdo->prepare($sql);
|
| 337 |
+
$stmt->execute([
|
| 338 |
+
'total_paid' => $st['total'],
|
| 339 |
+
'student_id' => $studentId,
|
| 340 |
+
'academic_session' => $st['academic_session'],
|
| 341 |
+
'term_of_session' => $st['term_of_session']
|
| 342 |
+
]);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// Commit transaction
|
| 346 |
+
$pdo->commit();
|
| 347 |
+
|
| 348 |
+
$success = true;
|
| 349 |
+
$message = 'Payment processed successfully!';
|
| 350 |
+
|
| 351 |
+
// Fetch fee descriptions for display
|
| 352 |
+
$feeIds = array_column($feeAllocations, 'fee_id');
|
| 353 |
+
$placeholders = implode(',', array_fill(0, count($feeIds), '?'));
|
| 354 |
+
|
| 355 |
+
$sql = "SELECT id, description FROM tb_account_school_fees WHERE id IN ($placeholders)";
|
| 356 |
+
$stmt = $pdo->prepare($sql);
|
| 357 |
+
$stmt->execute($feeIds);
|
| 358 |
+
$feeDescriptions = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
| 359 |
+
|
| 360 |
+
// Prepare payment details for display
|
| 361 |
+
foreach ($feeAllocations as $key => $allocation) {
|
| 362 |
+
$feeAllocations[$key]['description'] = $feeDescriptions[$allocation['fee_id']] ?? 'Unknown Fee';
|
| 363 |
+
}
|
| 364 |
+
$paymentDetails = [
|
| 365 |
+
'student_code' => $studentCode,
|
| 366 |
+
'payment_date' => $paymentDate,
|
| 367 |
+
'teller_no' => $tellerNo,
|
| 368 |
+
'teller_name' => $tellerName,
|
| 369 |
+
'total_paid' => $totalPaid,
|
| 370 |
+
'receipt_no' => $receiptNo,
|
| 371 |
+
'transaction_id' => $transactionId,
|
| 372 |
+
'allocations' => $feeAllocations,
|
| 373 |
+
'remaining_unreconciled' => $bankStatement['unreconciled_amount'] - $totalPaid
|
| 374 |
+
];
|
| 375 |
+
|
| 376 |
+
} catch (Exception $e) {
|
| 377 |
+
$pdo->rollBack();
|
| 378 |
+
throw new Exception('Transaction failed: ' . $e->getMessage());
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
} catch (Exception $e) {
|
| 382 |
+
$success = false;
|
| 383 |
+
$message = $e->getMessage();
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// Fetch student name for display
|
| 387 |
+
$studentName = '';
|
| 388 |
+
if (!empty($studentId)) {
|
| 389 |
+
try {
|
| 390 |
+
$sql = "SELECT CONCAT(last_name, ' ', first_name, ' ', COALESCE(other_name, '')) AS full_name
|
| 391 |
+
FROM tb_student_registrations
|
| 392 |
+
WHERE id = :student_id";
|
| 393 |
+
$stmt = $pdo->prepare($sql);
|
| 394 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 395 |
+
$result = $stmt->fetch();
|
| 396 |
+
$studentName = $result['full_name'] ?? 'Unknown Student';
|
| 397 |
+
} catch (Exception $e) {
|
| 398 |
+
$studentName = 'Unknown Student';
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
?>
|
| 402 |
+
<!DOCTYPE html>
|
| 403 |
+
<html lang="en">
|
| 404 |
+
|
| 405 |
+
<head>
|
| 406 |
+
<meta charset="UTF-8">
|
| 407 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 408 |
+
<title>Payment Processing Result</title>
|
| 409 |
+
<style>
|
| 410 |
+
* {
|
| 411 |
+
margin: 0;
|
| 412 |
+
padding: 0;
|
| 413 |
+
box-sizing: border-box;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
body {
|
| 417 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 418 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 419 |
+
min-height: 100vh;
|
| 420 |
+
padding: 20px;
|
| 421 |
+
display: flex;
|
| 422 |
+
align-items: center;
|
| 423 |
+
justify-content: center;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.container {
|
| 427 |
+
max-width: 800px;
|
| 428 |
+
width: 100%;
|
| 429 |
+
background: white;
|
| 430 |
+
border-radius: 12px;
|
| 431 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
| 432 |
+
padding: 40px;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.success-icon {
|
| 436 |
+
width: 80px;
|
| 437 |
+
height: 80px;
|
| 438 |
+
margin: 0 auto 20px;
|
| 439 |
+
background: #28a745;
|
| 440 |
+
border-radius: 50%;
|
| 441 |
+
display: flex;
|
| 442 |
+
align-items: center;
|
| 443 |
+
justify-content: center;
|
| 444 |
+
color: white;
|
| 445 |
+
font-size: 48px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.error-icon {
|
| 449 |
+
width: 80px;
|
| 450 |
+
height: 80px;
|
| 451 |
+
margin: 0 auto 20px;
|
| 452 |
+
background: #dc3545;
|
| 453 |
+
border-radius: 50%;
|
| 454 |
+
display: flex;
|
| 455 |
+
align-items: center;
|
| 456 |
+
justify-content: center;
|
| 457 |
+
color: white;
|
| 458 |
+
font-size: 48px;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
h1 {
|
| 462 |
+
text-align: center;
|
| 463 |
+
color: #333;
|
| 464 |
+
margin-bottom: 10px;
|
| 465 |
+
font-size: 28px;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.message {
|
| 469 |
+
text-align: center;
|
| 470 |
+
color: #666;
|
| 471 |
+
margin-bottom: 30px;
|
| 472 |
+
font-size: 16px;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.details-section {
|
| 476 |
+
background: #f8f9fa;
|
| 477 |
+
padding: 20px;
|
| 478 |
+
border-radius: 8px;
|
| 479 |
+
margin-bottom: 20px;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
.details-section h2 {
|
| 483 |
+
color: #333;
|
| 484 |
+
margin-bottom: 15px;
|
| 485 |
+
font-size: 20px;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.detail-row {
|
| 489 |
+
display: flex;
|
| 490 |
+
justify-content: space-between;
|
| 491 |
+
padding: 10px 0;
|
| 492 |
+
border-bottom: 1px solid #ddd;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.detail-row:last-child {
|
| 496 |
+
border-bottom: none;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.detail-label {
|
| 500 |
+
font-weight: 600;
|
| 501 |
+
color: #555;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.detail-value {
|
| 505 |
+
color: #333;
|
| 506 |
+
text-align: right;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
table {
|
| 510 |
+
width: 100%;
|
| 511 |
+
border-collapse: collapse;
|
| 512 |
+
margin-top: 15px;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
th,
|
| 516 |
+
td {
|
| 517 |
+
padding: 12px;
|
| 518 |
+
text-align: left;
|
| 519 |
+
border-bottom: 1px solid #ddd;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
th {
|
| 523 |
+
background-color: #667eea;
|
| 524 |
+
color: white;
|
| 525 |
+
font-weight: 600;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.amount {
|
| 529 |
+
text-align: right;
|
| 530 |
+
font-family: 'Courier New', monospace;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.total-row {
|
| 534 |
+
font-weight: bold;
|
| 535 |
+
background-color: #f0f0f0;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.btn {
|
| 539 |
+
display: inline-block;
|
| 540 |
+
padding: 12px 30px;
|
| 541 |
+
border: none;
|
| 542 |
+
border-radius: 6px;
|
| 543 |
+
font-size: 16px;
|
| 544 |
+
font-weight: 600;
|
| 545 |
+
cursor: pointer;
|
| 546 |
+
text-decoration: none;
|
| 547 |
+
transition: all 0.3s;
|
| 548 |
+
text-align: center;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.btn-primary {
|
| 552 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 553 |
+
color: white;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
.btn-primary:hover {
|
| 557 |
+
transform: translateY(-2px);
|
| 558 |
+
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.actions {
|
| 562 |
+
text-align: center;
|
| 563 |
+
margin-top: 30px;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.highlight {
|
| 567 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 568 |
+
color: white;
|
| 569 |
+
padding: 15px;
|
| 570 |
+
border-radius: 8px;
|
| 571 |
+
margin-bottom: 20px;
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
.highlight p {
|
| 575 |
+
margin: 5px 0;
|
| 576 |
+
}
|
| 577 |
+
</style>
|
| 578 |
+
</head>
|
| 579 |
+
|
| 580 |
+
<body>
|
| 581 |
+
<div class="container">
|
| 582 |
+
<?php if ($success): ?>
|
| 583 |
+
<div class="success-icon">✓</div>
|
| 584 |
+
<h1>Payment Processed Successfully!</h1>
|
| 585 |
+
<p class="message"><?php echo htmlspecialchars($message); ?></p>
|
| 586 |
+
|
| 587 |
+
<div class="highlight">
|
| 588 |
+
<p><strong>Student:</strong> <?php echo htmlspecialchars($studentName); ?></p>
|
| 589 |
+
<p><strong>Receipt No:</strong> <?php echo htmlspecialchars($paymentDetails['receipt_no']); ?></p>
|
| 590 |
+
<p><strong>Transaction ID:</strong> <?php echo htmlspecialchars($paymentDetails['transaction_id']); ?></p>
|
| 591 |
+
</div>
|
| 592 |
+
|
| 593 |
+
<div class="details-section">
|
| 594 |
+
<h2>Payment Details</h2>
|
| 595 |
+
<div class="detail-row">
|
| 596 |
+
<span class="detail-label">Payment Date:</span>
|
| 597 |
+
<span class="detail-value"><?php echo htmlspecialchars($paymentDetails['payment_date']); ?></span>
|
| 598 |
+
</div>
|
| 599 |
+
<div class="detail-row">
|
| 600 |
+
<span class="detail-label">Teller Number:</span>
|
| 601 |
+
<span class="detail-value"><?php echo htmlspecialchars($paymentDetails['teller_no']); ?></span>
|
| 602 |
+
</div>
|
| 603 |
+
<div class="detail-row">
|
| 604 |
+
<span class="detail-label">Bank Narration:</span>
|
| 605 |
+
<span class="detail-value"><?php echo htmlspecialchars($paymentDetails['teller_name']); ?></span>
|
| 606 |
+
</div>
|
| 607 |
+
<div class="detail-row">
|
| 608 |
+
<span class="detail-label">Total Amount Used:</span>
|
| 609 |
+
<span class="detail-value">₦<?php echo number_format($paymentDetails['total_paid'], 2); ?></span>
|
| 610 |
+
</div>
|
| 611 |
+
<div class="detail-row">
|
| 612 |
+
<span class="detail-label">Remaining Unreconciled on Teller:</span>
|
| 613 |
+
<span
|
| 614 |
+
class="detail-value">₦<?php echo number_format($paymentDetails['remaining_unreconciled'], 2); ?></span>
|
| 615 |
+
</div>
|
| 616 |
+
</div>
|
| 617 |
+
|
| 618 |
+
<div class="details-section">
|
| 619 |
+
<h2>Fees Settled</h2>
|
| 620 |
+
<table>
|
| 621 |
+
<thead>
|
| 622 |
+
<tr>
|
| 623 |
+
<th>Fee Description</th>
|
| 624 |
+
<th width="100">Session</th>
|
| 625 |
+
<th width="80">Term</th>
|
| 626 |
+
<th width="120" class="amount">Amount Paid</th>
|
| 627 |
+
</tr>
|
| 628 |
+
</thead>
|
| 629 |
+
<tbody>
|
| 630 |
+
<?php foreach ($paymentDetails['allocations'] as $allocation): ?>
|
| 631 |
+
<tr>
|
| 632 |
+
<td><?php echo htmlspecialchars($allocation['description']); ?></td>
|
| 633 |
+
<td><?php echo htmlspecialchars($allocation['academic_session']); ?></td>
|
| 634 |
+
<td><?php echo htmlspecialchars($allocation['term_of_session']); ?></td>
|
| 635 |
+
<td class="amount">₦<?php echo number_format($allocation['amount'], 2); ?></td>
|
| 636 |
+
</tr>
|
| 637 |
+
<?php endforeach; ?>
|
| 638 |
+
<tr class="total-row">
|
| 639 |
+
<td colspan="3" style="text-align: right;">Total:</td>
|
| 640 |
+
<td class="amount">₦<?php echo number_format($paymentDetails['total_paid'], 2); ?></td>
|
| 641 |
+
</tr>
|
| 642 |
+
</tbody>
|
| 643 |
+
</table>
|
| 644 |
+
</div>
|
| 645 |
+
|
| 646 |
+
<?php else: ?>
|
| 647 |
+
<div class="error-icon">✗</div>
|
| 648 |
+
<h1>Payment Processing Failed</h1>
|
| 649 |
+
<p class="message" style="color: #dc3545;"><?php echo htmlspecialchars($message); ?></p>
|
| 650 |
+
|
| 651 |
+
<div class="details-section">
|
| 652 |
+
<h2>Error Details</h2>
|
| 653 |
+
<p>The payment could not be processed. No changes were made to the database.</p>
|
| 654 |
+
<p style="margin-top: 10px;"><strong>Error:</strong> <?php echo htmlspecialchars($message); ?></p>
|
| 655 |
+
</div>
|
| 656 |
+
<?php endif; ?>
|
| 657 |
+
|
| 658 |
+
<div class="actions">
|
| 659 |
+
<a href="index.php" class="btn btn-primary">Return to Main Page</a>
|
| 660 |
+
<?php if ($success && !empty($paymentDetails['receipt_no'])): ?>
|
| 661 |
+
<a href="download_receipt.php?receipt_no=<?php echo urlencode($paymentDetails['receipt_no']); ?>"
|
| 662 |
+
class="btn btn-primary" style="background: #28a745; margin-left: 10px;">Download Receipt</a>
|
| 663 |
+
<?php endif; ?>
|
| 664 |
+
</div>
|
| 665 |
+
</div>
|
| 666 |
+
</body>
|
| 667 |
+
|
| 668 |
+
</html>
|