Spaces:
Running
Running
Upload 25 files
Browse files- easypay-api/.gemini/RECEIPT_FIX_SUMMARY.md +101 -0
- easypay-api/README.md +204 -0
- easypay-api/ajax_handlers.php +122 -0
- easypay-api/api/.htaccess +26 -0
- easypay-api/api/API_DOCUMENTATION.md +667 -0
- easypay-api/api/LAST_RECEIPT_ENDPOINT.md +221 -0
- easypay-api/api/README.md +396 -0
- easypay-api/api/payments/process.php +340 -0
- easypay-api/api/students/balance.php +63 -0
- easypay-api/api/students/invoice.php +74 -0
- easypay-api/api/students/last_receipt.php +209 -0
- easypay-api/api/students/payment_status.php +69 -0
- easypay-api/api/test.html +350 -0
- easypay-api/api/test_receipt.html +322 -0
- easypay-api/assets/logo.png +0 -0
- easypay-api/config/api_config.php +30 -0
- easypay-api/db_config.php +27 -0
- easypay-api/download_receipt.php +134 -0
- easypay-api/includes/ApiValidator.php +293 -0
- easypay-api/includes/PaymentProcessor.php +691 -0
- easypay-api/includes/ReceiptGenerator.php +283 -0
- easypay-api/index.php +711 -0
- easypay-api/logs/api.log +3 -0
- easypay-api/process_payment - v0.php +663 -0
- easypay-api/process_payment.php +668 -0
easypay-api/.gemini/RECEIPT_FIX_SUMMARY.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Receipt Generation Fix - API vs Web Interface
|
| 2 |
+
|
| 3 |
+
## Issue Identified
|
| 4 |
+
|
| 5 |
+
When payments were processed through the API endpoint (`POST /api/payments/process`), the generated receipts only displayed the fee items that received payment in that specific transaction. This differed from the web interface behavior, where receipts showed **all** fee items for the term with their complete financial status (amount billed, paid to date, and outstanding balance).
|
| 6 |
+
|
| 7 |
+
This discrepancy left parents with an incomplete and inaccurate picture of their outstanding fees when using the API.
|
| 8 |
+
|
| 9 |
+
## Root Cause
|
| 10 |
+
|
| 11 |
+
### Web Interface (`download_receipt.php`)
|
| 12 |
+
- Fetches **ALL fees** for the student from `tb_account_receivables`
|
| 13 |
+
- Calculates cumulative "paid to date" for each fee up to the receipt date
|
| 14 |
+
- Displays all fees with either:
|
| 15 |
+
- Outstanding balance > 0, OR
|
| 16 |
+
- Payment made in that receipt > 0
|
| 17 |
+
- Shows complete columns: Fee Description, Term/Session, Amount Billed, Amount Paid, Paid To Date, To Balance
|
| 18 |
+
|
| 19 |
+
### API Endpoint (`api/payments/process.php` - BEFORE FIX)
|
| 20 |
+
- Used only the `allocations` array returned by `PaymentProcessor::processPayment()`
|
| 21 |
+
- This array contained **only** the fees that received payment in the current transaction
|
| 22 |
+
- Did not show other outstanding fees or complete financial picture
|
| 23 |
+
- Parents could not see their total outstanding balance
|
| 24 |
+
|
| 25 |
+
## Solution Implemented
|
| 26 |
+
|
| 27 |
+
Modified `api/payments/process.php` (lines 206-298) to use the **same comprehensive receipt generation logic** as the web interface:
|
| 28 |
+
|
| 29 |
+
### Changes Made:
|
| 30 |
+
1. **Fetch All Student Fees**: Query `tb_account_receivables` to get all fees billed to the student
|
| 31 |
+
2. **Calculate Paid To Date**: For each fee, sum all payments made up to the receipt date from `tb_account_payment_registers`
|
| 32 |
+
3. **Calculate Amount Paid in This Receipt**: Determine how much was paid for each fee in this specific transaction
|
| 33 |
+
4. **Calculate Balance**: Compute outstanding balance for each fee (amount billed - paid to date)
|
| 34 |
+
5. **Filter Display**: Show fees that either have:
|
| 35 |
+
- Outstanding balance > 0, OR
|
| 36 |
+
- Payment made in this receipt > 0
|
| 37 |
+
6. **Generate Complete Receipt**: Pass all fee information to `ReceiptGenerator` with proper structure
|
| 38 |
+
|
| 39 |
+
### Key Code Changes:
|
| 40 |
+
```php
|
| 41 |
+
// OLD (Incomplete)
|
| 42 |
+
$result['data']['student_name'] = $student['full_name'];
|
| 43 |
+
$generator = new ReceiptGenerator();
|
| 44 |
+
$receiptBase64 = $generator->generateBase64($result['data']);
|
| 45 |
+
|
| 46 |
+
// NEW (Complete)
|
| 47 |
+
// Fetch ALL fees from receivables
|
| 48 |
+
$sqlFees = "SELECT ar.fee_id, ar.actual_value as amount_billed,
|
| 49 |
+
ar.academic_session, ar.term_of_session,
|
| 50 |
+
sf.description as fee_description
|
| 51 |
+
FROM tb_account_receivables ar
|
| 52 |
+
JOIN tb_account_school_fees sf ON ar.fee_id = sf.id
|
| 53 |
+
WHERE ar.student_id = :sid
|
| 54 |
+
ORDER BY ar.academic_session ASC, ar.term_of_session ASC";
|
| 55 |
+
|
| 56 |
+
// Calculate paid to date and balances for each fee
|
| 57 |
+
// Build comprehensive allocations array
|
| 58 |
+
// Generate receipt with complete fee information
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## Impact
|
| 62 |
+
|
| 63 |
+
### Before Fix:
|
| 64 |
+
- API receipts showed only 2-3 fees (those paid in transaction)
|
| 65 |
+
- Parents couldn't see total outstanding balance
|
| 66 |
+
- Incomplete financial picture
|
| 67 |
+
- Inconsistent with web interface
|
| 68 |
+
|
| 69 |
+
### After Fix:
|
| 70 |
+
- API receipts show ALL relevant fees (matching web interface)
|
| 71 |
+
- Complete financial transparency:
|
| 72 |
+
- All fees billed for the term
|
| 73 |
+
- Amount paid in this transaction
|
| 74 |
+
- Cumulative paid to date
|
| 75 |
+
- Outstanding balance for each fee
|
| 76 |
+
- Parents have accurate picture of total outstanding fees
|
| 77 |
+
- Consistent behavior across web and API interfaces
|
| 78 |
+
|
| 79 |
+
## Testing Recommendations
|
| 80 |
+
|
| 81 |
+
1. **Test API Payment**: Process a payment via API that partially pays multiple fees
|
| 82 |
+
2. **Verify Receipt**: Check that the generated receipt shows:
|
| 83 |
+
- All fees for the student (not just paid ones)
|
| 84 |
+
- Correct "Amount Paid" column (only for fees paid in this transaction)
|
| 85 |
+
- Correct "Paid To Date" column (cumulative payments)
|
| 86 |
+
- Correct "To Balance" column (outstanding amounts)
|
| 87 |
+
3. **Compare with Web**: Process a similar payment via web interface and compare receipts
|
| 88 |
+
4. **Edge Cases**:
|
| 89 |
+
- Payment that fully settles some fees but not others
|
| 90 |
+
- Payment that partially pays multiple fees
|
| 91 |
+
- Student with fees from multiple terms/sessions
|
| 92 |
+
|
| 93 |
+
## Files Modified
|
| 94 |
+
|
| 95 |
+
- `c:\xampp\htdocs\easypay\api\payments\process.php` (lines 206-298)
|
| 96 |
+
|
| 97 |
+
## Files Referenced (No Changes)
|
| 98 |
+
|
| 99 |
+
- `c:\xampp\htdocs\easypay\download_receipt.php` (used as reference for correct behavior)
|
| 100 |
+
- `c:\xampp\htdocs\easypay\includes\ReceiptGenerator.php` (receipt rendering logic)
|
| 101 |
+
- `c:\xampp\htdocs\easypay\includes\PaymentProcessor.php` (payment processing logic)
|
easypay-api/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-api/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/api/.htaccess
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 11 |
+
# Route /api/students/...
|
| 12 |
+
RewriteRule ^students/balance$ students/balance.php [L]
|
| 13 |
+
RewriteRule ^students/invoice$ students/invoice.php [L]
|
| 14 |
+
RewriteRule ^students/payment_status$ students/payment_status.php [L]
|
| 15 |
+
</IfModule>
|
| 16 |
+
|
| 17 |
+
# Security headers
|
| 18 |
+
<IfModule mod_headers.c>
|
| 19 |
+
# Prevent directory listing
|
| 20 |
+
Options -Indexes
|
| 21 |
+
|
| 22 |
+
# Security headers
|
| 23 |
+
Header set X-Content-Type-Options "nosniff"
|
| 24 |
+
Header set X-Frame-Options "DENY"
|
| 25 |
+
Header set X-XSS-Protection "1; mode=block"
|
| 26 |
+
</IfModule>
|
easypay-api/api/API_DOCUMENTATION.md
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
{
|
| 168 |
+
"status": "error",
|
| 169 |
+
"message": "Internal server error"
|
| 170 |
+
}
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
### Student Information Endpoints
|
| 174 |
+
|
| 175 |
+
#### 1. Get Outstanding Balance
|
| 176 |
+
|
| 177 |
+
**GET** `/api/students/balance`
|
| 178 |
+
|
| 179 |
+
Fetches a student's total outstanding balance and a breakdown of unpaid fees.
|
| 180 |
+
|
| 181 |
+
**Parameters:**
|
| 182 |
+
|
| 183 |
+
| Parameter | Type | Required | Description |
|
| 184 |
+
|-----------|------|----------|-------------|
|
| 185 |
+
| `student_id` | string | Yes | The ID of the student |
|
| 186 |
+
|
| 187 |
+
**Success Response:**
|
| 188 |
+
|
| 189 |
+
```json
|
| 190 |
+
{
|
| 191 |
+
"status": "success",
|
| 192 |
+
"data": {
|
| 193 |
+
"student_id": "000001234567890123",
|
| 194 |
+
"currency": "NGN",
|
| 195 |
+
"total_outstanding": 50000,
|
| 196 |
+
"breakdown": [
|
| 197 |
+
{
|
| 198 |
+
"fee_id": "...",
|
| 199 |
+
"fee_description": "Tuition Fee",
|
| 200 |
+
"academic_session": "2024",
|
| 201 |
+
"term_of_session": "1",
|
| 202 |
+
"amount_billed": 100000,
|
| 203 |
+
"amount_paid": 50000,
|
| 204 |
+
"outstanding_amount": 50000
|
| 205 |
+
}
|
| 206 |
+
]
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
#### 2. Get Term Invoice
|
| 212 |
+
|
| 213 |
+
**GET** `/api/students/invoice`
|
| 214 |
+
|
| 215 |
+
Fetches the breakdown of fees billed to a student for a specific term and session.
|
| 216 |
+
|
| 217 |
+
**Parameters:**
|
| 218 |
+
|
| 219 |
+
| Parameter | Type | Required | Description |
|
| 220 |
+
|-----------|------|----------|-------------|
|
| 221 |
+
| `student_id` | string | Yes | The ID of the student |
|
| 222 |
+
| `academic_session` | string | Yes | The accounting year (e.g. "2024") |
|
| 223 |
+
| `term` | string | Yes | The term number (e.g. "1") |
|
| 224 |
+
|
| 225 |
+
**Success Response:**
|
| 226 |
+
|
| 227 |
+
```json
|
| 228 |
+
{
|
| 229 |
+
"status": "success",
|
| 230 |
+
"data": {
|
| 231 |
+
"student_id": "...",
|
| 232 |
+
"academic_session": "2024",
|
| 233 |
+
"term": "1",
|
| 234 |
+
"items": [
|
| 235 |
+
{
|
| 236 |
+
"description": "Tuition Fee",
|
| 237 |
+
"amount_billed": 100000,
|
| 238 |
+
"amount_paid": 100000,
|
| 239 |
+
"balance": 0
|
| 240 |
+
}
|
| 241 |
+
]
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
#### 3. Get Term Payment Status
|
| 247 |
+
|
| 248 |
+
**GET** `/api/students/payment_status`
|
| 249 |
+
|
| 250 |
+
Gets a copy of the payment status for a term, including summary and transaction history.
|
| 251 |
+
|
| 252 |
+
**Parameters:**
|
| 253 |
+
|
| 254 |
+
| Parameter | Type | Required | Description |
|
| 255 |
+
|-----------|------|----------|-------------|
|
| 256 |
+
| `student_id` | string | Yes | The ID of the student |
|
| 257 |
+
| `academic_session` | string | Yes | The accounting year |
|
| 258 |
+
| `term` | string | Yes | The term number |
|
| 259 |
+
|
| 260 |
+
**Success Response:**
|
| 261 |
+
|
| 262 |
+
```json
|
| 263 |
+
{
|
| 264 |
+
"status": "success",
|
| 265 |
+
"data": {
|
| 266 |
+
"summary": {
|
| 267 |
+
"total_billed": 150000,
|
| 268 |
+
"total_paid": 100000,
|
| 269 |
+
"outstanding": 50000
|
| 270 |
+
},
|
| 271 |
+
"transactions": [
|
| 272 |
+
{
|
| 273 |
+
"payment_date": "2026-01-15",
|
| 274 |
+
"amount": 50000,
|
| 275 |
+
"transaction_id": "...",
|
| 276 |
+
"payment_ref": "..."
|
| 277 |
+
}
|
| 278 |
+
]
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
```
|
| 282 |
+
|
| 283 |
+
#### 4. Get Last Payment Receipt Image
|
| 284 |
+
|
| 285 |
+
**GET** `/api/students/last_receipt`
|
| 286 |
+
|
| 287 |
+
Returns the receipt image (PNG format) of the most recent payment made by a student.
|
| 288 |
+
|
| 289 |
+
**Parameters:**
|
| 290 |
+
|
| 291 |
+
| Parameter | Type | Required | Description |
|
| 292 |
+
|-----------|------|----------|-------------|
|
| 293 |
+
| `student_id` | string | Yes | The ID of the student |
|
| 294 |
+
|
| 295 |
+
**Success Response:**
|
| 296 |
+
|
| 297 |
+
- **Content-Type:** `image/png`
|
| 298 |
+
- **Body:** Binary PNG image data
|
| 299 |
+
- The image can be saved directly to a file or displayed in an application
|
| 300 |
+
|
| 301 |
+
**Error Responses:**
|
| 302 |
+
|
| 303 |
+
```json
|
| 304 |
+
// Student ID missing
|
| 305 |
+
{
|
| 306 |
+
"status": "error",
|
| 307 |
+
"message": "Student ID is required"
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// Student not found
|
| 311 |
+
{
|
| 312 |
+
"status": "error",
|
| 313 |
+
"message": "Student not found or inactive"
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// No payment records
|
| 317 |
+
{
|
| 318 |
+
"status": "error",
|
| 319 |
+
"message": "No payment records found for this student"
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// Server error
|
| 323 |
+
{
|
| 324 |
+
"status": "error",
|
| 325 |
+
"message": "Error generating receipt: [error details]"
|
| 326 |
+
}
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
**Example Requests:**
|
| 330 |
+
|
| 331 |
+
**cURL:**
|
| 332 |
+
```bash
|
| 333 |
+
# Download receipt to file
|
| 334 |
+
curl -X GET "http://your-domain.com/easypay/api/students/last_receipt?student_id=000001234567890123" \
|
| 335 |
+
--output receipt.png
|
| 336 |
+
|
| 337 |
+
# With API authentication
|
| 338 |
+
curl -X GET "http://your-domain.com/easypay/api/students/last_receipt?student_id=000001234567890123" \
|
| 339 |
+
-H "Authorization: Bearer your-api-key-here" \
|
| 340 |
+
--output receipt.png
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
**PHP:**
|
| 344 |
+
```php
|
| 345 |
+
<?php
|
| 346 |
+
$studentId = '000001234567890123';
|
| 347 |
+
$url = "http://your-domain.com/easypay/api/students/last_receipt?student_id={$studentId}";
|
| 348 |
+
|
| 349 |
+
$context = stream_context_create([
|
| 350 |
+
'http' => [
|
| 351 |
+
'header' => 'Authorization: Bearer your-api-key-here'
|
| 352 |
+
]
|
| 353 |
+
]);
|
| 354 |
+
|
| 355 |
+
$imageData = file_get_contents($url, false, $context);
|
| 356 |
+
|
| 357 |
+
if ($imageData !== false) {
|
| 358 |
+
// Save to file
|
| 359 |
+
file_put_contents('receipt.png', $imageData);
|
| 360 |
+
|
| 361 |
+
// Or display in browser
|
| 362 |
+
header('Content-Type: image/png');
|
| 363 |
+
echo $imageData;
|
| 364 |
+
} else {
|
| 365 |
+
echo "Error fetching receipt";
|
| 366 |
+
}
|
| 367 |
+
?>
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
**JavaScript (Fetch API):**
|
| 371 |
+
```javascript
|
| 372 |
+
const studentId = '000001234567890123';
|
| 373 |
+
const url = `http://your-domain.com/easypay/api/students/last_receipt?student_id=${studentId}`;
|
| 374 |
+
|
| 375 |
+
fetch(url, {
|
| 376 |
+
headers: {
|
| 377 |
+
'Authorization': 'Bearer your-api-key-here'
|
| 378 |
+
}
|
| 379 |
+
})
|
| 380 |
+
.then(response => {
|
| 381 |
+
if (response.ok) {
|
| 382 |
+
return response.blob();
|
| 383 |
+
} else {
|
| 384 |
+
return response.json().then(err => {
|
| 385 |
+
throw new Error(err.message);
|
| 386 |
+
});
|
| 387 |
+
}
|
| 388 |
+
})
|
| 389 |
+
.then(blob => {
|
| 390 |
+
// Create download link
|
| 391 |
+
const url = window.URL.createObjectURL(blob);
|
| 392 |
+
const a = document.createElement('a');
|
| 393 |
+
a.href = url;
|
| 394 |
+
a.download = 'receipt.png';
|
| 395 |
+
a.click();
|
| 396 |
+
|
| 397 |
+
// Or display in img tag
|
| 398 |
+
// document.getElementById('receiptImage').src = url;
|
| 399 |
+
})
|
| 400 |
+
.catch(error => console.error('Error:', error));
|
| 401 |
+
```
|
| 402 |
+
|
| 403 |
+
**Python:**
|
| 404 |
+
```python
|
| 405 |
+
import requests
|
| 406 |
+
|
| 407 |
+
student_id = '000001234567890123'
|
| 408 |
+
url = f'http://your-domain.com/easypay/api/students/last_receipt?student_id={student_id}'
|
| 409 |
+
|
| 410 |
+
headers = {
|
| 411 |
+
'Authorization': 'Bearer your-api-key-here'
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
response = requests.get(url, headers=headers)
|
| 415 |
+
|
| 416 |
+
if response.status_code == 200:
|
| 417 |
+
# Save to file
|
| 418 |
+
with open('receipt.png', 'wb') as f:
|
| 419 |
+
f.write(response.content)
|
| 420 |
+
print("Receipt downloaded successfully")
|
| 421 |
+
else:
|
| 422 |
+
# Handle error
|
| 423 |
+
error = response.json()
|
| 424 |
+
print(f"Error: {error['message']}")
|
| 425 |
+
```
|
| 426 |
+
|
| 427 |
+
## Payment Allocation Logic
|
| 428 |
+
|
| 429 |
+
The API automatically allocates payments using the following rules:
|
| 430 |
+
|
| 431 |
+
1. **Automatic Fee Selection**: The system fetches all outstanding fees for the student
|
| 432 |
+
2. **Oldest First**: Fees are sorted by academic session (ascending), then term (ascending)
|
| 433 |
+
3. **Full Settlement Priority**: Each fee is fully settled before moving to the next
|
| 434 |
+
4. **Partial Settlement**: If the payment amount runs out, the last fee is partially settled
|
| 435 |
+
5. **Transaction Integrity**: All database operations are wrapped in a transaction (all-or-nothing)
|
| 436 |
+
|
| 437 |
+
### Example
|
| 438 |
+
|
| 439 |
+
If a student has these outstanding fees:
|
| 440 |
+
- 2024 Term 1 Tuition: ₦30,000
|
| 441 |
+
- 2024 Term 2 Tuition: ₦30,000
|
| 442 |
+
- 2024 Term 3 Tuition: ₦30,000
|
| 443 |
+
|
| 444 |
+
And you send a payment of ₦50,000:
|
| 445 |
+
- 2024 Term 1: Fully settled (₦30,000)
|
| 446 |
+
- 2024 Term 2: Partially settled (₦20,000)
|
| 447 |
+
- 2024 Term 3: Not settled
|
| 448 |
+
|
| 449 |
+
## Validation Rules
|
| 450 |
+
|
| 451 |
+
### Student Validation
|
| 452 |
+
- Student ID must exist in `tb_student_registrations`
|
| 453 |
+
- Student must have `admission_status = 'Active'`
|
| 454 |
+
- Student must have outstanding fees
|
| 455 |
+
|
| 456 |
+
### Teller Validation
|
| 457 |
+
- Teller number must exist in `tb_account_bank_statements`
|
| 458 |
+
- Teller must have unreconciled amount > 0
|
| 459 |
+
- The teller number is extracted as the last word in the `description` field
|
| 460 |
+
|
| 461 |
+
### Amount Validation
|
| 462 |
+
- Amount must be a positive number
|
| 463 |
+
- Amount must not exceed the unreconciled amount on the teller
|
| 464 |
+
- Amount must not be zero or negative
|
| 465 |
+
|
| 466 |
+
### Duplicate Prevention
|
| 467 |
+
- Only one payment per student per day is allowed
|
| 468 |
+
- This prevents accidental duplicate submissions
|
| 469 |
+
|
| 470 |
+
## Example Requests
|
| 471 |
+
|
| 472 |
+
### cURL
|
| 473 |
+
|
| 474 |
+
```bash
|
| 475 |
+
curl -X POST http://your-domain.com/easypay/api/payments/process \
|
| 476 |
+
-H "Content-Type: application/json" \
|
| 477 |
+
-H "Authorization: Bearer your-api-key-here" \
|
| 478 |
+
-d '{
|
| 479 |
+
"student_id": "000001234567890123",
|
| 480 |
+
"teller_no": "1234567890",
|
| 481 |
+
"amount": 50000
|
| 482 |
+
}'
|
| 483 |
+
```
|
| 484 |
+
|
| 485 |
+
### PHP
|
| 486 |
+
|
| 487 |
+
```php
|
| 488 |
+
<?php
|
| 489 |
+
$url = 'http://your-domain.com/easypay/api/payments/process';
|
| 490 |
+
|
| 491 |
+
$data = [
|
| 492 |
+
'student_id' => '000001234567890123',
|
| 493 |
+
'teller_no' => '1234567890',
|
| 494 |
+
'amount' => 50000
|
| 495 |
+
];
|
| 496 |
+
|
| 497 |
+
$options = [
|
| 498 |
+
'http' => [
|
| 499 |
+
'header' => [
|
| 500 |
+
'Content-Type: application/json',
|
| 501 |
+
'Authorization: Bearer your-api-key-here'
|
| 502 |
+
],
|
| 503 |
+
'method' => 'POST',
|
| 504 |
+
'content' => json_encode($data)
|
| 505 |
+
]
|
| 506 |
+
];
|
| 507 |
+
|
| 508 |
+
$context = stream_context_create($options);
|
| 509 |
+
$response = file_get_contents($url, false, $context);
|
| 510 |
+
$result = json_decode($response, true);
|
| 511 |
+
|
| 512 |
+
if ($result['status'] === 'success') {
|
| 513 |
+
echo "Payment processed: " . $result['data']['payment_id'];
|
| 514 |
+
} else {
|
| 515 |
+
echo "Error: " . $result['message'];
|
| 516 |
+
}
|
| 517 |
+
?>
|
| 518 |
+
```
|
| 519 |
+
|
| 520 |
+
### JavaScript (Fetch API)
|
| 521 |
+
|
| 522 |
+
```javascript
|
| 523 |
+
const url = 'http://your-domain.com/easypay/api/payments/process';
|
| 524 |
+
|
| 525 |
+
const data = {
|
| 526 |
+
student_id: '000001234567890123',
|
| 527 |
+
teller_no: '1234567890',
|
| 528 |
+
amount: 50000
|
| 529 |
+
};
|
| 530 |
+
|
| 531 |
+
fetch(url, {
|
| 532 |
+
method: 'POST',
|
| 533 |
+
headers: {
|
| 534 |
+
'Content-Type': 'application/json',
|
| 535 |
+
'Authorization': 'Bearer your-api-key-here'
|
| 536 |
+
},
|
| 537 |
+
body: JSON.stringify(data)
|
| 538 |
+
})
|
| 539 |
+
.then(response => response.json())
|
| 540 |
+
.then(result => {
|
| 541 |
+
if (result.status === 'success') {
|
| 542 |
+
console.log('Payment processed:', result.data.payment_id);
|
| 543 |
+
} else {
|
| 544 |
+
console.error('Error:', result.message);
|
| 545 |
+
}
|
| 546 |
+
})
|
| 547 |
+
.catch(error => console.error('Request failed:', error));
|
| 548 |
+
```
|
| 549 |
+
|
| 550 |
+
### Python
|
| 551 |
+
|
| 552 |
+
```python
|
| 553 |
+
import requests
|
| 554 |
+
import json
|
| 555 |
+
|
| 556 |
+
url = 'http://your-domain.com/easypay/api/payments/process'
|
| 557 |
+
|
| 558 |
+
headers = {
|
| 559 |
+
'Content-Type': 'application/json',
|
| 560 |
+
'Authorization': 'Bearer your-api-key-here'
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
data = {
|
| 564 |
+
'student_id': '000001234567890123',
|
| 565 |
+
'teller_no': '1234567890',
|
| 566 |
+
'amount': 50000
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
response = requests.post(url, headers=headers, json=data)
|
| 570 |
+
result = response.json()
|
| 571 |
+
|
| 572 |
+
if result['status'] == 'success':
|
| 573 |
+
print(f"Payment processed: {result['data']['payment_id']}")
|
| 574 |
+
else:
|
| 575 |
+
print(f"Error: {result['message']}")
|
| 576 |
+
```
|
| 577 |
+
|
| 578 |
+
## Database Tables Affected
|
| 579 |
+
|
| 580 |
+
The API writes to the same tables as the web application:
|
| 581 |
+
|
| 582 |
+
1. `tb_account_school_fee_payments` - Individual fee payment records
|
| 583 |
+
2. `tb_account_school_fee_sum_payments` - Aggregated payment records
|
| 584 |
+
3. `tb_account_student_payments` - Cumulative payment tracking
|
| 585 |
+
4. `tb_account_payment_registers` - Receipt records
|
| 586 |
+
5. `tb_student_logistics` - Student outstanding balance updates
|
| 587 |
+
|
| 588 |
+
## Logging
|
| 589 |
+
|
| 590 |
+
API requests are logged to `logs/api.log` (if enabled in config).
|
| 591 |
+
|
| 592 |
+
Each log entry includes:
|
| 593 |
+
- Timestamp
|
| 594 |
+
- Client IP address
|
| 595 |
+
- Request data
|
| 596 |
+
- Response data
|
| 597 |
+
- HTTP status code
|
| 598 |
+
|
| 599 |
+
To enable/disable logging, edit `config/api_config.php`:
|
| 600 |
+
|
| 601 |
+
```php
|
| 602 |
+
define('API_LOG_ENABLED', true);
|
| 603 |
+
```
|
| 604 |
+
|
| 605 |
+
## Security Considerations
|
| 606 |
+
|
| 607 |
+
1. **HTTPS Required**: Always use HTTPS in production to encrypt API keys and payment data
|
| 608 |
+
2. **API Key Management**: Store API keys securely and rotate them regularly
|
| 609 |
+
3. **IP Whitelisting**: Consider restricting API access to specific IP addresses
|
| 610 |
+
4. **Rate Limiting**: Implement rate limiting to prevent abuse (future enhancement)
|
| 611 |
+
5. **Input Validation**: All inputs are validated and sanitized before processing
|
| 612 |
+
6. **SQL Injection Protection**: All database queries use prepared statements
|
| 613 |
+
7. **Transaction Integrity**: All operations are atomic (all-or-nothing)
|
| 614 |
+
|
| 615 |
+
## Testing
|
| 616 |
+
|
| 617 |
+
### Test with API Authentication Disabled
|
| 618 |
+
|
| 619 |
+
For initial testing, you can disable API authentication:
|
| 620 |
+
|
| 621 |
+
1. Edit `config/api_config.php`
|
| 622 |
+
2. Set `API_AUTH_ENABLED` to `false`
|
| 623 |
+
3. Test without the `Authorization` header
|
| 624 |
+
|
| 625 |
+
### Test Cases
|
| 626 |
+
|
| 627 |
+
1. **Valid Payment**: Send a valid request with existing student and teller
|
| 628 |
+
2. **Invalid Student**: Send request with non-existent student ID
|
| 629 |
+
3. **Invalid Teller**: Send request with non-existent teller number
|
| 630 |
+
4. **Excessive Amount**: Send amount greater than unreconciled amount
|
| 631 |
+
5. **Duplicate Payment**: Send two payments for same student on same day
|
| 632 |
+
6. **Invalid JSON**: Send malformed JSON
|
| 633 |
+
7. **Missing Fields**: Send request with missing required fields
|
| 634 |
+
|
| 635 |
+
## Troubleshooting
|
| 636 |
+
|
| 637 |
+
### "Database connection failed"
|
| 638 |
+
- Check database credentials in `db_config.php`
|
| 639 |
+
- Ensure MySQL server is running
|
| 640 |
+
|
| 641 |
+
### "Student not found"
|
| 642 |
+
- Verify student ID exists in `tb_student_registrations`
|
| 643 |
+
- Check that student has `admission_status = 'Active'`
|
| 644 |
+
|
| 645 |
+
### "Teller number not found"
|
| 646 |
+
- Verify teller exists in `tb_account_bank_statements`
|
| 647 |
+
- Check that teller number is the last word in the description field
|
| 648 |
+
|
| 649 |
+
### "Amount exceeds unreconciled amount"
|
| 650 |
+
- Check the unreconciled amount on the teller
|
| 651 |
+
- Reduce the payment amount
|
| 652 |
+
|
| 653 |
+
### "No outstanding fees found"
|
| 654 |
+
- Verify student has unpaid fees in `tb_account_receivables`
|
| 655 |
+
|
| 656 |
+
## Support
|
| 657 |
+
|
| 658 |
+
For technical support or questions, contact your system administrator.
|
| 659 |
+
|
| 660 |
+
## Version History
|
| 661 |
+
|
| 662 |
+
- **v1.0** (2026-01-09): Initial API release
|
| 663 |
+
- Payment processing endpoint
|
| 664 |
+
- JSON request/response format
|
| 665 |
+
- Optional API key authentication
|
| 666 |
+
- Comprehensive validation
|
| 667 |
+
- Transaction logging
|
easypay-api/api/LAST_RECEIPT_ENDPOINT.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Last Payment Receipt API - Implementation Summary
|
| 2 |
+
|
| 3 |
+
## 🎯 Overview
|
| 4 |
+
|
| 5 |
+
A new API endpoint has been added to the EasyPay system that returns the receipt image (PNG format) of the last payment made by a student.
|
| 6 |
+
|
| 7 |
+
## ✅ What Was Implemented
|
| 8 |
+
|
| 9 |
+
### 1. **New API Endpoint**
|
| 10 |
+
|
| 11 |
+
**File:** `api/students/last_receipt.php`
|
| 12 |
+
|
| 13 |
+
**Endpoint:** `GET /api/students/last_receipt`
|
| 14 |
+
|
| 15 |
+
**Description:** Returns the receipt image of the most recent payment made by a student.
|
| 16 |
+
|
| 17 |
+
**Parameters:**
|
| 18 |
+
- `student_id` (required): The student's unique ID
|
| 19 |
+
|
| 20 |
+
**Response:**
|
| 21 |
+
- **Success:** PNG image (binary data)
|
| 22 |
+
- **Error:** JSON with error message
|
| 23 |
+
|
| 24 |
+
### 2. **Key Features**
|
| 25 |
+
|
| 26 |
+
✅ **Automatic Last Payment Detection**
|
| 27 |
+
- Queries the database to find the most recent payment for the student
|
| 28 |
+
- Orders by payment date (DESC) and ID (DESC) to get the latest
|
| 29 |
+
|
| 30 |
+
✅ **Complete Receipt Generation**
|
| 31 |
+
- Uses the existing `ReceiptGenerator` class
|
| 32 |
+
- Shows all fee items (not just those in the last payment)
|
| 33 |
+
- Displays proper totals and balances
|
| 34 |
+
- Includes school branding and formatting
|
| 35 |
+
|
| 36 |
+
✅ **Validation & Security**
|
| 37 |
+
- Validates student ID is provided
|
| 38 |
+
- Checks student exists and is active
|
| 39 |
+
- Verifies payment records exist
|
| 40 |
+
- Optional API key authentication support
|
| 41 |
+
- Proper error handling with JSON responses
|
| 42 |
+
|
| 43 |
+
✅ **Error Handling**
|
| 44 |
+
- Student ID missing (HTTP 400)
|
| 45 |
+
- Student not found (HTTP 404)
|
| 46 |
+
- No payment records (HTTP 404)
|
| 47 |
+
- Receipt generation errors (HTTP 500)
|
| 48 |
+
|
| 49 |
+
### 3. **Documentation Updates**
|
| 50 |
+
|
| 51 |
+
**Updated Files:**
|
| 52 |
+
- `api/README.md` - Added student endpoints section with last_receipt documentation
|
| 53 |
+
- `api/API_DOCUMENTATION.md` - Added comprehensive documentation with examples in:
|
| 54 |
+
- cURL
|
| 55 |
+
- PHP
|
| 56 |
+
- JavaScript (Fetch API)
|
| 57 |
+
- Python
|
| 58 |
+
|
| 59 |
+
### 4. **Testing Tool**
|
| 60 |
+
|
| 61 |
+
**File:** `api/test_receipt.html`
|
| 62 |
+
|
| 63 |
+
An interactive web-based testing tool that allows you to:
|
| 64 |
+
- Enter a student ID
|
| 65 |
+
- Optionally use API key authentication
|
| 66 |
+
- View the receipt image in the browser
|
| 67 |
+
- Download the receipt as a PNG file
|
| 68 |
+
|
| 69 |
+
## 📁 Files Created/Modified
|
| 70 |
+
|
| 71 |
+
### Created:
|
| 72 |
+
1. `api/students/last_receipt.php` - Main endpoint
|
| 73 |
+
2. `api/test_receipt.html` - Testing tool
|
| 74 |
+
|
| 75 |
+
### Modified:
|
| 76 |
+
1. `api/README.md` - Added student endpoints documentation
|
| 77 |
+
2. `api/API_DOCUMENTATION.md` - Added detailed endpoint documentation
|
| 78 |
+
|
| 79 |
+
## 🚀 How to Use
|
| 80 |
+
|
| 81 |
+
### 1. **Basic Usage (cURL)**
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
# Get receipt and save to file
|
| 85 |
+
curl -X GET "http://localhost/easypay/api/students/last_receipt?student_id=000001234567890123" \
|
| 86 |
+
--output receipt.png
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### 2. **With API Authentication**
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
curl -X GET "http://localhost/easypay/api/students/last_receipt?student_id=000001234567890123" \
|
| 93 |
+
-H "Authorization: Bearer your-api-key-here" \
|
| 94 |
+
--output receipt.png
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### 3. **Using the Test Tool**
|
| 98 |
+
|
| 99 |
+
Open in your browser:
|
| 100 |
+
```
|
| 101 |
+
http://localhost/easypay/api/test_receipt.html
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
Enter a student ID and click "Get Receipt" to view and download.
|
| 105 |
+
|
| 106 |
+
### 4. **PHP Example**
|
| 107 |
+
|
| 108 |
+
```php
|
| 109 |
+
<?php
|
| 110 |
+
$studentId = '000001234567890123';
|
| 111 |
+
$url = "http://localhost/easypay/api/students/last_receipt?student_id={$studentId}";
|
| 112 |
+
|
| 113 |
+
$imageData = file_get_contents($url);
|
| 114 |
+
|
| 115 |
+
if ($imageData !== false) {
|
| 116 |
+
// Save to file
|
| 117 |
+
file_put_contents('receipt.png', $imageData);
|
| 118 |
+
|
| 119 |
+
// Or display in browser
|
| 120 |
+
header('Content-Type: image/png');
|
| 121 |
+
echo $imageData;
|
| 122 |
+
}
|
| 123 |
+
?>
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
### 5. **JavaScript Example**
|
| 127 |
+
|
| 128 |
+
```javascript
|
| 129 |
+
const studentId = '000001234567890123';
|
| 130 |
+
const url = `http://localhost/easypay/api/students/last_receipt?student_id=${studentId}`;
|
| 131 |
+
|
| 132 |
+
fetch(url)
|
| 133 |
+
.then(response => response.blob())
|
| 134 |
+
.then(blob => {
|
| 135 |
+
// Create download link
|
| 136 |
+
const url = window.URL.createObjectURL(blob);
|
| 137 |
+
const a = document.createElement('a');
|
| 138 |
+
a.href = url;
|
| 139 |
+
a.download = 'receipt.png';
|
| 140 |
+
a.click();
|
| 141 |
+
});
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
## 🔍 How It Works
|
| 145 |
+
|
| 146 |
+
1. **Validates Request**: Checks HTTP method and student ID parameter
|
| 147 |
+
2. **Validates Student**: Ensures student exists and is active
|
| 148 |
+
3. **Finds Last Payment**: Queries `tb_account_payment_registers` for most recent payment
|
| 149 |
+
4. **Fetches Receipt Data**: Gets all fee information for the student
|
| 150 |
+
5. **Generates Image**: Uses `ReceiptGenerator` to create PNG receipt
|
| 151 |
+
6. **Returns Image**: Sends PNG image with proper headers
|
| 152 |
+
|
| 153 |
+
## 📊 Database Tables Used
|
| 154 |
+
|
| 155 |
+
- `tb_account_payment_registers` - Payment records and receipts
|
| 156 |
+
- `tb_student_registrations` - Student information
|
| 157 |
+
- `tb_academic_levels` - Student class/level
|
| 158 |
+
- `tb_account_receivables` - Fee billing information
|
| 159 |
+
- `tb_account_school_fees` - Fee descriptions
|
| 160 |
+
|
| 161 |
+
## ✨ Key Benefits
|
| 162 |
+
|
| 163 |
+
1. **Easy Integration**: Simple GET request with student ID
|
| 164 |
+
2. **Consistent Format**: Uses same receipt generator as web application
|
| 165 |
+
3. **Automatic Detection**: No need to specify receipt number or date
|
| 166 |
+
4. **Flexible Output**: Can be displayed, downloaded, or embedded
|
| 167 |
+
5. **Secure**: Validates student and supports API authentication
|
| 168 |
+
6. **Well Documented**: Comprehensive examples in multiple languages
|
| 169 |
+
|
| 170 |
+
## 🧪 Testing
|
| 171 |
+
|
| 172 |
+
### Test Cases to Verify:
|
| 173 |
+
|
| 174 |
+
1. ✅ **Valid Student with Payments**
|
| 175 |
+
- Send request with valid student ID
|
| 176 |
+
- Expect: PNG image of last receipt
|
| 177 |
+
|
| 178 |
+
2. ✅ **Valid Student without Payments**
|
| 179 |
+
- Send request for student with no payments
|
| 180 |
+
- Expect: HTTP 404 "No payment records found"
|
| 181 |
+
|
| 182 |
+
3. ✅ **Invalid Student**
|
| 183 |
+
- Send request with non-existent student ID
|
| 184 |
+
- Expect: HTTP 404 "Student not found"
|
| 185 |
+
|
| 186 |
+
4. ✅ **Missing Student ID**
|
| 187 |
+
- Send request without student_id parameter
|
| 188 |
+
- Expect: HTTP 400 "Student ID is required"
|
| 189 |
+
|
| 190 |
+
5. ✅ **With API Authentication**
|
| 191 |
+
- Send request with valid API key
|
| 192 |
+
- Expect: PNG image
|
| 193 |
+
|
| 194 |
+
## 📝 Notes
|
| 195 |
+
|
| 196 |
+
- The endpoint returns the **last payment** made by the student (most recent by date)
|
| 197 |
+
- The receipt shows **all fees** for the student, not just those in the last payment
|
| 198 |
+
- The receipt format matches the web-generated receipts exactly
|
| 199 |
+
- Images are generated on-the-fly (not cached)
|
| 200 |
+
- The endpoint supports both authenticated and unauthenticated requests (based on config)
|
| 201 |
+
|
| 202 |
+
## 🔐 Security Considerations
|
| 203 |
+
|
| 204 |
+
- Input validation on student ID
|
| 205 |
+
- SQL injection protection (prepared statements)
|
| 206 |
+
- Optional API key authentication
|
| 207 |
+
- Proper error messages (no sensitive data leakage)
|
| 208 |
+
- Active student verification
|
| 209 |
+
|
| 210 |
+
## 📞 Support
|
| 211 |
+
|
| 212 |
+
For issues or questions:
|
| 213 |
+
1. Check the API documentation in `api/API_DOCUMENTATION.md`
|
| 214 |
+
2. Use the test tool at `api/test_receipt.html`
|
| 215 |
+
3. Review the endpoint code in `api/students/last_receipt.php`
|
| 216 |
+
|
| 217 |
+
---
|
| 218 |
+
|
| 219 |
+
**Version:** 1.0
|
| 220 |
+
**Date:** 2026-01-16
|
| 221 |
+
**Author:** AI Assistant
|
easypay-api/api/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
## 📚 Student API Endpoints
|
| 300 |
+
|
| 301 |
+
### 1. **Get Last Payment Receipt Image**
|
| 302 |
+
|
| 303 |
+
**Endpoint:** `GET /api/students/last_receipt`
|
| 304 |
+
|
| 305 |
+
**Description:** Returns the receipt image (PNG) of the last payment made by a student.
|
| 306 |
+
|
| 307 |
+
**Parameters:**
|
| 308 |
+
- `student_id` (required): The student's unique ID
|
| 309 |
+
|
| 310 |
+
**Example Request:**
|
| 311 |
+
```bash
|
| 312 |
+
curl -X GET "http://localhost/easypay/api/students/last_receipt?student_id=000001234567890123" \
|
| 313 |
+
--output receipt.png
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
**Success Response:**
|
| 317 |
+
- **Content-Type:** `image/png`
|
| 318 |
+
- **Body:** Binary PNG image data
|
| 319 |
+
|
| 320 |
+
**Error Responses:**
|
| 321 |
+
```json
|
| 322 |
+
// Student ID missing
|
| 323 |
+
{
|
| 324 |
+
"status": "error",
|
| 325 |
+
"message": "Student ID is required"
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
// Student not found
|
| 329 |
+
{
|
| 330 |
+
"status": "error",
|
| 331 |
+
"message": "Student not found or inactive"
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
// No payment records
|
| 335 |
+
{
|
| 336 |
+
"status": "error",
|
| 337 |
+
"message": "No payment records found for this student"
|
| 338 |
+
}
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
### 2. **Get Payment Status**
|
| 342 |
+
|
| 343 |
+
**Endpoint:** `GET /api/students/payment_status`
|
| 344 |
+
|
| 345 |
+
**Description:** Returns payment status for a specific term.
|
| 346 |
+
|
| 347 |
+
**Parameters:**
|
| 348 |
+
- `student_id` (required): The student's unique ID
|
| 349 |
+
- `academic_session` (required): Academic session (e.g., 2025)
|
| 350 |
+
- `term` (required): Term number (e.g., 1, 2, 3)
|
| 351 |
+
|
| 352 |
+
### 3. **Get Student Balance**
|
| 353 |
+
|
| 354 |
+
**Endpoint:** `GET /api/students/balance`
|
| 355 |
+
|
| 356 |
+
**Description:** Returns the current outstanding balance for a student.
|
| 357 |
+
|
| 358 |
+
**Parameters:**
|
| 359 |
+
- `student_id` (required): The student's unique ID
|
| 360 |
+
|
| 361 |
+
### 4. **Get Student Invoice**
|
| 362 |
+
|
| 363 |
+
**Endpoint:** `GET /api/students/invoice`
|
| 364 |
+
|
| 365 |
+
**Description:** Returns detailed invoice information for a student.
|
| 366 |
+
|
| 367 |
+
**Parameters:**
|
| 368 |
+
- `student_id` (required): The student's unique ID
|
| 369 |
+
- `academic_session` (optional): Filter by academic session
|
| 370 |
+
- `term` (optional): Filter by term
|
| 371 |
+
|
| 372 |
+
## 🎓 Usage Examples
|
| 373 |
+
|
| 374 |
+
See `api/API_DOCUMENTATION.md` for detailed examples in:
|
| 375 |
+
- cURL
|
| 376 |
+
- PHP
|
| 377 |
+
- JavaScript (Fetch API)
|
| 378 |
+
- Python
|
| 379 |
+
|
| 380 |
+
## 📞 Support
|
| 381 |
+
|
| 382 |
+
For issues or questions:
|
| 383 |
+
1. Check the API documentation
|
| 384 |
+
2. Review the logs in `logs/api.log`
|
| 385 |
+
3. Use the test tool at `api/test.html`
|
| 386 |
+
4. Contact your system administrator
|
| 387 |
+
|
| 388 |
+
## 📜 License
|
| 389 |
+
|
| 390 |
+
Internal use only - ARPS School Management System
|
| 391 |
+
|
| 392 |
+
---
|
| 393 |
+
|
| 394 |
+
**Version:** 1.0
|
| 395 |
+
**Date:** 2026-01-09
|
| 396 |
+
**Author:** Senior PHP Backend Engineer
|
easypay-api/api/payments/process.php
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
// Add level_name for receipt
|
| 148 |
+
$result['data']['level_name'] = $student['level_name'] ?? '';
|
| 149 |
+
|
| 150 |
+
// Step 5: Validate teller number
|
| 151 |
+
$tellerValidation = $validator->validateTellerNumber($sanitizedData['teller_no']);
|
| 152 |
+
if (!$tellerValidation['valid']) {
|
| 153 |
+
$response = [
|
| 154 |
+
'status' => 'error',
|
| 155 |
+
'message' => $tellerValidation['error']
|
| 156 |
+
];
|
| 157 |
+
logApiRequest($requestData, $response, $tellerValidation['http_code']);
|
| 158 |
+
sendResponse($response, $tellerValidation['http_code']);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
$teller = $tellerValidation['teller'];
|
| 162 |
+
|
| 163 |
+
// Step 6: Validate amount doesn't exceed unreconciled amount
|
| 164 |
+
if ($sanitizedData['amount'] > $teller['unreconciled_amount']) {
|
| 165 |
+
$response = [
|
| 166 |
+
'status' => 'error',
|
| 167 |
+
'message' => 'Amount exceeds unreconciled amount on teller',
|
| 168 |
+
'errors' => [
|
| 169 |
+
'amount' => 'Requested amount (' . number_format($sanitizedData['amount'], 2) .
|
| 170 |
+
') exceeds available unreconciled amount (' .
|
| 171 |
+
number_format($teller['unreconciled_amount'], 2) . ')'
|
| 172 |
+
]
|
| 173 |
+
];
|
| 174 |
+
logApiRequest($requestData, $response, 400);
|
| 175 |
+
sendResponse($response, 400);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Step 7: Get outstanding fees for the student
|
| 179 |
+
$processor = new PaymentProcessor($pdo);
|
| 180 |
+
$outstandingFees = $processor->getOutstandingFees($student['id']);
|
| 181 |
+
|
| 182 |
+
if (empty($outstandingFees)) {
|
| 183 |
+
$response = [
|
| 184 |
+
'status' => 'error',
|
| 185 |
+
'message' => 'No outstanding fees found for this student'
|
| 186 |
+
];
|
| 187 |
+
logApiRequest($requestData, $response, 400);
|
| 188 |
+
sendResponse($response, 400);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// Step 8: Prepare payment parameters
|
| 192 |
+
// The API will automatically allocate the payment to outstanding fees (oldest first)
|
| 193 |
+
$paymentParams = [
|
| 194 |
+
'student_id' => $student['id'],
|
| 195 |
+
'student_code' => $student['student_code'],
|
| 196 |
+
'selected_fees' => $outstandingFees,
|
| 197 |
+
'teller_number' => $sanitizedData['teller_no'],
|
| 198 |
+
'payment_date' => $sanitizedData['payment_date'], // Use provided date
|
| 199 |
+
'amount_to_use' => $sanitizedData['amount'],
|
| 200 |
+
'source' => 'api' // Mark this payment as API-initiated
|
| 201 |
+
];
|
| 202 |
+
|
| 203 |
+
// Step 9: Process payment using the internal payment logic
|
| 204 |
+
$result = $processor->processPayment($paymentParams);
|
| 205 |
+
|
| 206 |
+
if ($result['success']) {
|
| 207 |
+
// Fetch ALL fees for comprehensive receipt (matching web interface behavior)
|
| 208 |
+
// This ensures the receipt shows complete fee picture, not just current transaction
|
| 209 |
+
$studentId = $result['data']['student_id'];
|
| 210 |
+
$paymentDate = $result['data']['payment_date'];
|
| 211 |
+
$receiptNo = $result['data']['receipt_no'];
|
| 212 |
+
|
| 213 |
+
// Fetch all fees from receivables
|
| 214 |
+
$sqlFees = "SELECT ar.fee_id, ar.actual_value as amount_billed,
|
| 215 |
+
ar.academic_session, ar.term_of_session,
|
| 216 |
+
sf.description as fee_description
|
| 217 |
+
FROM tb_account_receivables ar
|
| 218 |
+
JOIN tb_account_school_fees sf ON ar.fee_id = sf.id
|
| 219 |
+
WHERE ar.student_id = :sid
|
| 220 |
+
ORDER BY ar.academic_session ASC, ar.term_of_session ASC";
|
| 221 |
+
|
| 222 |
+
$stmtFees = $pdo->prepare($sqlFees);
|
| 223 |
+
$stmtFees->execute(['sid' => $studentId]);
|
| 224 |
+
$allFees = $stmtFees->fetchAll(PDO::FETCH_ASSOC);
|
| 225 |
+
|
| 226 |
+
$allocations = [];
|
| 227 |
+
$receiptTotalPaid = 0;
|
| 228 |
+
|
| 229 |
+
foreach ($allFees as $fee) {
|
| 230 |
+
// Calculate Paid To Date (up to this receipt's date)
|
| 231 |
+
$sqlPaid = "SELECT SUM(amount_paid) as total_paid
|
| 232 |
+
FROM tb_account_payment_registers
|
| 233 |
+
WHERE student_id = :sid
|
| 234 |
+
AND fee_id = :fid
|
| 235 |
+
AND academic_session = :as
|
| 236 |
+
AND term_of_session = :ts
|
| 237 |
+
AND payment_date <= :pd";
|
| 238 |
+
|
| 239 |
+
$stmtPaid = $pdo->prepare($sqlPaid);
|
| 240 |
+
$stmtPaid->execute([
|
| 241 |
+
'sid' => $studentId,
|
| 242 |
+
'fid' => $fee['fee_id'],
|
| 243 |
+
'as' => $fee['academic_session'],
|
| 244 |
+
'ts' => $fee['term_of_session'],
|
| 245 |
+
'pd' => $paymentDate
|
| 246 |
+
]);
|
| 247 |
+
$paidResult = $stmtPaid->fetch(PDO::FETCH_ASSOC);
|
| 248 |
+
$paidToDate = floatval($paidResult['total_paid'] ?? 0);
|
| 249 |
+
|
| 250 |
+
// Calculate Amount paid IN THIS RECEIPT (for total calculation)
|
| 251 |
+
$sqlReceiptPay = "SELECT SUM(amount_paid) as receipt_paid
|
| 252 |
+
FROM tb_account_payment_registers
|
| 253 |
+
WHERE receipt_no = :rno
|
| 254 |
+
AND fee_id = :fid
|
| 255 |
+
AND academic_session = :as
|
| 256 |
+
AND term_of_session = :ts";
|
| 257 |
+
$stmtReceiptPay = $pdo->prepare($sqlReceiptPay);
|
| 258 |
+
$stmtReceiptPay->execute([
|
| 259 |
+
'rno' => $receiptNo,
|
| 260 |
+
'fid' => $fee['fee_id'],
|
| 261 |
+
'as' => $fee['academic_session'],
|
| 262 |
+
'ts' => $fee['term_of_session']
|
| 263 |
+
]);
|
| 264 |
+
$receiptPayResult = $stmtReceiptPay->fetch(PDO::FETCH_ASSOC);
|
| 265 |
+
$paidInReceipt = floatval($receiptPayResult['receipt_paid'] ?? 0);
|
| 266 |
+
|
| 267 |
+
$receiptTotalPaid += $paidInReceipt;
|
| 268 |
+
$balance = floatval($fee['amount_billed']) - $paidToDate;
|
| 269 |
+
|
| 270 |
+
// Show if (Balance > 0) OR (PaidInReceipt > 0)
|
| 271 |
+
// This filters out old fully paid fees but keeps current payments
|
| 272 |
+
if ($balance > 0.001 || $paidInReceipt > 0.001) {
|
| 273 |
+
$allocations[] = [
|
| 274 |
+
'description' => $fee['fee_description'],
|
| 275 |
+
'academic_session' => $fee['academic_session'],
|
| 276 |
+
'term_of_session' => $fee['term_of_session'],
|
| 277 |
+
'amount_billed' => floatval($fee['amount_billed']),
|
| 278 |
+
'amount' => $paidInReceipt,
|
| 279 |
+
'total_paid_to_date' => $paidToDate,
|
| 280 |
+
'balance' => $balance
|
| 281 |
+
];
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// Prepare comprehensive receipt data
|
| 286 |
+
$receiptData = [
|
| 287 |
+
'receipt_no' => $receiptNo,
|
| 288 |
+
'student_name' => $student['full_name'],
|
| 289 |
+
'student_code' => $student['student_code'],
|
| 290 |
+
'level_name' => $student['level_name'] ?? '',
|
| 291 |
+
'payment_date' => $paymentDate,
|
| 292 |
+
'total_paid' => $receiptTotalPaid,
|
| 293 |
+
'allocations' => $allocations
|
| 294 |
+
];
|
| 295 |
+
|
| 296 |
+
// Generate receipt with complete fee information
|
| 297 |
+
$generator = new ReceiptGenerator();
|
| 298 |
+
$receiptBase64 = $generator->generateBase64($receiptData);
|
| 299 |
+
|
| 300 |
+
$response = [
|
| 301 |
+
'status' => 'success',
|
| 302 |
+
'message' => $result['message'],
|
| 303 |
+
'data' => [
|
| 304 |
+
'student_id' => $result['data']['student_id'],
|
| 305 |
+
'teller_no' => $result['data']['teller_no'],
|
| 306 |
+
'amount' => $result['data']['total_paid'],
|
| 307 |
+
'payment_id' => $result['data']['transaction_id'],
|
| 308 |
+
'receipt_no' => $result['data']['receipt_no'],
|
| 309 |
+
'payment_date' => $result['data']['payment_date'],
|
| 310 |
+
'receipt_image' => $receiptBase64,
|
| 311 |
+
'fees_settled' => array_map(function ($allocation) {
|
| 312 |
+
return [
|
| 313 |
+
'fee_description' => $allocation['description'],
|
| 314 |
+
'session' => $allocation['academic_session'],
|
| 315 |
+
'term' => $allocation['term_of_session'],
|
| 316 |
+
'amount' => $allocation['amount']
|
| 317 |
+
];
|
| 318 |
+
}, $result['data']['allocations'])
|
| 319 |
+
]
|
| 320 |
+
];
|
| 321 |
+
logApiRequest($requestData, $response, 201);
|
| 322 |
+
sendResponse($response, 201);
|
| 323 |
+
} else {
|
| 324 |
+
$response = [
|
| 325 |
+
'status' => 'error',
|
| 326 |
+
'message' => $result['message']
|
| 327 |
+
];
|
| 328 |
+
logApiRequest($requestData, $response, 500);
|
| 329 |
+
sendResponse($response, 500);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
} catch (Exception $e) {
|
| 333 |
+
$response = [
|
| 334 |
+
'status' => 'error',
|
| 335 |
+
'message' => 'Internal server error',
|
| 336 |
+
'error_detail' => $e->getMessage() // Remove in production
|
| 337 |
+
];
|
| 338 |
+
logApiRequest($requestData ?? [], $response, 500);
|
| 339 |
+
sendResponse($response, 500);
|
| 340 |
+
}
|
easypay-api/api/students/balance.php
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* API Endpoint: Fetch Student Outstanding Balance
|
| 4 |
+
*
|
| 5 |
+
* Returns the total outstanding balance and a breakdown of fees
|
| 6 |
+
* for all terms and sessions.
|
| 7 |
+
*
|
| 8 |
+
* Method: GET
|
| 9 |
+
* URL: /api/students/balance?student_id=...
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
require_once '../../db_config.php';
|
| 13 |
+
require_once '../../includes/PaymentProcessor.php';
|
| 14 |
+
require_once '../../includes/ApiValidator.php';
|
| 15 |
+
require_once '../../config/api_config.php';
|
| 16 |
+
|
| 17 |
+
// Initialize core classes
|
| 18 |
+
$validator = new ApiValidator($pdo, defined('API_KEYS') ? API_KEYS : []);
|
| 19 |
+
$processor = new PaymentProcessor($pdo);
|
| 20 |
+
|
| 21 |
+
// 1. Validate Request (Method & Auth)
|
| 22 |
+
$validation = $validator->validateRequest(['GET']);
|
| 23 |
+
if (!$validation['valid']) {
|
| 24 |
+
http_response_code($validation['http_code']);
|
| 25 |
+
echo json_encode(['status' => 'error', 'message' => $validation['error']]);
|
| 26 |
+
exit;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// 2. Validate Student ID
|
| 30 |
+
$studentId = trim($_GET['student_id'] ?? '');
|
| 31 |
+
if (empty($studentId)) {
|
| 32 |
+
http_response_code(400);
|
| 33 |
+
echo json_encode(['status' => 'error', 'message' => 'Student ID is required']);
|
| 34 |
+
exit;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
$studentCheck = $validator->validateStudentExists($studentId);
|
| 38 |
+
if (!$studentCheck['valid']) {
|
| 39 |
+
http_response_code($studentCheck['http_code']);
|
| 40 |
+
echo json_encode(['status' => 'error', 'message' => $studentCheck['error']]);
|
| 41 |
+
exit;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
try {
|
| 45 |
+
// 3. Fetch Data
|
| 46 |
+
$data = $processor->getTotalOutstandingBalance($studentId);
|
| 47 |
+
|
| 48 |
+
// 4. Send Response
|
| 49 |
+
echo json_encode([
|
| 50 |
+
'status' => 'success',
|
| 51 |
+
'data' => $data
|
| 52 |
+
]);
|
| 53 |
+
|
| 54 |
+
} catch (Exception $e) {
|
| 55 |
+
// Log error (if logging enabled)
|
| 56 |
+
if (function_exists('log_api_request')) {
|
| 57 |
+
// log_api_request would be defined in api_config or similar,
|
| 58 |
+
// but for now we just handle the output.
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
http_response_code(500);
|
| 62 |
+
echo json_encode(['status' => 'error', 'message' => 'Internal server error: ' . $e->getMessage()]);
|
| 63 |
+
}
|
easypay-api/api/students/invoice.php
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* API Endpoint: Fetch Term Invoice (Fee Breakdown)
|
| 4 |
+
*
|
| 5 |
+
* Returns the breakdown of fees billed to a student for a specific term/session.
|
| 6 |
+
* Includes billed amount, paid amount, and outstanding balance per fee.
|
| 7 |
+
*
|
| 8 |
+
* Method: GET
|
| 9 |
+
* URL: /api/students/invoice?student_id=...&academic_session=...&term=...
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
require_once '../../db_config.php';
|
| 13 |
+
require_once '../../includes/PaymentProcessor.php';
|
| 14 |
+
require_once '../../includes/ApiValidator.php';
|
| 15 |
+
require_once '../../config/api_config.php';
|
| 16 |
+
|
| 17 |
+
// Initialize core classes
|
| 18 |
+
$validator = new ApiValidator($pdo, defined('API_KEYS') ? API_KEYS : []);
|
| 19 |
+
$processor = new PaymentProcessor($pdo);
|
| 20 |
+
|
| 21 |
+
// 1. Validate Request
|
| 22 |
+
$validation = $validator->validateRequest(['GET']);
|
| 23 |
+
if (!$validation['valid']) {
|
| 24 |
+
http_response_code($validation['http_code']);
|
| 25 |
+
echo json_encode(['status' => 'error', 'message' => $validation['error']]);
|
| 26 |
+
exit;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// 2. Validate Input Parameters
|
| 30 |
+
$studentId = trim($_GET['student_id'] ?? '');
|
| 31 |
+
$session = trim($_GET['academic_session'] ?? '');
|
| 32 |
+
$term = trim($_GET['term'] ?? ''); // term_of_session
|
| 33 |
+
|
| 34 |
+
$errors = [];
|
| 35 |
+
if (empty($studentId))
|
| 36 |
+
$errors[] = "Student ID is required";
|
| 37 |
+
if (empty($session))
|
| 38 |
+
$errors[] = "Academic Session is required";
|
| 39 |
+
if (empty($term))
|
| 40 |
+
$errors[] = "Term is required";
|
| 41 |
+
|
| 42 |
+
if (!empty($errors)) {
|
| 43 |
+
http_response_code(400);
|
| 44 |
+
echo json_encode(['status' => 'error', 'message' => implode(', ', $errors)]);
|
| 45 |
+
exit;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// 3. Validate Student Exists
|
| 49 |
+
$studentCheck = $validator->validateStudentExists($studentId);
|
| 50 |
+
if (!$studentCheck['valid']) {
|
| 51 |
+
http_response_code($studentCheck['http_code']);
|
| 52 |
+
echo json_encode(['status' => 'error', 'message' => $studentCheck['error']]);
|
| 53 |
+
exit;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
try {
|
| 57 |
+
// 4. Fetch Data
|
| 58 |
+
$invoice = $processor->getTermInvoice($studentId, $session, $term);
|
| 59 |
+
|
| 60 |
+
// 5. Send Response
|
| 61 |
+
echo json_encode([
|
| 62 |
+
'status' => 'success',
|
| 63 |
+
'data' => [
|
| 64 |
+
'student_id' => $studentId,
|
| 65 |
+
'academic_session' => $session,
|
| 66 |
+
'term' => $term,
|
| 67 |
+
'items' => $invoice
|
| 68 |
+
]
|
| 69 |
+
]);
|
| 70 |
+
|
| 71 |
+
} catch (Exception $e) {
|
| 72 |
+
http_response_code(500);
|
| 73 |
+
echo json_encode(['status' => 'error', 'message' => 'Internal server error: ' . $e->getMessage()]);
|
| 74 |
+
}
|
easypay-api/api/students/last_receipt.php
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* API Endpoint: Get Last Payment Receipt Image
|
| 4 |
+
*
|
| 5 |
+
* Returns the receipt image (PNG) of the last payment made by a student.
|
| 6 |
+
*
|
| 7 |
+
* Method: GET
|
| 8 |
+
* URL: /api/students/last_receipt?student_id=...
|
| 9 |
+
*
|
| 10 |
+
* Response: PNG image (binary)
|
| 11 |
+
*
|
| 12 |
+
* Error Response: JSON with error message
|
| 13 |
+
*/
|
| 14 |
+
|
| 15 |
+
require_once '../../db_config.php';
|
| 16 |
+
require_once '../../includes/ReceiptGenerator.php';
|
| 17 |
+
require_once '../../includes/ApiValidator.php';
|
| 18 |
+
require_once '../../config/api_config.php';
|
| 19 |
+
|
| 20 |
+
// Initialize validator
|
| 21 |
+
$validator = new ApiValidator($pdo, defined('API_KEYS') ? API_KEYS : []);
|
| 22 |
+
|
| 23 |
+
// 1. Validate Request
|
| 24 |
+
$validation = $validator->validateRequest(['GET']);
|
| 25 |
+
if (!$validation['valid']) {
|
| 26 |
+
http_response_code($validation['http_code']);
|
| 27 |
+
header('Content-Type: application/json');
|
| 28 |
+
echo json_encode(['status' => 'error', 'message' => $validation['error']]);
|
| 29 |
+
exit;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// 2. Validate Input Parameters
|
| 33 |
+
$studentId = trim($_GET['student_id'] ?? '');
|
| 34 |
+
|
| 35 |
+
if (empty($studentId)) {
|
| 36 |
+
http_response_code(400);
|
| 37 |
+
header('Content-Type: application/json');
|
| 38 |
+
echo json_encode(['status' => 'error', 'message' => 'Student ID is required']);
|
| 39 |
+
exit;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// 3. Validate Student Exists
|
| 43 |
+
$studentCheck = $validator->validateStudentExists($studentId);
|
| 44 |
+
if (!$studentCheck['valid']) {
|
| 45 |
+
http_response_code($studentCheck['http_code']);
|
| 46 |
+
header('Content-Type: application/json');
|
| 47 |
+
echo json_encode(['status' => 'error', 'message' => $studentCheck['error']]);
|
| 48 |
+
exit;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Prevent output from messing up image headers
|
| 52 |
+
ob_start();
|
| 53 |
+
ini_set('display_errors', 0);
|
| 54 |
+
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
|
| 55 |
+
|
| 56 |
+
try {
|
| 57 |
+
// 4. Fetch Last Payment Receipt Number
|
| 58 |
+
$sqlLastReceipt = "SELECT receipt_no, payment_date
|
| 59 |
+
FROM tb_account_payment_registers
|
| 60 |
+
WHERE student_id = :student_id
|
| 61 |
+
ORDER BY payment_date DESC, id DESC
|
| 62 |
+
LIMIT 1";
|
| 63 |
+
|
| 64 |
+
$stmt = $pdo->prepare($sqlLastReceipt);
|
| 65 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 66 |
+
$lastPayment = $stmt->fetch(PDO::FETCH_ASSOC);
|
| 67 |
+
|
| 68 |
+
if (!$lastPayment) {
|
| 69 |
+
http_response_code(404);
|
| 70 |
+
header('Content-Type: application/json');
|
| 71 |
+
echo json_encode([
|
| 72 |
+
'status' => 'error',
|
| 73 |
+
'message' => 'No payment records found for this student'
|
| 74 |
+
]);
|
| 75 |
+
exit;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
$receiptNo = $lastPayment['receipt_no'];
|
| 79 |
+
$paymentDate = $lastPayment['payment_date'];
|
| 80 |
+
|
| 81 |
+
// 5. Fetch Receipt Meta Info (Student, Date)
|
| 82 |
+
$sqlInfo = "SELECT pr.student_id, pr.payment_date,
|
| 83 |
+
sr.last_name, sr.first_name, sr.other_name, sr.student_code,
|
| 84 |
+
al.level_name
|
| 85 |
+
FROM tb_account_payment_registers pr
|
| 86 |
+
JOIN tb_student_registrations sr ON pr.student_id = sr.id
|
| 87 |
+
LEFT JOIN tb_academic_levels al ON sr.level_id = al.id
|
| 88 |
+
WHERE pr.receipt_no = :receipt_no
|
| 89 |
+
LIMIT 1";
|
| 90 |
+
|
| 91 |
+
$stmt = $pdo->prepare($sqlInfo);
|
| 92 |
+
$stmt->execute(['receipt_no' => $receiptNo]);
|
| 93 |
+
$receiptInfo = $stmt->fetch(PDO::FETCH_ASSOC);
|
| 94 |
+
|
| 95 |
+
if (!$receiptInfo) {
|
| 96 |
+
http_response_code(500);
|
| 97 |
+
header('Content-Type: application/json');
|
| 98 |
+
echo json_encode([
|
| 99 |
+
'status' => 'error',
|
| 100 |
+
'message' => 'Receipt information not found'
|
| 101 |
+
]);
|
| 102 |
+
exit;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
$studentId = $receiptInfo['student_id'];
|
| 106 |
+
|
| 107 |
+
// 6. Fetch All Fees for Student (from receivables)
|
| 108 |
+
$sqlFees = "SELECT ar.fee_id, ar.actual_value as amount_billed,
|
| 109 |
+
ar.academic_session, ar.term_of_session,
|
| 110 |
+
sf.description as fee_description
|
| 111 |
+
FROM tb_account_receivables ar
|
| 112 |
+
JOIN tb_account_school_fees sf ON ar.fee_id = sf.id
|
| 113 |
+
WHERE ar.student_id = :sid
|
| 114 |
+
ORDER BY ar.academic_session ASC, ar.term_of_session ASC";
|
| 115 |
+
|
| 116 |
+
$stmtFees = $pdo->prepare($sqlFees);
|
| 117 |
+
$stmtFees->execute(['sid' => $studentId]);
|
| 118 |
+
$allFees = $stmtFees->fetchAll(PDO::FETCH_ASSOC);
|
| 119 |
+
|
| 120 |
+
$allocations = [];
|
| 121 |
+
$receiptTotalPaid = 0;
|
| 122 |
+
|
| 123 |
+
foreach ($allFees as $fee) {
|
| 124 |
+
// Calculate Paid To Date (up to this receipt's date)
|
| 125 |
+
$sqlPaid = "SELECT SUM(amount_paid) as total_paid
|
| 126 |
+
FROM tb_account_payment_registers
|
| 127 |
+
WHERE student_id = :sid
|
| 128 |
+
AND fee_id = :fid
|
| 129 |
+
AND academic_session = :as
|
| 130 |
+
AND term_of_session = :ts
|
| 131 |
+
AND payment_date <= :pd";
|
| 132 |
+
|
| 133 |
+
$stmtPaid = $pdo->prepare($sqlPaid);
|
| 134 |
+
$stmtPaid->execute([
|
| 135 |
+
'sid' => $studentId,
|
| 136 |
+
'fid' => $fee['fee_id'],
|
| 137 |
+
'as' => $fee['academic_session'],
|
| 138 |
+
'ts' => $fee['term_of_session'],
|
| 139 |
+
'pd' => $paymentDate
|
| 140 |
+
]);
|
| 141 |
+
$paidResult = $stmtPaid->fetch(PDO::FETCH_ASSOC);
|
| 142 |
+
$paidToDate = floatval($paidResult['total_paid'] ?? 0);
|
| 143 |
+
|
| 144 |
+
// Calculate Amount paid IN THIS RECEIPT (for total calculation)
|
| 145 |
+
$sqlReceiptPay = "SELECT SUM(amount_paid) as receipt_paid
|
| 146 |
+
FROM tb_account_payment_registers
|
| 147 |
+
WHERE receipt_no = :rno
|
| 148 |
+
AND fee_id = :fid
|
| 149 |
+
AND academic_session = :as
|
| 150 |
+
AND term_of_session = :ts";
|
| 151 |
+
$stmtReceiptPay = $pdo->prepare($sqlReceiptPay);
|
| 152 |
+
$stmtReceiptPay->execute([
|
| 153 |
+
'rno' => $receiptNo,
|
| 154 |
+
'fid' => $fee['fee_id'],
|
| 155 |
+
'as' => $fee['academic_session'],
|
| 156 |
+
'ts' => $fee['term_of_session']
|
| 157 |
+
]);
|
| 158 |
+
$receiptPayResult = $stmtReceiptPay->fetch(PDO::FETCH_ASSOC);
|
| 159 |
+
$paidInReceipt = floatval($receiptPayResult['receipt_paid'] ?? 0);
|
| 160 |
+
|
| 161 |
+
$receiptTotalPaid += $paidInReceipt;
|
| 162 |
+
$balance = floatval($fee['amount_billed']) - $paidToDate;
|
| 163 |
+
|
| 164 |
+
// Condition: Show if (Balance > 0) OR (PaidInReceipt > 0)
|
| 165 |
+
// Helps filter out old fully paid fees, but keeps current payments even if they zeroed the balance
|
| 166 |
+
if ($balance > 0.001 || $paidInReceipt > 0.001) {
|
| 167 |
+
$allocations[] = [
|
| 168 |
+
'description' => $fee['fee_description'],
|
| 169 |
+
'academic_session' => $fee['academic_session'],
|
| 170 |
+
'term_of_session' => $fee['term_of_session'],
|
| 171 |
+
'amount_billed' => floatval($fee['amount_billed']),
|
| 172 |
+
'amount' => $paidInReceipt,
|
| 173 |
+
'total_paid_to_date' => $paidToDate,
|
| 174 |
+
'balance' => $balance
|
| 175 |
+
];
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// 7. Prepare data structure for generator
|
| 180 |
+
$data = [
|
| 181 |
+
'receipt_no' => $receiptNo,
|
| 182 |
+
'student_name' => trim($receiptInfo['last_name'] . ' ' . $receiptInfo['first_name'] . ' ' . ($receiptInfo['other_name'] ?? '')),
|
| 183 |
+
'student_code' => $receiptInfo['student_code'],
|
| 184 |
+
'level_name' => $receiptInfo['level_name'] ?? '',
|
| 185 |
+
'payment_date' => $paymentDate,
|
| 186 |
+
'total_paid' => $receiptTotalPaid,
|
| 187 |
+
'allocations' => $allocations
|
| 188 |
+
];
|
| 189 |
+
|
| 190 |
+
// 8. Generate Image
|
| 191 |
+
$generator = new ReceiptGenerator();
|
| 192 |
+
$imageData = $generator->generate($data);
|
| 193 |
+
|
| 194 |
+
// 9. Output Image
|
| 195 |
+
ob_end_clean(); // Discard any warnings/output buffered so far
|
| 196 |
+
header('Content-Type: image/png');
|
| 197 |
+
header('Content-Disposition: inline; filename="receipt_' . $receiptNo . '.png"');
|
| 198 |
+
header('Content-Length: ' . strlen($imageData));
|
| 199 |
+
echo $imageData;
|
| 200 |
+
|
| 201 |
+
} catch (Exception $e) {
|
| 202 |
+
ob_end_clean();
|
| 203 |
+
http_response_code(500);
|
| 204 |
+
header('Content-Type: application/json');
|
| 205 |
+
echo json_encode([
|
| 206 |
+
'status' => 'error',
|
| 207 |
+
'message' => 'Error generating receipt: ' . $e->getMessage()
|
| 208 |
+
]);
|
| 209 |
+
}
|
easypay-api/api/students/payment_status.php
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
/**
|
| 3 |
+
* API Endpoint: Fetch Term Payment Status
|
| 4 |
+
*
|
| 5 |
+
* Returns a summary of payment status for a term (Billed vs Paid)
|
| 6 |
+
* and a list of transactions (receipts) associated with that term.
|
| 7 |
+
*
|
| 8 |
+
* Method: GET
|
| 9 |
+
* URL: /api/students/payment_status?student_id=...&academic_session=...&term=...
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
require_once '../../db_config.php';
|
| 13 |
+
require_once '../../includes/PaymentProcessor.php';
|
| 14 |
+
require_once '../../includes/ApiValidator.php';
|
| 15 |
+
require_once '../../config/api_config.php';
|
| 16 |
+
|
| 17 |
+
// Initialize core classes
|
| 18 |
+
$validator = new ApiValidator($pdo, defined('API_KEYS') ? API_KEYS : []);
|
| 19 |
+
$processor = new PaymentProcessor($pdo);
|
| 20 |
+
|
| 21 |
+
// 1. Validate Request
|
| 22 |
+
$validation = $validator->validateRequest(['GET']);
|
| 23 |
+
if (!$validation['valid']) {
|
| 24 |
+
http_response_code($validation['http_code']);
|
| 25 |
+
echo json_encode(['status' => 'error', 'message' => $validation['error']]);
|
| 26 |
+
exit;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// 2. Validate Input Parameters
|
| 30 |
+
$studentId = trim($_GET['student_id'] ?? '');
|
| 31 |
+
$session = trim($_GET['academic_session'] ?? '');
|
| 32 |
+
$term = trim($_GET['term'] ?? '');
|
| 33 |
+
|
| 34 |
+
$errors = [];
|
| 35 |
+
if (empty($studentId))
|
| 36 |
+
$errors[] = "Student ID is required";
|
| 37 |
+
if (empty($session))
|
| 38 |
+
$errors[] = "Academic Session is required";
|
| 39 |
+
if (empty($term))
|
| 40 |
+
$errors[] = "Term is required";
|
| 41 |
+
|
| 42 |
+
if (!empty($errors)) {
|
| 43 |
+
http_response_code(400);
|
| 44 |
+
echo json_encode(['status' => 'error', 'message' => implode(', ', $errors)]);
|
| 45 |
+
exit;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// 3. Validate Student Exists
|
| 49 |
+
$studentCheck = $validator->validateStudentExists($studentId);
|
| 50 |
+
if (!$studentCheck['valid']) {
|
| 51 |
+
http_response_code($studentCheck['http_code']);
|
| 52 |
+
echo json_encode(['status' => 'error', 'message' => $studentCheck['error']]);
|
| 53 |
+
exit;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
try {
|
| 57 |
+
// 4. Fetch Data
|
| 58 |
+
$status = $processor->getTermPaymentStatus($studentId, $session, $term);
|
| 59 |
+
|
| 60 |
+
// 5. Send Response
|
| 61 |
+
echo json_encode([
|
| 62 |
+
'status' => 'success',
|
| 63 |
+
'data' => $status
|
| 64 |
+
]);
|
| 65 |
+
|
| 66 |
+
} catch (Exception $e) {
|
| 67 |
+
http_response_code(500);
|
| 68 |
+
echo json_encode(['status' => 'error', 'message' => 'Internal server error: ' . $e->getMessage()]);
|
| 69 |
+
}
|
easypay-api/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-api/api/test_receipt.html
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>Receipt 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 |
+
width: 100%;
|
| 56 |
+
padding: 12px;
|
| 57 |
+
border: 2px solid #ddd;
|
| 58 |
+
border-radius: 6px;
|
| 59 |
+
font-size: 16px;
|
| 60 |
+
transition: border-color 0.3s;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
input[type="text"]:focus {
|
| 64 |
+
outline: none;
|
| 65 |
+
border-color: #667eea;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.btn {
|
| 69 |
+
width: 100%;
|
| 70 |
+
padding: 14px;
|
| 71 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 72 |
+
color: white;
|
| 73 |
+
border: none;
|
| 74 |
+
border-radius: 6px;
|
| 75 |
+
font-size: 16px;
|
| 76 |
+
font-weight: 600;
|
| 77 |
+
cursor: pointer;
|
| 78 |
+
transition: transform 0.3s;
|
| 79 |
+
margin-bottom: 10px;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.btn:hover {
|
| 83 |
+
transform: translateY(-2px);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.btn:disabled {
|
| 87 |
+
opacity: 0.6;
|
| 88 |
+
cursor: not-allowed;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.btn-secondary {
|
| 92 |
+
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.response-section {
|
| 96 |
+
margin-top: 30px;
|
| 97 |
+
display: none;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.response-section.show {
|
| 101 |
+
display: block;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.receipt-container {
|
| 105 |
+
background: #f8f9fa;
|
| 106 |
+
border-radius: 8px;
|
| 107 |
+
padding: 20px;
|
| 108 |
+
margin-top: 15px;
|
| 109 |
+
text-align: center;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.receipt-container img {
|
| 113 |
+
max-width: 100%;
|
| 114 |
+
border: 2px solid #ddd;
|
| 115 |
+
border-radius: 8px;
|
| 116 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.error-box {
|
| 120 |
+
background: #f8d7da;
|
| 121 |
+
border: 1px solid #f5c6cb;
|
| 122 |
+
border-radius: 8px;
|
| 123 |
+
padding: 20px;
|
| 124 |
+
margin-top: 15px;
|
| 125 |
+
color: #721c24;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.info-box {
|
| 129 |
+
background: #e7f3ff;
|
| 130 |
+
border-left: 4px solid #2196F3;
|
| 131 |
+
padding: 15px;
|
| 132 |
+
margin-bottom: 20px;
|
| 133 |
+
border-radius: 4px;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.info-box p {
|
| 137 |
+
margin: 5px 0;
|
| 138 |
+
color: #333;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.checkbox-group {
|
| 142 |
+
margin-bottom: 20px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.checkbox-group label {
|
| 146 |
+
display: flex;
|
| 147 |
+
align-items: center;
|
| 148 |
+
font-weight: normal;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.checkbox-group input[type="checkbox"] {
|
| 152 |
+
margin-right: 10px;
|
| 153 |
+
width: 18px;
|
| 154 |
+
height: 18px;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.loading {
|
| 158 |
+
text-align: center;
|
| 159 |
+
padding: 20px;
|
| 160 |
+
color: #667eea;
|
| 161 |
+
font-weight: 600;
|
| 162 |
+
}
|
| 163 |
+
</style>
|
| 164 |
+
</head>
|
| 165 |
+
|
| 166 |
+
<body>
|
| 167 |
+
<div class="container">
|
| 168 |
+
<h1>📄 Receipt API Tester</h1>
|
| 169 |
+
<p class="subtitle">Test the Last Payment Receipt endpoint</p>
|
| 170 |
+
|
| 171 |
+
<div class="info-box">
|
| 172 |
+
<p><strong>API Endpoint:</strong> <code id="apiEndpoint"></code></p>
|
| 173 |
+
<p><strong>Method:</strong> GET</p>
|
| 174 |
+
<p><strong>Response:</strong> PNG Image</p>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<form id="receiptForm">
|
| 178 |
+
<div class="form-group">
|
| 179 |
+
<label for="studentId">Student ID *</label>
|
| 180 |
+
<input type="text" id="studentId" name="student_id" required placeholder="e.g., 000001234567890123">
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<div class="checkbox-group">
|
| 184 |
+
<label>
|
| 185 |
+
<input type="checkbox" id="useAuth" name="use_auth">
|
| 186 |
+
Use API Key Authentication
|
| 187 |
+
</label>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div class="form-group" id="apiKeyGroup" style="display: none;">
|
| 191 |
+
<label for="apiKey">API Key</label>
|
| 192 |
+
<input type="text" id="apiKey" name="api_key" placeholder="Enter your API key">
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<button type="submit" class="btn" id="submitBtn">Get Receipt</button>
|
| 196 |
+
<button type="button" class="btn btn-secondary" id="downloadBtn" style="display: none;">Download
|
| 197 |
+
Receipt</button>
|
| 198 |
+
</form>
|
| 199 |
+
|
| 200 |
+
<div class="response-section" id="responseSection">
|
| 201 |
+
<div id="loadingIndicator" class="loading" style="display: none;">
|
| 202 |
+
Loading receipt...
|
| 203 |
+
</div>
|
| 204 |
+
<div id="receiptContainer" class="receipt-container" style="display: none;">
|
| 205 |
+
<h3>Receipt Image</h3>
|
| 206 |
+
<img id="receiptImage" src="" alt="Receipt">
|
| 207 |
+
</div>
|
| 208 |
+
<div id="errorContainer" class="error-box" style="display: none;">
|
| 209 |
+
<h3>Error</h3>
|
| 210 |
+
<p id="errorMessage"></p>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<script>
|
| 216 |
+
// Set API endpoint
|
| 217 |
+
const apiEndpoint = window.location.origin + '/easypay/api/students/last_receipt';
|
| 218 |
+
document.getElementById('apiEndpoint').textContent = apiEndpoint;
|
| 219 |
+
|
| 220 |
+
let currentReceiptBlob = null;
|
| 221 |
+
|
| 222 |
+
// Toggle API key field
|
| 223 |
+
document.getElementById('useAuth').addEventListener('change', function () {
|
| 224 |
+
document.getElementById('apiKeyGroup').style.display = this.checked ? 'block' : 'none';
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
// Handle form submission
|
| 228 |
+
document.getElementById('receiptForm').addEventListener('submit', async function (e) {
|
| 229 |
+
e.preventDefault();
|
| 230 |
+
|
| 231 |
+
const submitBtn = document.getElementById('submitBtn');
|
| 232 |
+
const downloadBtn = document.getElementById('downloadBtn');
|
| 233 |
+
const responseSection = document.getElementById('responseSection');
|
| 234 |
+
const loadingIndicator = document.getElementById('loadingIndicator');
|
| 235 |
+
const receiptContainer = document.getElementById('receiptContainer');
|
| 236 |
+
const errorContainer = document.getElementById('errorContainer');
|
| 237 |
+
|
| 238 |
+
// Reset UI
|
| 239 |
+
submitBtn.disabled = true;
|
| 240 |
+
submitBtn.textContent = 'Loading...';
|
| 241 |
+
downloadBtn.style.display = 'none';
|
| 242 |
+
responseSection.classList.add('show');
|
| 243 |
+
loadingIndicator.style.display = 'block';
|
| 244 |
+
receiptContainer.style.display = 'none';
|
| 245 |
+
errorContainer.style.display = 'none';
|
| 246 |
+
|
| 247 |
+
// Prepare URL
|
| 248 |
+
const studentId = document.getElementById('studentId').value;
|
| 249 |
+
const url = `${apiEndpoint}?student_id=${encodeURIComponent(studentId)}`;
|
| 250 |
+
|
| 251 |
+
// Prepare headers
|
| 252 |
+
const headers = {};
|
| 253 |
+
if (document.getElementById('useAuth').checked) {
|
| 254 |
+
const apiKey = document.getElementById('apiKey').value;
|
| 255 |
+
if (apiKey) {
|
| 256 |
+
headers['Authorization'] = 'Bearer ' + apiKey;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
try {
|
| 261 |
+
// Send request
|
| 262 |
+
const response = await fetch(url, {
|
| 263 |
+
method: 'GET',
|
| 264 |
+
headers: headers
|
| 265 |
+
});
|
| 266 |
+
|
| 267 |
+
loadingIndicator.style.display = 'none';
|
| 268 |
+
|
| 269 |
+
if (response.ok) {
|
| 270 |
+
// Success - display image
|
| 271 |
+
const blob = await response.blob();
|
| 272 |
+
currentReceiptBlob = blob;
|
| 273 |
+
|
| 274 |
+
const imageUrl = URL.createObjectURL(blob);
|
| 275 |
+
document.getElementById('receiptImage').src = imageUrl;
|
| 276 |
+
receiptContainer.style.display = 'block';
|
| 277 |
+
downloadBtn.style.display = 'block';
|
| 278 |
+
} else {
|
| 279 |
+
// Error - try to parse JSON error
|
| 280 |
+
const contentType = response.headers.get('content-type');
|
| 281 |
+
if (contentType && contentType.includes('application/json')) {
|
| 282 |
+
const errorData = await response.json();
|
| 283 |
+
displayError(errorData.message || 'Unknown error occurred');
|
| 284 |
+
} else {
|
| 285 |
+
displayError(`HTTP ${response.status}: ${response.statusText}`);
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
} catch (error) {
|
| 290 |
+
loadingIndicator.style.display = 'none';
|
| 291 |
+
displayError('Request failed: ' + error.message);
|
| 292 |
+
} finally {
|
| 293 |
+
submitBtn.disabled = false;
|
| 294 |
+
submitBtn.textContent = 'Get Receipt';
|
| 295 |
+
}
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
// Handle download button
|
| 299 |
+
document.getElementById('downloadBtn').addEventListener('click', function () {
|
| 300 |
+
if (currentReceiptBlob) {
|
| 301 |
+
const url = URL.createObjectURL(currentReceiptBlob);
|
| 302 |
+
const a = document.createElement('a');
|
| 303 |
+
a.href = url;
|
| 304 |
+
a.download = 'receipt_' + document.getElementById('studentId').value + '.png';
|
| 305 |
+
document.body.appendChild(a);
|
| 306 |
+
a.click();
|
| 307 |
+
document.body.removeChild(a);
|
| 308 |
+
URL.revokeObjectURL(url);
|
| 309 |
+
}
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
function displayError(message) {
|
| 313 |
+
const errorContainer = document.getElementById('errorContainer');
|
| 314 |
+
const errorMessage = document.getElementById('errorMessage');
|
| 315 |
+
|
| 316 |
+
errorMessage.textContent = message;
|
| 317 |
+
errorContainer.style.display = 'block';
|
| 318 |
+
}
|
| 319 |
+
</script>
|
| 320 |
+
</body>
|
| 321 |
+
|
| 322 |
+
</html>
|
easypay-api/assets/logo.png
ADDED
|
easypay-api/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-api/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-api/download_receipt.php
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
require_once 'db_config.php';
|
| 3 |
+
require_once 'includes/ReceiptGenerator.php';
|
| 4 |
+
|
| 5 |
+
// Prevent output from messing up image headers
|
| 6 |
+
ob_start();
|
| 7 |
+
ini_set('display_errors', 0);
|
| 8 |
+
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
|
| 9 |
+
|
| 10 |
+
// Validate input
|
| 11 |
+
$receiptNo = $_GET['receipt_no'] ?? '';
|
| 12 |
+
if (empty($receiptNo)) {
|
| 13 |
+
die("Receipt number is required.");
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
try {
|
| 17 |
+
// 1. Fetch Receipt Meta Info (Student, Date)
|
| 18 |
+
$sqlInfo = "SELECT pr.student_id, pr.payment_date,
|
| 19 |
+
sr.last_name, sr.first_name, sr.other_name, sr.student_code,
|
| 20 |
+
al.level_name
|
| 21 |
+
FROM tb_account_payment_registers pr
|
| 22 |
+
JOIN tb_student_registrations sr ON pr.student_id = sr.id
|
| 23 |
+
LEFT JOIN tb_academic_levels al ON sr.level_id = al.id
|
| 24 |
+
WHERE pr.receipt_no = :receipt_no
|
| 25 |
+
LIMIT 1";
|
| 26 |
+
|
| 27 |
+
$stmt = $pdo->prepare($sqlInfo);
|
| 28 |
+
$stmt->execute(['receipt_no' => $receiptNo]);
|
| 29 |
+
$receiptInfo = $stmt->fetch(PDO::FETCH_ASSOC);
|
| 30 |
+
|
| 31 |
+
if (!$receiptInfo) {
|
| 32 |
+
die("Receipt not found.");
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
$studentId = $receiptInfo['student_id'];
|
| 36 |
+
$paymentDate = $receiptInfo['payment_date'];
|
| 37 |
+
|
| 38 |
+
// 2. Fetch All Fees for Student (from receivables)
|
| 39 |
+
$sqlFees = "SELECT ar.fee_id, ar.actual_value as amount_billed,
|
| 40 |
+
ar.academic_session, ar.term_of_session,
|
| 41 |
+
sf.description as fee_description
|
| 42 |
+
FROM tb_account_receivables ar
|
| 43 |
+
JOIN tb_account_school_fees sf ON ar.fee_id = sf.id
|
| 44 |
+
WHERE ar.student_id = :sid
|
| 45 |
+
ORDER BY ar.academic_session ASC, ar.term_of_session ASC";
|
| 46 |
+
|
| 47 |
+
$stmtFees = $pdo->prepare($sqlFees);
|
| 48 |
+
$stmtFees->execute(['sid' => $studentId]);
|
| 49 |
+
$allFees = $stmtFees->fetchAll(PDO::FETCH_ASSOC);
|
| 50 |
+
|
| 51 |
+
$allocations = [];
|
| 52 |
+
$receiptTotalPaid = 0;
|
| 53 |
+
|
| 54 |
+
foreach ($allFees as $fee) {
|
| 55 |
+
// Calculate Paid To Date (up to this receipt's date)
|
| 56 |
+
$sqlPaid = "SELECT SUM(amount_paid) as total_paid
|
| 57 |
+
FROM tb_account_payment_registers
|
| 58 |
+
WHERE student_id = :sid
|
| 59 |
+
AND fee_id = :fid
|
| 60 |
+
AND academic_session = :as
|
| 61 |
+
AND term_of_session = :ts
|
| 62 |
+
AND payment_date <= :pd";
|
| 63 |
+
|
| 64 |
+
$stmtPaid = $pdo->prepare($sqlPaid);
|
| 65 |
+
$stmtPaid->execute([
|
| 66 |
+
'sid' => $studentId,
|
| 67 |
+
'fid' => $fee['fee_id'],
|
| 68 |
+
'as' => $fee['academic_session'],
|
| 69 |
+
'ts' => $fee['term_of_session'],
|
| 70 |
+
'pd' => $paymentDate
|
| 71 |
+
]);
|
| 72 |
+
$paidResult = $stmtPaid->fetch(PDO::FETCH_ASSOC);
|
| 73 |
+
$paidToDate = floatval($paidResult['total_paid'] ?? 0);
|
| 74 |
+
|
| 75 |
+
// Calculate Amount paid IN THIS RECEIPT (for total calculation)
|
| 76 |
+
$sqlReceiptPay = "SELECT SUM(amount_paid) as receipt_paid
|
| 77 |
+
FROM tb_account_payment_registers
|
| 78 |
+
WHERE receipt_no = :rno
|
| 79 |
+
AND fee_id = :fid
|
| 80 |
+
AND academic_session = :as
|
| 81 |
+
AND term_of_session = :ts";
|
| 82 |
+
$stmtReceiptPay = $pdo->prepare($sqlReceiptPay);
|
| 83 |
+
$stmtReceiptPay->execute([
|
| 84 |
+
'rno' => $receiptNo,
|
| 85 |
+
'fid' => $fee['fee_id'],
|
| 86 |
+
'as' => $fee['academic_session'],
|
| 87 |
+
'ts' => $fee['term_of_session']
|
| 88 |
+
]);
|
| 89 |
+
$receiptPayResult = $stmtReceiptPay->fetch(PDO::FETCH_ASSOC);
|
| 90 |
+
$paidInReceipt = floatval($receiptPayResult['receipt_paid'] ?? 0);
|
| 91 |
+
|
| 92 |
+
$receiptTotalPaid += $paidInReceipt;
|
| 93 |
+
$balance = floatval($fee['amount_billed']) - $paidToDate;
|
| 94 |
+
|
| 95 |
+
// Condition: Show if (Balance > 0) OR (PaidInReceipt > 0)
|
| 96 |
+
// Helps filter out old fully paid fees, but keeps current payments even if they zeroed the balance
|
| 97 |
+
if ($balance > 0.001 || $paidInReceipt > 0.001) {
|
| 98 |
+
$allocations[] = [
|
| 99 |
+
'description' => $fee['fee_description'],
|
| 100 |
+
'academic_session' => $fee['academic_session'],
|
| 101 |
+
'term_of_session' => $fee['term_of_session'],
|
| 102 |
+
'amount_billed' => floatval($fee['amount_billed']),
|
| 103 |
+
'amount' => $paidInReceipt,
|
| 104 |
+
'total_paid_to_date' => $paidToDate,
|
| 105 |
+
'balance' => $balance
|
| 106 |
+
];
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// 3. Prepare data structure for generator
|
| 111 |
+
$data = [
|
| 112 |
+
'receipt_no' => $receiptNo,
|
| 113 |
+
'student_name' => trim($receiptInfo['last_name'] . ' ' . $receiptInfo['first_name'] . ' ' . ($receiptInfo['other_name'] ?? '')),
|
| 114 |
+
'student_code' => $receiptInfo['student_code'],
|
| 115 |
+
'level_name' => $receiptInfo['level_name'] ?? '',
|
| 116 |
+
'payment_date' => $paymentDate,
|
| 117 |
+
'total_paid' => $receiptTotalPaid,
|
| 118 |
+
'allocations' => $allocations
|
| 119 |
+
];
|
| 120 |
+
|
| 121 |
+
// 4. Generate Image
|
| 122 |
+
$generator = new ReceiptGenerator();
|
| 123 |
+
$imageData = $generator->generate($data);
|
| 124 |
+
|
| 125 |
+
// 5. Output
|
| 126 |
+
ob_end_clean(); // Discard any warnings/output buffered so far
|
| 127 |
+
header('Content-Type: image/png');
|
| 128 |
+
header('Content-Disposition: attachment; filename="receipt_' . $receiptNo . '.png"');
|
| 129 |
+
header('Content-Length: ' . strlen($imageData));
|
| 130 |
+
echo $imageData;
|
| 131 |
+
|
| 132 |
+
} catch (Exception $e) {
|
| 133 |
+
die("Error generating receipt: " . $e->getMessage());
|
| 134 |
+
}
|
easypay-api/includes/ApiValidator.php
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
* @param array $allowedMethods Array of allowed HTTP methods (e.g. ['POST', 'GET'])
|
| 24 |
+
* @return array Validation result
|
| 25 |
+
*/
|
| 26 |
+
public function validateRequest($allowedMethods = ['POST'])
|
| 27 |
+
{
|
| 28 |
+
// Check request method
|
| 29 |
+
$method = $_SERVER['REQUEST_METHOD'];
|
| 30 |
+
if (!in_array($method, $allowedMethods)) {
|
| 31 |
+
return [
|
| 32 |
+
'valid' => false,
|
| 33 |
+
'error' => "Invalid request method. Allowed: " . implode(', ', $allowedMethods),
|
| 34 |
+
'http_code' => 405
|
| 35 |
+
];
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Check Content-Type header (only for methods with body)
|
| 39 |
+
if (in_array($method, ['POST', 'PUT', 'PATCH'])) {
|
| 40 |
+
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
| 41 |
+
if (stripos($contentType, 'application/json') === false) {
|
| 42 |
+
return [
|
| 43 |
+
'valid' => false,
|
| 44 |
+
'error' => 'Invalid Content-Type. Expected application/json',
|
| 45 |
+
'http_code' => 400
|
| 46 |
+
];
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// Validate API key if configured
|
| 51 |
+
if (!empty($this->apiKeys)) {
|
| 52 |
+
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
| 53 |
+
|
| 54 |
+
if (empty($authHeader)) {
|
| 55 |
+
return [
|
| 56 |
+
'valid' => false,
|
| 57 |
+
'error' => 'Missing Authorization header',
|
| 58 |
+
'http_code' => 401
|
| 59 |
+
];
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Extract Bearer token
|
| 63 |
+
if (!preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
|
| 64 |
+
return [
|
| 65 |
+
'valid' => false,
|
| 66 |
+
'error' => 'Invalid Authorization header format. Expected: Bearer <API_KEY>',
|
| 67 |
+
'http_code' => 401
|
| 68 |
+
];
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
$apiKey = $matches[1];
|
| 72 |
+
|
| 73 |
+
if (!in_array($apiKey, $this->apiKeys)) {
|
| 74 |
+
return [
|
| 75 |
+
'valid' => false,
|
| 76 |
+
'error' => 'Invalid API key',
|
| 77 |
+
'http_code' => 401
|
| 78 |
+
];
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return ['valid' => true];
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Parse and validate JSON payload
|
| 87 |
+
*
|
| 88 |
+
* @return array Parsed data or error
|
| 89 |
+
*/
|
| 90 |
+
public function parseJsonPayload()
|
| 91 |
+
{
|
| 92 |
+
$rawInput = file_get_contents('php://input');
|
| 93 |
+
|
| 94 |
+
if (empty($rawInput)) {
|
| 95 |
+
return [
|
| 96 |
+
'valid' => false,
|
| 97 |
+
'error' => 'Empty request body',
|
| 98 |
+
'http_code' => 400
|
| 99 |
+
];
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
$data = json_decode($rawInput, true);
|
| 103 |
+
|
| 104 |
+
if (json_last_error() !== JSON_ERROR_NONE) {
|
| 105 |
+
return [
|
| 106 |
+
'valid' => false,
|
| 107 |
+
'error' => 'Invalid JSON: ' . json_last_error_msg(),
|
| 108 |
+
'http_code' => 400
|
| 109 |
+
];
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return [
|
| 113 |
+
'valid' => true,
|
| 114 |
+
'data' => $data
|
| 115 |
+
];
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* Validate payment request fields
|
| 120 |
+
*
|
| 121 |
+
* @param array $data Request data
|
| 122 |
+
* @return array Validation result with errors
|
| 123 |
+
*/
|
| 124 |
+
public function validatePaymentRequest($data)
|
| 125 |
+
{
|
| 126 |
+
$errors = [];
|
| 127 |
+
|
| 128 |
+
// Validate student_id
|
| 129 |
+
if (empty($data['student_id'])) {
|
| 130 |
+
$errors['student_id'] = 'Student ID is required';
|
| 131 |
+
} elseif (!is_string($data['student_id']) && !is_numeric($data['student_id'])) {
|
| 132 |
+
$errors['student_id'] = 'Student ID must be a string or number';
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Validate teller_no
|
| 136 |
+
if (empty($data['teller_no'])) {
|
| 137 |
+
$errors['teller_no'] = 'Teller number is required';
|
| 138 |
+
} elseif (!is_string($data['teller_no']) && !is_numeric($data['teller_no'])) {
|
| 139 |
+
$errors['teller_no'] = 'Teller number must be a string or number';
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Validate amount
|
| 143 |
+
if (!isset($data['amount'])) {
|
| 144 |
+
$errors['amount'] = 'Amount is required';
|
| 145 |
+
} elseif (!is_numeric($data['amount'])) {
|
| 146 |
+
$errors['amount'] = 'Amount must be a number';
|
| 147 |
+
} elseif (floatval($data['amount']) <= 0) {
|
| 148 |
+
$errors['amount'] = 'Amount must be greater than zero';
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Validate payment_date
|
| 152 |
+
if (empty($data['payment_date'])) {
|
| 153 |
+
$errors['payment_date'] = 'Payment date is required';
|
| 154 |
+
} elseif (!strtotime($data['payment_date'])) {
|
| 155 |
+
$errors['payment_date'] = 'Invalid payment date format';
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
if (!empty($errors)) {
|
| 159 |
+
return [
|
| 160 |
+
'valid' => false,
|
| 161 |
+
'errors' => $errors,
|
| 162 |
+
'http_code' => 400
|
| 163 |
+
];
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
return ['valid' => true];
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Validate student exists in database
|
| 171 |
+
*
|
| 172 |
+
* @param string $studentId Student ID
|
| 173 |
+
* @return array Validation result with student data
|
| 174 |
+
*/
|
| 175 |
+
public function validateStudentExists($studentId)
|
| 176 |
+
{
|
| 177 |
+
$sql = "SELECT sr.id, sr.student_code,
|
| 178 |
+
CONCAT(sr.last_name, ' ', sr.first_name, ' ', COALESCE(sr.other_name, '')) AS full_name,
|
| 179 |
+
sr.admission_status,
|
| 180 |
+
al.level_name
|
| 181 |
+
FROM tb_student_registrations sr
|
| 182 |
+
LEFT JOIN tb_academic_levels al ON sr.level_id = al.id
|
| 183 |
+
WHERE sr.id = :student_id";
|
| 184 |
+
|
| 185 |
+
$stmt = $this->pdo->prepare($sql);
|
| 186 |
+
$stmt->execute(['student_id' => $studentId]);
|
| 187 |
+
$student = $stmt->fetch();
|
| 188 |
+
|
| 189 |
+
if (!$student) {
|
| 190 |
+
return [
|
| 191 |
+
'valid' => false,
|
| 192 |
+
'error' => "Student not found (ID searched: '$studentId')",
|
| 193 |
+
'http_code' => 404
|
| 194 |
+
];
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if ($student['admission_status'] !== 'Active') {
|
| 198 |
+
return [
|
| 199 |
+
'valid' => false,
|
| 200 |
+
'error' => 'Student is not active',
|
| 201 |
+
'http_code' => 400
|
| 202 |
+
];
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
return [
|
| 206 |
+
'valid' => true,
|
| 207 |
+
'student' => $student
|
| 208 |
+
];
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/**
|
| 212 |
+
* Validate teller number exists and has unreconciled amount
|
| 213 |
+
*
|
| 214 |
+
* @param string $tellerNo Teller number
|
| 215 |
+
* @return array Validation result with teller data
|
| 216 |
+
*/
|
| 217 |
+
public function validateTellerNumber($tellerNo)
|
| 218 |
+
{
|
| 219 |
+
$sql = "SELECT
|
| 220 |
+
bs.id,
|
| 221 |
+
bs.description,
|
| 222 |
+
bs.amount_paid,
|
| 223 |
+
bs.payment_date,
|
| 224 |
+
COALESCE(fp.total_registered_fee, 0.00) AS registered_amount,
|
| 225 |
+
(bs.amount_paid - COALESCE(fp.total_registered_fee, 0.00)) AS unreconciled_amount
|
| 226 |
+
FROM tb_account_bank_statements bs
|
| 227 |
+
LEFT JOIN (
|
| 228 |
+
SELECT teller_no, SUM(amount_paid) AS total_registered_fee
|
| 229 |
+
FROM tb_account_school_fee_payments
|
| 230 |
+
GROUP BY teller_no
|
| 231 |
+
) fp ON SUBSTRING_INDEX(bs.description, ' ', -1) = fp.teller_no
|
| 232 |
+
WHERE SUBSTRING_INDEX(bs.description, ' ', -1) = :teller_number
|
| 233 |
+
LIMIT 1";
|
| 234 |
+
|
| 235 |
+
$stmt = $this->pdo->prepare($sql);
|
| 236 |
+
$stmt->execute(['teller_number' => $tellerNo]);
|
| 237 |
+
$teller = $stmt->fetch();
|
| 238 |
+
|
| 239 |
+
if (!$teller) {
|
| 240 |
+
return [
|
| 241 |
+
'valid' => false,
|
| 242 |
+
'error' => 'Teller number not found',
|
| 243 |
+
'http_code' => 404
|
| 244 |
+
];
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
if ($teller['unreconciled_amount'] <= 0) {
|
| 248 |
+
return [
|
| 249 |
+
'valid' => false,
|
| 250 |
+
'error' => 'Teller number has no unreconciled amount',
|
| 251 |
+
'http_code' => 400
|
| 252 |
+
];
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
return [
|
| 256 |
+
'valid' => true,
|
| 257 |
+
'teller' => $teller
|
| 258 |
+
];
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/**
|
| 262 |
+
* Check for duplicate teller number usage
|
| 263 |
+
* Prevents the same teller number from being used multiple times
|
| 264 |
+
*
|
| 265 |
+
* @param string $tellerNo Teller number
|
| 266 |
+
* @param string $studentId Student ID (optional, for better error message)
|
| 267 |
+
* @return array Validation result
|
| 268 |
+
*/
|
| 269 |
+
public function checkTellerDuplicate($tellerNo, $studentId = null)
|
| 270 |
+
{
|
| 271 |
+
// Note: The system allows the same teller to be used for different students
|
| 272 |
+
// This check is to ensure the teller has available unreconciled amount
|
| 273 |
+
// The actual duplicate check is handled by the validateTellerNumber method
|
| 274 |
+
|
| 275 |
+
return ['valid' => true];
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
/**
|
| 279 |
+
* Sanitize input data
|
| 280 |
+
*
|
| 281 |
+
* @param array $data Input data
|
| 282 |
+
* @return array Sanitized data
|
| 283 |
+
*/
|
| 284 |
+
public function sanitizeInput($data)
|
| 285 |
+
{
|
| 286 |
+
return [
|
| 287 |
+
'student_id' => trim($data['student_id'] ?? ''),
|
| 288 |
+
'teller_no' => trim($data['teller_no'] ?? ''),
|
| 289 |
+
'amount' => floatval($data['amount'] ?? 0),
|
| 290 |
+
'payment_date' => trim($data['payment_date'] ?? '')
|
| 291 |
+
];
|
| 292 |
+
}
|
| 293 |
+
}
|
easypay-api/includes/PaymentProcessor.php
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 565 |
+
/**
|
| 566 |
+
* Get total outstanding balance with breakdown
|
| 567 |
+
*
|
| 568 |
+
* @param string $studentId
|
| 569 |
+
* @return array Total and breakdown
|
| 570 |
+
*/
|
| 571 |
+
public function getTotalOutstandingBalance($studentId)
|
| 572 |
+
{
|
| 573 |
+
// Reuse getOutstandingFees as it already provides the breakdown of unpaid items
|
| 574 |
+
$breakdown = $this->getOutstandingFees($studentId);
|
| 575 |
+
|
| 576 |
+
$totalOutstanding = 0;
|
| 577 |
+
foreach ($breakdown as $item) {
|
| 578 |
+
$totalOutstanding += floatval($item['outstanding_amount']);
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
return [
|
| 582 |
+
'student_id' => $studentId,
|
| 583 |
+
'currency' => 'NGN',
|
| 584 |
+
'total_outstanding' => $totalOutstanding,
|
| 585 |
+
'breakdown' => $breakdown
|
| 586 |
+
];
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
/**
|
| 590 |
+
* Get invoice (fee breakdown) for a specific term
|
| 591 |
+
* Shows all fees billed, paid, and balance
|
| 592 |
+
*
|
| 593 |
+
* @param string $studentId
|
| 594 |
+
* @param string $session
|
| 595 |
+
* @param string $term
|
| 596 |
+
* @return array List of fees
|
| 597 |
+
*/
|
| 598 |
+
public function getTermInvoice($studentId, $session, $term)
|
| 599 |
+
{
|
| 600 |
+
$sql = "SELECT
|
| 601 |
+
ar.fee_id,
|
| 602 |
+
sf.description,
|
| 603 |
+
ar.actual_value AS amount_billed,
|
| 604 |
+
COALESCE(SUM(sp.payment_to_date), 0) AS amount_paid,
|
| 605 |
+
(ar.actual_value - COALESCE(SUM(sp.payment_to_date), 0)) AS balance
|
| 606 |
+
FROM tb_account_receivables ar
|
| 607 |
+
INNER JOIN tb_account_school_fees sf ON ar.fee_id = sf.id
|
| 608 |
+
LEFT JOIN tb_account_student_payments sp ON
|
| 609 |
+
ar.student_id = sp.student_id AND
|
| 610 |
+
ar.fee_id = sp.fee_id AND
|
| 611 |
+
ar.academic_session = sp.academic_session AND
|
| 612 |
+
ar.term_of_session = sp.term_of_session
|
| 613 |
+
WHERE ar.student_id = :student_id
|
| 614 |
+
AND ar.academic_session = :session
|
| 615 |
+
AND ar.term_of_session = :term
|
| 616 |
+
GROUP BY ar.fee_id, sf.description, ar.actual_value";
|
| 617 |
+
|
| 618 |
+
$stmt = $this->pdo->prepare($sql);
|
| 619 |
+
$stmt->execute([
|
| 620 |
+
'student_id' => $studentId,
|
| 621 |
+
'session' => $session,
|
| 622 |
+
'term' => $term
|
| 623 |
+
]);
|
| 624 |
+
|
| 625 |
+
return $stmt->fetchAll();
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/**
|
| 629 |
+
* Get payment status summary and transactions for a term
|
| 630 |
+
*
|
| 631 |
+
* @param string $studentId
|
| 632 |
+
* @param string $session
|
| 633 |
+
* @param string $term
|
| 634 |
+
* @return array Summary and transactions
|
| 635 |
+
*/
|
| 636 |
+
public function getTermPaymentStatus($studentId, $session, $term)
|
| 637 |
+
{
|
| 638 |
+
// 1. Get Summary (Billed vs Paid)
|
| 639 |
+
// We can use getTermInvoice to aggregate this
|
| 640 |
+
$invoiceItems = $this->getTermInvoice($studentId, $session, $term);
|
| 641 |
+
|
| 642 |
+
$totalBilled = 0;
|
| 643 |
+
$totalPaid = 0;
|
| 644 |
+
$totalBalance = 0;
|
| 645 |
+
|
| 646 |
+
foreach ($invoiceItems as $item) {
|
| 647 |
+
$totalBilled += floatval($item['amount_billed']);
|
| 648 |
+
$totalPaid += floatval($item['amount_paid']);
|
| 649 |
+
$totalBalance += floatval($item['balance']);
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
// 2. Get Transactions (Payments made for this term)
|
| 653 |
+
// We look at the sum_payments table or specific fee payments
|
| 654 |
+
// Using sum_payments is better for receipts, but it tracks 'dominant' session/term.
|
| 655 |
+
// A better approach for "transactions" might be to look at where the money went.
|
| 656 |
+
// However, usually we want to see "Receipts issued for this term".
|
| 657 |
+
// Let's query tb_account_payment_registers which tracks receipts per fee,
|
| 658 |
+
// or tb_account_school_fee_sum_payments which tracks the main transaction.
|
| 659 |
+
|
| 660 |
+
// Let's use tb_account_school_fee_sum_payments for the main "Transactions" list
|
| 661 |
+
// filtering by the session/term assigned to the payment.
|
| 662 |
+
|
| 663 |
+
$sql = "SELECT
|
| 664 |
+
payment_date,
|
| 665 |
+
total_paid AS amount,
|
| 666 |
+
transaction_id,
|
| 667 |
+
id AS payment_ref
|
| 668 |
+
FROM tb_account_school_fee_sum_payments
|
| 669 |
+
WHERE student_id = :student_id
|
| 670 |
+
AND academic_session = :session
|
| 671 |
+
AND term_of_session = :term
|
| 672 |
+
ORDER BY payment_date DESC";
|
| 673 |
+
|
| 674 |
+
$stmt = $this->pdo->prepare($sql);
|
| 675 |
+
$stmt->execute([
|
| 676 |
+
'student_id' => $studentId,
|
| 677 |
+
'session' => $session,
|
| 678 |
+
'term' => $term
|
| 679 |
+
]);
|
| 680 |
+
$transactions = $stmt->fetchAll();
|
| 681 |
+
|
| 682 |
+
return [
|
| 683 |
+
'summary' => [
|
| 684 |
+
'total_billed' => $totalBilled,
|
| 685 |
+
'total_paid' => $totalPaid,
|
| 686 |
+
'outstanding' => $totalBalance
|
| 687 |
+
],
|
| 688 |
+
'transactions' => $transactions
|
| 689 |
+
];
|
| 690 |
+
}
|
| 691 |
+
}
|
easypay-api/includes/ReceiptGenerator.php
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?php
|
| 2 |
+
|
| 3 |
+
class ReceiptGenerator
|
| 4 |
+
{
|
| 5 |
+
private $fontPath;
|
| 6 |
+
private $logoPath;
|
| 7 |
+
|
| 8 |
+
public function __construct()
|
| 9 |
+
{
|
| 10 |
+
if (!extension_loaded('gd')) {
|
| 11 |
+
throw new Exception("The PHP GD extension is not enabled. Please enable it in your php.ini or install php-gd.");
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
// Try to find a suitable font
|
| 15 |
+
$fonts = [
|
| 16 |
+
'C:/Windows/Fonts/arial.ttf',
|
| 17 |
+
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
|
| 18 |
+
'/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',
|
| 19 |
+
'/usr/share/fonts/TTF/arial.ttf'
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
$this->fontPath = null;
|
| 23 |
+
foreach ($fonts as $font) {
|
| 24 |
+
if (file_exists($font)) {
|
| 25 |
+
$this->fontPath = $font;
|
| 26 |
+
break;
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
$this->logoPath = __DIR__ . '/../assets/logo.png';
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
public function generate($data)
|
| 34 |
+
{
|
| 35 |
+
$width = 900; // Increased width to accommodate 6 columns comfortably
|
| 36 |
+
// Calculate dynamic height
|
| 37 |
+
$baseHeight = 450;
|
| 38 |
+
$rowHeight = 40;
|
| 39 |
+
$numRows = count($data['allocations'] ?? []);
|
| 40 |
+
$height = $baseHeight + ($numRows * $rowHeight) + 150;
|
| 41 |
+
|
| 42 |
+
$image = imagecreatetruecolor($width, $height);
|
| 43 |
+
|
| 44 |
+
// Colors
|
| 45 |
+
$white = imagecolorallocate($image, 255, 255, 255);
|
| 46 |
+
$black = imagecolorallocate($image, 0, 0, 0);
|
| 47 |
+
$grey = imagecolorallocate($image, 100, 100, 100);
|
| 48 |
+
$lightGrey = imagecolorallocate($image, 240, 240, 240);
|
| 49 |
+
|
| 50 |
+
imagefilledrectangle($image, 0, 0, $width, $height, $white);
|
| 51 |
+
|
| 52 |
+
// Logo
|
| 53 |
+
if (file_exists($this->logoPath)) {
|
| 54 |
+
$logo = @imagecreatefrompng($this->logoPath);
|
| 55 |
+
if ($logo) {
|
| 56 |
+
$logoW = imagesx($logo);
|
| 57 |
+
$logoH = imagesy($logo);
|
| 58 |
+
$targetW = 100;
|
| 59 |
+
$targetH = ($targetW / $logoW) * $logoH;
|
| 60 |
+
|
| 61 |
+
imagecopyresampled($image, $logo, 40, 30, 0, 0, (int) $targetW, (int) $targetH, (int) $logoW, (int) $logoH);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// School Name Logic
|
| 66 |
+
$schoolName = "ACE JUNIOR COLLEGE";
|
| 67 |
+
$levelName = isset($data['level_name']) ? strtoupper($data['level_name']) : '';
|
| 68 |
+
// Check for Senior classes (SSS 1, SSS 2, SSS 3, or Senior)
|
| 69 |
+
if (strpos($levelName, 'SSS') !== false || strpos($levelName, 'SENIOR') !== false) {
|
| 70 |
+
$schoolName = "ACE SENIOR COLLEGE";
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Header
|
| 74 |
+
$this->centerText($image, 18, 50, $schoolName, $black);
|
| 75 |
+
$this->centerText($image, 10, 75, "...education for excellence", $grey);
|
| 76 |
+
$this->centerText($image, 10, 100, "1, Ebute - Igbogbo Road, Ipakodo, Ikorodu, Lagos, Nigeria", $black);
|
| 77 |
+
$this->centerText($image, 10, 120, "Phone No(s): 08027449739, 08077275777", $black);
|
| 78 |
+
$this->centerText($image, 10, 140, "mailto:info@acecollege.com.ng https://www.acecollege.com.ng", $black);
|
| 79 |
+
|
| 80 |
+
// Receipt Title
|
| 81 |
+
$titleY = 180;
|
| 82 |
+
$receiptNo = $data['receipt_no'] ?? 'N/A';
|
| 83 |
+
$this->centerText($image, 12, $titleY, "SCHOOL FEES RECEIPT - REFERENCE NO: $receiptNo", $black);
|
| 84 |
+
|
| 85 |
+
// Session Info
|
| 86 |
+
$session = $data['allocations'][0]['academic_session'] ?? '----';
|
| 87 |
+
$term = $data['allocations'][0]['term_of_session'] ?? '-';
|
| 88 |
+
$nextSession = is_numeric($session) ? ($session + 1) : '';
|
| 89 |
+
$levelPart = isset($data['level_name']) ? ' - ' . strtoupper($data['level_name']) : '';
|
| 90 |
+
$sessionStr = "$session/$nextSession ACADEMIC SESSION, TERM $term" . $levelPart;
|
| 91 |
+
$this->centerText($image, 10, $titleY + 25, $sessionStr, $grey);
|
| 92 |
+
|
| 93 |
+
// Student Box
|
| 94 |
+
$boxY = 230;
|
| 95 |
+
imagerectangle($image, 40, $boxY, $width - 40, $boxY + 40, $grey);
|
| 96 |
+
// We expect student_name to be passed in $data. If not, fallback.
|
| 97 |
+
$studentName = isset($data['student_name']) ? strtoupper($data['student_name']) : (isset($data['student_code']) ? "STUDENT CODE: " . $data['student_code'] : 'UNKNOWN STUDENT');
|
| 98 |
+
$this->centerText($image, 14, $boxY + 26, $studentName, $grey);
|
| 99 |
+
|
| 100 |
+
// Table Header
|
| 101 |
+
$tableY = 300;
|
| 102 |
+
// 6 Columns: Description, Term/Session, Amount Billed, Amount Paid, Paid To Date, To Balance
|
| 103 |
+
// Width 900. Margins 40. Active 820.
|
| 104 |
+
// Approx X: 40, 320, 440, 560, 680, 800 (Right Boundary for last col is width-40 = 860)
|
| 105 |
+
// Let's adjust spacing.
|
| 106 |
+
$cols = [40, 300, 440, 550, 660, 780];
|
| 107 |
+
|
| 108 |
+
imageline($image, 40, $tableY, $width - 40, $tableY, $black);
|
| 109 |
+
imageline($image, 40, $tableY + 30, $width - 40, $tableY + 30, $black);
|
| 110 |
+
|
| 111 |
+
if ($this->fontPath) {
|
| 112 |
+
imagettftext($image, 8, 0, $cols[0], $tableY + 20, $black, $this->fontPath, "FEE DESCRIPTION");
|
| 113 |
+
imagettftext($image, 8, 0, $cols[1], $tableY + 20, $black, $this->fontPath, "TERM/SESSION");
|
| 114 |
+
imagettftext($image, 8, 0, $cols[2], $tableY + 20, $black, $this->fontPath, "AMOUNT BILLED");
|
| 115 |
+
imagettftext($image, 8, 0, $cols[3], $tableY + 20, $black, $this->fontPath, "AMOUNT PAID");
|
| 116 |
+
imagettftext($image, 8, 0, $cols[4], $tableY + 20, $black, $this->fontPath, "PAID TO DATE");
|
| 117 |
+
imagettftext($image, 8, 0, $cols[5], $tableY + 20, $black, $this->fontPath, "TO BALANCE");
|
| 118 |
+
} else {
|
| 119 |
+
// Fallback font
|
| 120 |
+
imagestring($image, 3, $cols[0], $tableY + 5, "FEE DESCRIPTION", $black);
|
| 121 |
+
imagestring($image, 3, $cols[1], $tableY + 5, "TERM/SESSION", $black);
|
| 122 |
+
imagestring($image, 3, $cols[2], $tableY + 5, "BILLED", $black);
|
| 123 |
+
imagestring($image, 3, $cols[3], $tableY + 5, "PAID", $black);
|
| 124 |
+
imagestring($image, 3, $cols[4], $tableY + 5, "PAID TO DATE", $black);
|
| 125 |
+
imagestring($image, 3, $cols[5], $tableY + 5, "TO BALANCE", $black);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Rows
|
| 129 |
+
$currY = $tableY + 30;
|
| 130 |
+
$totalBilled = 0;
|
| 131 |
+
$totalPaidToDate = 0;
|
| 132 |
+
$totalBalance = 0;
|
| 133 |
+
// Total Paid is passed from outside
|
| 134 |
+
$totalAmountPaid = 0;
|
| 135 |
+
|
| 136 |
+
if (isset($data['allocations']) && is_array($data['allocations'])) {
|
| 137 |
+
foreach ($data['allocations'] as $alloc) {
|
| 138 |
+
$desc = $alloc['description'];
|
| 139 |
+
$session = $alloc['academic_session'] ?? '';
|
| 140 |
+
$term = $alloc['term_of_session'] ?? '';
|
| 141 |
+
|
| 142 |
+
// Format: Term/Session e.g. "1/2025" or "1st/2025"
|
| 143 |
+
// Assuming simpler "1/2025" format for space
|
| 144 |
+
$termSessionStr = "$term/$session";
|
| 145 |
+
|
| 146 |
+
$billedVal = $alloc['amount_billed'] ?? 0;
|
| 147 |
+
$paidVal = $alloc['amount'] ?? 0;
|
| 148 |
+
$paidToDateVal = $alloc['total_paid_to_date'] ?? 0;
|
| 149 |
+
$balanceVal = $alloc['balance'] ?? 0;
|
| 150 |
+
|
| 151 |
+
$totalBilled += $billedVal;
|
| 152 |
+
$totalAmountPaid += $paidVal;
|
| 153 |
+
$totalPaidToDate += $paidToDateVal;
|
| 154 |
+
$totalBalance += $balanceVal;
|
| 155 |
+
|
| 156 |
+
$billed = number_format($billedVal, 2);
|
| 157 |
+
$paid = number_format($paidVal, 2);
|
| 158 |
+
$paidToDate = number_format($paidToDateVal, 2);
|
| 159 |
+
$balance = number_format($balanceVal, 2);
|
| 160 |
+
|
| 161 |
+
$currY += 30;
|
| 162 |
+
|
| 163 |
+
if ($this->fontPath) {
|
| 164 |
+
imagettftext($image, 8, 0, $cols[0], $currY, $black, $this->fontPath, substr($desc, 0, 35));
|
| 165 |
+
imagettftext($image, 8, 0, $cols[1], $currY, $black, $this->fontPath, $termSessionStr); // Term/Session
|
| 166 |
+
} else {
|
| 167 |
+
imagestring($image, 3, $cols[0], $currY - 10, substr($desc, 0, 35), $black);
|
| 168 |
+
imagestring($image, 3, $cols[1], $currY - 10, $termSessionStr, $black);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// Right Alignment using approx right boundaries
|
| 172 |
+
// Col 2 (Billed): starts 440. Right align around 530?
|
| 173 |
+
// Col 3 (Paid): starts 550.
|
| 174 |
+
// Col 4 (PTD): starts 660.
|
| 175 |
+
// Col 5 (Bal): starts 780.
|
| 176 |
+
|
| 177 |
+
$rbBilled = 530;
|
| 178 |
+
$rbPaid = 640;
|
| 179 |
+
$rbPTD = 760;
|
| 180 |
+
$rbBal = $width - 40;
|
| 181 |
+
|
| 182 |
+
$this->rightAlignText($image, 8, $rbBilled, $currY, $billed, $black);
|
| 183 |
+
$this->rightAlignText($image, 8, $rbPaid, $currY, $paid, $black);
|
| 184 |
+
$this->rightAlignText($image, 8, $rbPTD, $currY, $paidToDate, $black);
|
| 185 |
+
$this->rightAlignText($image, 8, $rbBal, $currY, $balance, $black);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Total Row
|
| 190 |
+
$currY += 20;
|
| 191 |
+
imageline($image, 40, $currY, $width - 40, $currY, $black);
|
| 192 |
+
$currY += 26;
|
| 193 |
+
|
| 194 |
+
$totalPaidStr = number_format($data['total_paid'] ?? 0, 2);
|
| 195 |
+
$totalBilledStr = number_format($totalBilled, 2);
|
| 196 |
+
$totalPaidToDateStr = number_format($totalPaidToDate, 2);
|
| 197 |
+
$totalBalanceStr = number_format($totalBalance, 2);
|
| 198 |
+
|
| 199 |
+
// Columns for totals
|
| 200 |
+
$rbBilled = 530;
|
| 201 |
+
$rbPaid = 640;
|
| 202 |
+
$rbPTD = 760;
|
| 203 |
+
$rbBal = $width - 40;
|
| 204 |
+
|
| 205 |
+
$this->rightAlignText($image, 9, $rbBilled, $currY, $totalBilledStr, $grey);
|
| 206 |
+
$this->rightAlignText($image, 9, $rbPaid, $currY, $totalPaidStr, $grey);
|
| 207 |
+
$this->rightAlignText($image, 9, $rbPTD, $currY, $totalPaidToDateStr, $grey);
|
| 208 |
+
$this->rightAlignText($image, 9, $rbBal, $currY, $totalBalanceStr, $grey);
|
| 209 |
+
|
| 210 |
+
imageline($image, 40, $currY + 14, $width - 40, $currY + 14, $black);
|
| 211 |
+
|
| 212 |
+
// Footer
|
| 213 |
+
$footerY = $currY + 50;
|
| 214 |
+
if ($this->fontPath) {
|
| 215 |
+
imagettftext($image, 9, 0, 40, $footerY, $black, $this->fontPath, "RECEIPT DULY VERIFIED BY");
|
| 216 |
+
imagettftext($image, 9, 0, 40, $footerY + 20, $black, $this->fontPath, "BURSAR'S OFFICE");
|
| 217 |
+
} else {
|
| 218 |
+
imagestring($image, 3, 40, $footerY - 10, "RECEIPT DULY VERIFIED BY", $black);
|
| 219 |
+
imagestring($image, 3, 40, $footerY + 10, "BURSAR'S OFFICE", $black);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
$receivedY = $footerY;
|
| 223 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY, "RECEIVED IN", $black);
|
| 224 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY + 15, "BANK", $black);
|
| 225 |
+
|
| 226 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY + 40, "RECEIVED ON", $black);
|
| 227 |
+
$dateStr = isset($data['payment_date']) ? strtoupper(date("F d, Y", strtotime($data['payment_date']))) : date("F d, Y");
|
| 228 |
+
$this->rightAlignText($image, 9, $width - 40, $receivedY + 55, $dateStr, $black);
|
| 229 |
+
|
| 230 |
+
ob_start();
|
| 231 |
+
imagepng($image);
|
| 232 |
+
$content = ob_get_clean();
|
| 233 |
+
imagedestroy($image);
|
| 234 |
+
|
| 235 |
+
return $content;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
public function generateBase64($data)
|
| 239 |
+
{
|
| 240 |
+
$content = $this->generate($data);
|
| 241 |
+
return base64_encode($content);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
private function centerText($image, $size, $y, $text, $color)
|
| 245 |
+
{
|
| 246 |
+
if ($this->fontPath) {
|
| 247 |
+
try {
|
| 248 |
+
$box = imagettfbbox($size, 0, $this->fontPath, $text);
|
| 249 |
+
$textW = $box[2] - $box[0];
|
| 250 |
+
$x = (int) ((imagesx($image) - $textW) / 2);
|
| 251 |
+
imagettftext($image, $size, 0, $x, (int) $y, $color, $this->fontPath, $text);
|
| 252 |
+
return;
|
| 253 |
+
} catch (Exception $e) {
|
| 254 |
+
// Fallthrough to built-in font
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
// Fallback or if fontPath is missing
|
| 258 |
+
$gdSize = (int) min(5, max(1, floor($size / 3)));
|
| 259 |
+
$charWidth = imagefontwidth($gdSize);
|
| 260 |
+
$textW = strlen($text) * $charWidth;
|
| 261 |
+
$x = (int) ((imagesx($image) - $textW) / 2);
|
| 262 |
+
imagestring($image, $gdSize, $x, (int) $y, $text, $color);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
private function rightAlignText($image, $size, $rightX, $y, $text, $color)
|
| 266 |
+
{
|
| 267 |
+
if ($this->fontPath) {
|
| 268 |
+
try {
|
| 269 |
+
$box = imagettfbbox($size, 0, $this->fontPath, $text);
|
| 270 |
+
$textW = $box[2] - $box[0];
|
| 271 |
+
$x = (int) ($rightX - $textW);
|
| 272 |
+
imagettftext($image, $size, 0, $x, (int) $y, $color, $this->fontPath, $text);
|
| 273 |
+
return;
|
| 274 |
+
} catch (Exception $e) {
|
| 275 |
+
// Fallthrough
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
$gdSize = (int) min(5, max(1, floor($size / 3)));
|
| 279 |
+
$charWidth = imagefontwidth($gdSize);
|
| 280 |
+
$x = (int) ($rightX - (strlen($text) * $charWidth));
|
| 281 |
+
imagestring($image, $gdSize, $x, (int) $y, $text, $color);
|
| 282 |
+
}
|
| 283 |
+
}
|
easypay-api/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-api/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-api/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-api/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>
|