kingkay000 commited on
Commit
e31284f
·
verified ·
1 Parent(s): bdca4f9

Upload 25 files

Browse files
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>