Compare commits
5 Commits
feature/ik
...
ebd7efe21c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd7efe21c | ||
|
|
6ff20c1ffc | ||
|
|
35c177b11d | ||
|
|
acd7f563b1 | ||
|
|
5768d8a7af |
@@ -94,10 +94,13 @@ RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L]
|
|||||||
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
|
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
|
||||||
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
|
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
|
||||||
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
|
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
|
||||||
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
|
RewriteRule ^admin_transactions$ src/admin/admin_transactions.php [L]
|
||||||
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
|
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
|
||||||
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
|
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
|
||||||
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
|
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
|
||||||
|
RewriteRule ^admin_courses$ /src/admin/admin_courses.php [L,QSA]
|
||||||
|
RewriteRule ^manage_courses$ /src/admin/manage_courses.php [L,QSA]
|
||||||
|
|
||||||
|
|
||||||
# === API/AJAX ENDPOINTS ===
|
# === API/AJAX ENDPOINTS ===
|
||||||
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
|
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
|
||||||
@@ -108,6 +111,8 @@ RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
|
|||||||
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
|
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
|
||||||
|
|
||||||
# === PROCESSORS ===
|
# === PROCESSORS ===
|
||||||
|
RewriteRule ^process_course$ /src/processors/process_course.php [L,QSA]
|
||||||
|
RewriteRule ^delete_course$ /src/processors/delete_course.php [L,QSA]
|
||||||
RewriteRule ^validate_login$ src/processors/validate_login.php [L]
|
RewriteRule ^validate_login$ src/processors/validate_login.php [L]
|
||||||
RewriteRule ^register_user$ src/processors/register_user.php [L]
|
RewriteRule ^register_user$ src/processors/register_user.php [L]
|
||||||
RewriteRule ^process_application$ src/processors/process_application.php [L]
|
RewriteRule ^process_application$ src/processors/process_application.php [L]
|
||||||
@@ -156,7 +161,7 @@ RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
|||||||
|
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
php_flag display_errors On
|
php_flag display_errors Off
|
||||||
# php_value error_reporting -1
|
# php_value error_reporting -1
|
||||||
RedirectMatch 403 ^/\.well-known
|
RedirectMatch 403 ^/\.well-known
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|||||||
@@ -286,10 +286,11 @@ if ($headerStyle === 'light') {
|
|||||||
<li><a href="admin_blogs">Manage Blogs</a></li>
|
<li><a href="admin_blogs">Manage Blogs</a></li>
|
||||||
<li><a href="admin_events">Manage Events</a></li>
|
<li><a href="admin_events">Manage Events</a></li>
|
||||||
<li><a href="admin_trips">Manage Trips</a></li>
|
<li><a href="admin_trips">Manage Trips</a></li>
|
||||||
|
<li><a href="admin_courses">Manage Courses</a></li>
|
||||||
<li><a href="admin_trip_bookings">Trip Bookings</a></li>
|
<li><a href="admin_trip_bookings">Trip Bookings</a></li>
|
||||||
<li><a href="admin_course_bookings">Course Bookings</a></li>
|
<li><a href="admin_course_bookings">Course Bookings</a></li>
|
||||||
<li><a href="admin_efts">EFT Payments</a></li>
|
<li><a href="admin_transactions">iKhokha Payment History</a></li>
|
||||||
<li><a href="process_payments">Process Payments</a></li>
|
<!-- <li><a href="process_payments">Process Payments</a></li> -->
|
||||||
<?php if ($role === 'superadmin') { ?>
|
<?php if ($role === 'superadmin') { ?>
|
||||||
<li><a href="admin_visitors">Visitor Log</a></li>
|
<li><a href="admin_visitors">Visitor Log</a></li>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ if (!empty($bannerImages)) {
|
|||||||
<div style="padding-top: 50px; padding-bottom: 50px;">
|
<div style="padding-top: 50px; padding-bottom: 50px;">
|
||||||
<img style="width: 250px; margin-bottom: 20px;" src="assets/images/logos/weblogo2.png" alt="Logo">
|
<img style="width: 250px; margin-bottom: 20px;" src="assets/images/logos/weblogo2.png" alt="Logo">
|
||||||
<h1 class="hero-title" data-aos="flip-up" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
|
<h1 class="hero-title" data-aos="flip-up" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
|
||||||
Welcome to<br>the 4 Wheel Drive Club<br>of Southern Africa
|
Welcome to<br>the Four Wheel Drive Club<br>of Southern Africa
|
||||||
</h1>
|
</h1>
|
||||||
<a href="membership.php" class="theme-btn style-two bgc-secondary" style="margin-top: 20px; background-color: #e90000; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
<a href="membership.php" class="theme-btn style-two bgc-secondary" style="margin-top: 20px; background-color: #e90000; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||||
<span data-hover="Become a Member">Become a Member</span>
|
<span data-hover="Become a Member">Become a Member</span>
|
||||||
|
|||||||
170
progress.log
170
progress.log
@@ -1,48 +1,122 @@
|
|||||||
[2025-12-14 16:27:25] Testing Log Entry at 2025-12-14 16:27:25
|
[2025-12-15 12:32:19] AJAX BLOCK ENTERED
|
||||||
[2025-12-14 16:28:03] Testing Log Entry at 2025-12-14 16:28:03
|
[2025-12-15 12:32:19] startDate=2025-10-16
|
||||||
[2025-12-14 16:28:18] Testing Log Entry at 2025-12-14 16:28:18
|
[2025-12-15 12:32:19] endDate=2025-12-15
|
||||||
[2025-12-14 18:30:36] --- iKhokha WEBHOOK DEBUG ---
|
[2025-12-15 12:32:19] APP ID present: YES
|
||||||
[2025-12-14 18:30:36] PATH: /src/api/ikhokha_webhook.php
|
[2025-12-15 12:32:19] APP SECRET present: YES
|
||||||
[2025-12-14 18:30:36] RAW BODY: {"paylinkID":"cwm225jy497j0qc","status":"SUCCESS","externalTransactionID":"693ee604c8e38","responseCode":"00","text":null}
|
[2025-12-15 12:32:19] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
[2025-12-14 18:30:36] IK-SIGN: 3a2b8c9da4f1aff63c011adec10552450c5b6605f587e82ab1272914715fd4cf
|
[2025-12-15 12:32:19] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||||
[2025-12-14 18:30:36] iKhokha webhook: signature mismatch
|
[2025-12-15 12:32:19] CURL HTTP CODE: 422
|
||||||
[2025-12-14 18:30:36] EXPECTED SIGN: f8356ee6b609e9bd44221fa71851b06171bbb7523c52dca4bde605e3bd93f654
|
[2025-12-15 12:32:19] CURL ERROR: none
|
||||||
[2025-12-14 18:30:37] --- iKhokha WEBHOOK DEBUG ---
|
[2025-12-15 12:32:19] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
[2025-12-14 18:30:37] PATH: /src/api/ikhokha_webhook.php
|
[2025-12-15 12:33:31] AJAX BLOCK ENTERED
|
||||||
[2025-12-14 18:30:37] RAW BODY: {"paylinkID":"cwm225jy497j0qc","status":"SUCCESS","externalTransactionID":"693ee604c8e38","responseCode":"00","text":null}
|
[2025-12-15 12:33:31] startDate=2025-10-16
|
||||||
[2025-12-14 18:30:37] IK-SIGN: 3a2b8c9da4f1aff63c011adec10552450c5b6605f587e82ab1272914715fd4cf
|
[2025-12-15 12:33:31] endDate=2025-12-15
|
||||||
[2025-12-14 18:30:37] iKhokha webhook: signature mismatch
|
[2025-12-15 12:33:31] APP ID present: YES
|
||||||
[2025-12-14 18:30:37] EXPECTED SIGN: f8356ee6b609e9bd44221fa71851b06171bbb7523c52dca4bde605e3bd93f654
|
[2025-12-15 12:33:31] APP SECRET present: YES
|
||||||
[2025-12-14 18:36:19] --- iKhokha WEBHOOK DEBUG ---
|
[2025-12-15 12:33:31] IKHOKHA PAYLOAD (FULL URL): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
[2025-12-14 18:36:19] PATH: /api/ikhokha_webhook.php
|
[2025-12-15 12:33:31] IKHOKHA IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||||
[2025-12-14 18:36:19] RAW BODY: {"paylinkID":"xx6225jyhx6x5n1","status":"SUCCESS","externalTransactionID":"693ee75cec963","responseCode":"00","text":null}
|
[2025-12-15 12:33:31] CURL HTTP CODE: 422
|
||||||
[2025-12-14 18:36:19] IK-SIGN: 8fb11ceb8ea6b2cd6fec1773719927889f02764b0f04ad3c8543edbc9f74cf43
|
[2025-12-15 12:33:31] CURL ERROR: none
|
||||||
[2025-12-14 18:36:19] iKhokha webhook: signature mismatch
|
[2025-12-15 12:33:31] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
[2025-12-14 18:36:19] EXPECTED SIGN: 7a594957e7dc8775a6f9e1f8e3a1292f4dcd10268facaec57735f937b1cd9949
|
[2025-12-15 12:33:59] AJAX BLOCK ENTERED
|
||||||
[2025-12-14 18:36:19] --- iKhokha WEBHOOK DEBUG ---
|
[2025-12-15 12:33:59] startDate=2025-10-16
|
||||||
[2025-12-14 18:36:19] PATH: /api/ikhokha_webhook.php
|
[2025-12-15 12:33:59] endDate=2025-12-15
|
||||||
[2025-12-14 18:36:19] RAW BODY: {"paylinkID":"xx6225jyhx6x5n1","status":"SUCCESS","externalTransactionID":"693ee75cec963","responseCode":"00","text":null}
|
[2025-12-15 12:33:59] APP ID present: YES
|
||||||
[2025-12-14 18:36:19] IK-SIGN: 8fb11ceb8ea6b2cd6fec1773719927889f02764b0f04ad3c8543edbc9f74cf43
|
[2025-12-15 12:33:59] APP SECRET present: YES
|
||||||
[2025-12-14 18:36:19] iKhokha webhook: signature mismatch
|
[2025-12-15 12:33:59] IKHOKHA PAYLOAD (FULL URL): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
[2025-12-14 18:36:19] EXPECTED SIGN: 7a594957e7dc8775a6f9e1f8e3a1292f4dcd10268facaec57735f937b1cd9949
|
[2025-12-15 12:33:59] IKHOKHA IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||||
[2025-12-14 18:55:49] --- iKhokha WEBHOOK DEBUG ---
|
[2025-12-15 12:34:00] CURL HTTP CODE: 422
|
||||||
[2025-12-14 18:55:49] RAW BODY: {"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
[2025-12-15 12:34:00] CURL ERROR: none
|
||||||
[2025-12-14 18:55:49] IK-SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
[2025-12-15 12:34:00] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
[2025-12-14 18:55:49] SIGN BASE STRING: | CONTEXT: https://beta.4wdcsa.co.za/src/api/ikhokha_webhook.php{"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
[2025-12-15 12:37:06] AJAX BLOCK ENTERED
|
||||||
[2025-12-14 18:55:49] iKhokha webhook: signature mismatch
|
[2025-12-15 12:37:06] startDate=2025-10-16
|
||||||
[2025-12-14 18:55:49] EXPECTED SIGN: 75c491d05ac5006b3ad4fcec20c870e860a66ea03ed5a3e8900d6e29cd601445
|
[2025-12-15 12:37:06] endDate=2025-12-15
|
||||||
[2025-12-14 18:55:49] RECEIVED SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
[2025-12-15 12:37:06] APP ID present: YES
|
||||||
[2025-12-14 18:55:49] --- iKhokha WEBHOOK DEBUG ---
|
[2025-12-15 12:37:06] APP SECRET present: YES
|
||||||
[2025-12-14 18:55:49] RAW BODY: {"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
[2025-12-15 12:37:06] IKHOKHA ENDPOINT (REQUEST): https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
[2025-12-14 18:55:49] IK-SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
[2025-12-15 12:37:06] IKHOKHA PAYLOAD (SIGNED): https://api.ikhokha.com/public-api/v1/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
[2025-12-14 18:55:49] SIGN BASE STRING: | CONTEXT: https://beta.4wdcsa.co.za/src/api/ikhokha_webhook.php{"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
[2025-12-15 12:37:06] IKHOKHA IK-SIGN: 418e48921e566e5804b58f65e1ca4a28dba4d69de3611d1cf7f90f865490f42d
|
||||||
[2025-12-14 18:55:49] iKhokha webhook: signature mismatch
|
[2025-12-15 12:37:06] CURL HTTP CODE: 422
|
||||||
[2025-12-14 18:55:49] EXPECTED SIGN: 75c491d05ac5006b3ad4fcec20c870e860a66ea03ed5a3e8900d6e29cd601445
|
[2025-12-15 12:37:06] CURL ERROR: none
|
||||||
[2025-12-14 18:55:49] RECEIVED SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
[2025-12-15 12:37:06] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
[2025-12-14 20:15:55] --- iKhokha WEBHOOK DEBUG ---
|
[2025-12-15 12:56:21] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
[2025-12-14 20:15:55] RAW BODY: {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null}
|
[2025-12-15 12:56:21] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
[2025-12-14 20:15:55] IK-SIGN: bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557
|
[2025-12-15 12:56:37] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
[2025-12-14 20:15:55] ⚠️ IKHOKHA SIGNATURE CHECK BYPASSED
|
[2025-12-15 12:56:37] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
[2025-12-14 20:15:55] Parsed externalTransactionID: 693efeaca71a9
|
[2025-12-15 12:57:04] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
[2025-12-14 20:15:55] Parsed providerPaymentId: ys5225k4z56x0mm
|
[2025-12-15 12:57:04] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
|
||||||
[2025-12-14 20:15:55] Parsed providerStatus: SUCCESS
|
[2025-12-15 12:57:30] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 12:57:30] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 12:57:32] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 12:57:32] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 12:57:34] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 12:57:34] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 12:58:00] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA Endpoint
|
||||||
|
[2025-12-15 12:58:00] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 12:58:00] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 12:58:04] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA Endpoint
|
||||||
|
[2025-12-15 12:58:04] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 12:58:04] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 12:58:17] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 12:58:17] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 12:58:17] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 12:58:48] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 12:58:48] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 12:58:48] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 13:00:13] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 13:00:13] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:00:13] 13e6e02a7ccad937bc27b31038373d48d8ba2700a7ba8d9a7a2e4f9b07378692 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 13:00:29] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 13:00:29] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:00:29] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 13:03:10] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 13:03:10] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:03:10] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 13:03:19] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 13:03:19] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:03:19] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 13:05:51] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||||
|
[2025-12-15 13:05:51] "{\"paylinkID\":\"ys5225k4z56x0mm\",\"status\":\"SUCCESS\",\"externalTransactionID\":\"693efeaca71a9\",\"responseCode\":\"00\",\"text\":null}" | CONTEXT: IKHOKHA Stringified Body
|
||||||
|
[2025-12-15 13:06:29] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||||
|
[2025-12-15 13:06:29] "{\"paylinkID\":\"ys5225k4z56x0mm\",\"status\":\"SUCCESS\",\"externalTransactionID\":\"693efeaca71a9\",\"responseCode\":\"00\",\"text\":null}" | CONTEXT: IKHOKHA Stringified Body
|
||||||
|
[2025-12-15 13:06:29] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:06:29] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
|
||||||
|
[2025-12-15 13:06:42] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||||
|
[2025-12-15 13:06:42] {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null} | CONTEXT: IKHOKHA Stringified Body
|
||||||
|
[2025-12-15 13:06:42] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:06:42] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
|
||||||
|
[2025-12-15 13:07:09] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||||
|
[2025-12-15 13:07:09] {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null} | CONTEXT: IKHOKHA Stringified Body
|
||||||
|
[2025-12-15 13:07:09] /src/api/ikhokha_webhook.php\"{\\"paylinkID\\":\\"ys5225k4z56x0mm\\",\\"status\\":\\"SUCCESS\\",\\"externalTransactionID\\":\\"693efeaca71a9\\",\\"responseCode\\":\\"00\\",\\"text\\":null}\" | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:07:09] 43a6a56af31c276174953e115eb41402f12969fedab5b673dd34327cd7135a75 | CONTEXT: IKHOKHA Generated Signature
|
||||||
|
[2025-12-15 13:09:39] bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557 | CONTEXT: IKHOKHA Signature from Webhook
|
||||||
|
[2025-12-15 13:19:12] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 13:19:12] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:19:12] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 13:36:28] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 13:36:28] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:36:28] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 13:36:54] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 13:36:54] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 13:36:54] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 15:41:25] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 15:41:25] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 15:41:25] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 15:43:53] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 15:43:53] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 15:43:53] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 15:44:29] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 15:44:29] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 15:44:29] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 15:46:02] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 15:46:02] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 15:46:02] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 15:47:46] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 15:47:46] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 15:47:46] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 15:47:51] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 15:47:51] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 15:47:51] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-15 15:48:43] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-15 15:48:43] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-15 15:48:43] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
|||||||
56
src/admin/_admin_tx_debug.log
Normal file
56
src/admin/_admin_tx_debug.log
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
[2025-12-15 12:28:42] FILE HIT
|
||||||
|
[2025-12-15 12:28:42] AJAX BLOCK ENTERED
|
||||||
|
[2025-12-15 12:28:42] startDate=2025-10-16
|
||||||
|
[2025-12-15 12:28:42] endDate=2025-12-15
|
||||||
|
[2025-12-15 12:28:42] APP ID present: YES
|
||||||
|
[2025-12-15 12:28:42] APP SECRET present: YES
|
||||||
|
[2025-12-15 12:28:42] PAYLOAD: /public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
|
[2025-12-15 12:28:42] IK-SIGN: 3d610c60c8306cd1d5c99b2639f0e810594f8ffb9306a98d703f691173dab47d
|
||||||
|
[2025-12-15 12:28:44] CURL HTTP CODE: 422
|
||||||
|
[2025-12-15 12:28:44] CURL ERROR: none
|
||||||
|
[2025-12-15 12:28:44] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
|
[2025-12-15 12:28:51] FILE HIT
|
||||||
|
[2025-12-15 12:28:51] AJAX BLOCK ENTERED
|
||||||
|
[2025-12-15 12:28:51] startDate=2025-10-16
|
||||||
|
[2025-12-15 12:28:51] endDate=2025-12-15
|
||||||
|
[2025-12-15 12:28:51] APP ID present: YES
|
||||||
|
[2025-12-15 12:28:51] APP SECRET present: YES
|
||||||
|
[2025-12-15 12:28:51] PAYLOAD: /public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
|
[2025-12-15 12:28:51] IK-SIGN: 3d610c60c8306cd1d5c99b2639f0e810594f8ffb9306a98d703f691173dab47d
|
||||||
|
[2025-12-15 12:28:51] CURL HTTP CODE: 422
|
||||||
|
[2025-12-15 12:28:51] CURL ERROR: none
|
||||||
|
[2025-12-15 12:28:51] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
|
[2025-12-15 12:30:54] FILE HIT
|
||||||
|
[2025-12-15 12:30:54] AJAX BLOCK ENTERED
|
||||||
|
[2025-12-15 12:30:54] startDate=2025-10-16
|
||||||
|
[2025-12-15 12:30:54] endDate=2025-12-15
|
||||||
|
[2025-12-15 12:30:54] APP ID present: YES
|
||||||
|
[2025-12-15 12:30:54] APP SECRET present: YES
|
||||||
|
[2025-12-15 12:30:54] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
|
[2025-12-15 12:30:54] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||||
|
[2025-12-15 12:30:55] CURL HTTP CODE: 422
|
||||||
|
[2025-12-15 12:30:55] CURL ERROR: none
|
||||||
|
[2025-12-15 12:30:55] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
|
[2025-12-15 12:31:13] FILE HIT
|
||||||
|
[2025-12-15 12:31:13] AJAX BLOCK ENTERED
|
||||||
|
[2025-12-15 12:31:13] startDate=2025-10-16
|
||||||
|
[2025-12-15 12:31:13] endDate=2025-12-15
|
||||||
|
[2025-12-15 12:31:13] APP ID present: YES
|
||||||
|
[2025-12-15 12:31:13] APP SECRET present: YES
|
||||||
|
[2025-12-15 12:31:13] PAYLOAD: https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2025-10-16&endDate=2025-12-15
|
||||||
|
[2025-12-15 12:31:13] IK-SIGN: ced35ab201ad54e8f8b5935d2778c4ec7e75af0102d40d9c4515f8118ca8b5dd
|
||||||
|
[2025-12-15 12:31:13] CURL HTTP CODE: 422
|
||||||
|
[2025-12-15 12:31:13] CURL ERROR: none
|
||||||
|
[2025-12-15 12:31:13] RAW RESPONSE: {"error":"Invalid Signature"}
|
||||||
|
[2025-12-15 12:31:21] FILE HIT
|
||||||
|
[2025-12-15 12:31:47] FILE HIT
|
||||||
|
[2025-12-15 12:31:47] FILE HIT
|
||||||
|
[2025-12-15 12:31:58] FILE HIT
|
||||||
|
[2025-12-15 12:32:18] FILE HIT
|
||||||
|
[2025-12-15 12:32:19] FILE HIT
|
||||||
|
[2025-12-15 12:33:30] FILE HIT
|
||||||
|
[2025-12-15 12:33:31] FILE HIT
|
||||||
|
[2025-12-15 12:33:59] FILE HIT
|
||||||
|
[2025-12-15 12:33:59] FILE HIT
|
||||||
|
[2025-12-15 12:37:05] FILE HIT
|
||||||
|
[2025-12-15 12:37:06] FILE HIT
|
||||||
161
src/admin/admin_courses.php
Normal file
161
src/admin/admin_courses.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
$rootPath = dirname(dirname(__DIR__));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . '/src/config/connection.php');
|
||||||
|
require_once($rootPath . '/src/config/functions.php');
|
||||||
|
require_once($rootPath . '/header.php');
|
||||||
|
checkAdmin();
|
||||||
|
checkUserSession();
|
||||||
|
|
||||||
|
$pageTitle = 'Manage Courses';
|
||||||
|
$breadcrumbs = [['Home' => 'index']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
|
||||||
|
// Fetch all courses
|
||||||
|
$courses_query = "
|
||||||
|
SELECT
|
||||||
|
course_id, course_type, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email, code
|
||||||
|
FROM courses
|
||||||
|
ORDER BY date DESC
|
||||||
|
";
|
||||||
|
|
||||||
|
$result = $conn->query($courses_query);
|
||||||
|
$courses = [];
|
||||||
|
if ($result && $result->num_rows > 0) {
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$courses[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Courses Management Area start -->
|
||||||
|
<section class="blog-list-page py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||||
|
<h2 style="margin: 0;">Manage Courses</h2>
|
||||||
|
<a href="manage_courses" class="theme-btn create-album-btn">
|
||||||
|
<i class="far fa-plus"></i> New Course
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']);
|
||||||
|
endif;?>
|
||||||
|
|
||||||
|
<?php if (count($courses) > 0): ?>
|
||||||
|
<input type="text" class="filter-input" placeholder="Filter courses...">
|
||||||
|
<div class="courses-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<?php foreach ($courses as $course):
|
||||||
|
$available = intval($course['capacity']) - intval($course['booked']);
|
||||||
|
$type_label = strtoupper($course['course_type']);
|
||||||
|
if ($course['course_type'] == 'driver_training') {
|
||||||
|
$type_label = 'Driver Training';
|
||||||
|
} elseif ($course['course_type'] == 'bush_mechanics') {
|
||||||
|
$type_label = 'Bush Mechanics';
|
||||||
|
} elseif ($course['course_type'] == 'rescue_recovery') {
|
||||||
|
$type_label = 'Rescue & Recovery';
|
||||||
|
} elseif ($course['course_type'] == 'ladies_driver_training') {
|
||||||
|
$type_label = 'Ladies Driver Training';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<div class="content" style="width:100%;">
|
||||||
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0"><?php echo $type_label; ?></h5>
|
||||||
|
<small class="text-muted"><?php echo htmlspecialchars($course['course_type']); ?> — <?php echo date('M d, Y', strtotime($course['date'])); ?></small><br
|
||||||
|
<small class="text-muted"><?php echo $course['code'] ? 'Code: ' . htmlspecialchars($course['code']) : ''; ?></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="margin: 10px 0;">
|
||||||
|
<strong>Instructor:</strong> <?php echo htmlspecialchars($course['instructor']); ?> (<?php echo htmlspecialchars($course['instructor_email']); ?>)<br>
|
||||||
|
<strong>Capacity:</strong> <?php echo intval($course['booked']); ?> / <?php echo intval($course['capacity']); ?> <strong>Available:</strong> <?php echo $available; ?><br>
|
||||||
|
<strong>Costs:</strong> Members: R <?php echo number_format($course['cost_members'],2); ?> | Non-Members: R <?php echo number_format($course['cost_nonmembers'],2); ?>
|
||||||
|
</p>
|
||||||
|
<div class="destination-footer">
|
||||||
|
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
|
||||||
|
<a href="manage_courses?course_id=<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
|
||||||
|
<button type="button" class="delete-course" data-course-id="<?php echo $course['course_id']; ?>" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="no-courses">
|
||||||
|
<p>No courses found. <a href="manage_courses">Create one</a></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Courses Management Area end -->
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const filterInput = document.querySelector('.filter-input');
|
||||||
|
const cards = document.querySelectorAll('.destination-item');
|
||||||
|
|
||||||
|
if (cards.length === 0 && filterInput) {
|
||||||
|
filterInput.style.display = "none";
|
||||||
|
} else if (filterInput) {
|
||||||
|
filterInput.addEventListener("input", function() {
|
||||||
|
const filterValue = filterInput.value.trim().toLowerCase();
|
||||||
|
cards.forEach(card => {
|
||||||
|
const cardText = card.textContent.trim().toLowerCase();
|
||||||
|
card.style.display = cardText.includes(filterValue) ? "" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle delete button clicks
|
||||||
|
document.querySelectorAll('.delete-course').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
if (!confirm('Are you sure you want to delete this course? This action cannot be undone.')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseId = this.dataset.courseId;
|
||||||
|
const card = this.closest('.destination-item');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('course_id', courseId);
|
||||||
|
|
||||||
|
fetch('delete_course', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
alert('Course deleted successfully!');
|
||||||
|
card.remove();
|
||||||
|
if (document.querySelectorAll('.destination-item').length === 0) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error:', err);
|
||||||
|
alert('Delete failed due to network error.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
248
src/admin/admin_transactions.php
Normal file
248
src/admin/admin_transactions.php
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
$headerStyle = 'light';
|
||||||
|
$rootPath = dirname(dirname(__DIR__));
|
||||||
|
include_once($rootPath . '/header.php');
|
||||||
|
checkAdmin();
|
||||||
|
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th::after {
|
||||||
|
content: '\25B2';
|
||||||
|
/* Up arrow */
|
||||||
|
font-size: 0.8em;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th.asc::after {
|
||||||
|
content: '\25B2';
|
||||||
|
/* Up arrow */
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th.desc::after {
|
||||||
|
content: '\25BC';
|
||||||
|
/* Down arrow */
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(odd) {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) {
|
||||||
|
background-color: rgb(255, 255, 255);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) td:first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(even) td:last-child {
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
/* margin-bottom: 20px; */
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: rgb(255, 255, 255);
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infobox {
|
||||||
|
color: #484848;
|
||||||
|
background: #f9f9f7;
|
||||||
|
border: 1px solid #d8d8d8;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const table = document.querySelector("table");
|
||||||
|
const headers = table.querySelectorAll("thead th");
|
||||||
|
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
||||||
|
const filterInput = document.getElementById("filterInput");
|
||||||
|
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
header.addEventListener("click", () => {
|
||||||
|
const sortedRows = rows.sort((a, b) => {
|
||||||
|
const aText = a.cells[index].textContent.trim().toLowerCase();
|
||||||
|
const bText = b.cells[index].textContent.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (aText < bText) return -1;
|
||||||
|
if (aText > bText) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (header.classList.contains("asc")) {
|
||||||
|
header.classList.remove("asc");
|
||||||
|
header.classList.add("desc");
|
||||||
|
sortedRows.reverse();
|
||||||
|
} else {
|
||||||
|
headers.forEach(h => h.classList.remove("asc", "desc"));
|
||||||
|
header.classList.add("asc");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = table.querySelector("tbody");
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
sortedRows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
filterInput.addEventListener("input", function() {
|
||||||
|
const filterValue = filterInput.value.trim().toLowerCase();
|
||||||
|
rows.forEach(row => {
|
||||||
|
const rowText = row.textContent.trim().toLowerCase();
|
||||||
|
row.style.display = rowText.includes(filterValue) ? "" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<!-- Page Banner Start -->
|
||||||
|
<?php
|
||||||
|
$bannerFolder = 'assets/images/banners/';
|
||||||
|
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||||
|
|
||||||
|
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
|
||||||
|
if (!empty($bannerImages)) {
|
||||||
|
$randomBanner = $bannerImages[array_rand($bannerImages)];
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
|
||||||
|
<div class="banner-overlay"></div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="banner-inner text-white mb-50">
|
||||||
|
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">iKhokha Payments</h2>
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
|
||||||
|
<li class="breadcrumb-item"><a href="index.php">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active">iKhokha Payments</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tour List Area start -->
|
||||||
|
<section class="tour-list-page py-10 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class='infobox' data-aos='fade-up' data-aos-duration='1500' data-aos-offset='50'>
|
||||||
|
<div style='padding:10px;'>
|
||||||
|
<?php
|
||||||
|
// Fetch transactions from iKhokha API instead of DB
|
||||||
|
$startDate = isset($_GET['start']) ? $_GET['start'] : date('Y-m-d', strtotime('-30 days'));
|
||||||
|
$endDate = isset($_GET['end']) ? $_GET['end'] : date('Y-m-d');
|
||||||
|
|
||||||
|
// getIkhokhaTransactionHistory should return JSON (string) or an array
|
||||||
|
$raw = getIkhokhaTransactionHistory($startDate, $endDate);
|
||||||
|
$transactions = [];
|
||||||
|
if (is_string($raw)) {
|
||||||
|
$transactions = json_decode($raw, true);
|
||||||
|
} elseif (is_array($raw)) {
|
||||||
|
$transactions = $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($transactions)) {
|
||||||
|
echo '<input id="filterInput" type="text" class="filter-input" placeholder="Filter results...">';
|
||||||
|
echo '<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>PaylinkID</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>';
|
||||||
|
|
||||||
|
$printed = false;
|
||||||
|
foreach ($transactions as $row) {
|
||||||
|
$createdAt = isset($row['createdAt']) ? htmlspecialchars($row['createdAt']) : '';
|
||||||
|
// prefer externalTransactionID when available, fallback to paylinkID
|
||||||
|
$txId = isset($row['externalTransactionID']) ? $row['externalTransactionID'] : (isset($row['paylinkID']) ? $row['paylinkID'] : '');
|
||||||
|
$ikhokhaTxId = isset($row['paylinkID']) ? $row['paylinkID'] : '';
|
||||||
|
$description = isset($row['description']) ? $row['description'] : '';
|
||||||
|
$amount = isset($row['amount']) ? $row['amount'] : '';
|
||||||
|
$status = isset($row['status']) ? $row['status'] : '';
|
||||||
|
|
||||||
|
// Skip unpaid transactions
|
||||||
|
if (strcasecmp($status, 'UNPAID') === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "<tr>
|
||||||
|
<td>" . htmlspecialchars($createdAt) . "</td>
|
||||||
|
<td>" . htmlspecialchars($txId) . "</td>
|
||||||
|
<td>" . htmlspecialchars($ikhokhaTxId) . "</td>
|
||||||
|
<td>" . htmlspecialchars($description) . "</td>
|
||||||
|
<td>R " . htmlspecialchars($amount/100) . ".00</td>
|
||||||
|
<td>" . htmlspecialchars($status) . "</td>
|
||||||
|
</tr>";
|
||||||
|
|
||||||
|
$printed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$printed) {
|
||||||
|
echo '<tr><td colspan="6">No records found</td></tr>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<input id="filterInput" type="text" class="filter-input" placeholder="Filter results...">';
|
||||||
|
echo '<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>';
|
||||||
|
echo '<tr><td colspan="5">No records found</td></tr>';
|
||||||
|
} ?>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Tour List Area end -->
|
||||||
|
|
||||||
|
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
221
src/admin/manage_courses.php
Normal file
221
src/admin/manage_courses.php
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
$headerStyle = 'light';
|
||||||
|
$rootPath = dirname(dirname(__DIR__));
|
||||||
|
include_once($rootPath . '/header.php');
|
||||||
|
checkAdmin();
|
||||||
|
|
||||||
|
$course_id = $_GET['course_id'] ?? null;
|
||||||
|
$course = null;
|
||||||
|
|
||||||
|
// If editing an existing course, fetch its data
|
||||||
|
if ($course_id) {
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM courses WHERE course_id = ?");
|
||||||
|
$stmt->bind_param("i", $course_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
$course = $result->fetch_assoc();
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$pageTitle = $course ? 'Edit Course' : 'Create New Course';
|
||||||
|
$breadcrumbs = [['Home' => 'index'], ['Admin' => 'admin_courses'], [$pageTitle => '']];
|
||||||
|
require_once($rootPath . '/components/banner.php');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!-- Course Manager Area start -->
|
||||||
|
<section class="trip-manager-area py-100 rel z-1">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
|
||||||
|
<form id="courseForm" method="POST" action="process_course">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||||
|
<?php if ($course): ?>
|
||||||
|
<input type="hidden" name="course_id" value="<?php echo $course['course_id']; ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="section-title py-20">
|
||||||
|
<h2><?php echo $course ? 'Edit Course: ' . htmlspecialchars($course['code'] ?: $course['course_type']) : 'Create New Course'; ?></h2>
|
||||||
|
<div id="responseMessage"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-35">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="course_type">Course Type *</label>
|
||||||
|
<select id="course_type" name="course_type" class="form-control" required>
|
||||||
|
<?php
|
||||||
|
$types = ['driver_training' => 'Driver Training', 'bush_mechanics' => 'Bush Mechanics', 'rescue_recovery' => 'Rescue & Recovery', 'ladies_driver_training' => 'Ladies Driver Training'];
|
||||||
|
foreach ($types as $key => $label) {
|
||||||
|
$sel = ($course && $course['course_type'] === $key) ? 'selected' : '';
|
||||||
|
echo "<option value=\"$key\" $sel>$label</option>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="code">Course Code</label>
|
||||||
|
<input type="text" id="code" name="code" class="form-control" maxlength="12" value="<?php echo $course ? htmlspecialchars($course['code']) : ''; ?>" placeholder="Optional code e.g., CRSE001" data-manual="0">
|
||||||
|
<small class="form-text text-muted">Auto-generated from type + date (you can edit manually)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="date">Date *</label>
|
||||||
|
<input type="date" id="date" name="date" class="form-control" value="<?php echo $course ? $course['date'] : ''; ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="capacity">Capacity *</label>
|
||||||
|
<input type="number" id="capacity" name="capacity" class="form-control" min="1" value="<?php echo $course ? intval($course['capacity']) : ''; ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cost_members">Member Cost (R) *</label>
|
||||||
|
<input type="number" id="cost_members" name="cost_members" class="form-control" step="0.01" min="0" value="<?php echo $course ? $course['cost_members'] : ''; ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cost_nonmembers">Non-Member Cost (R) *</label>
|
||||||
|
<input type="number" id="cost_nonmembers" name="cost_nonmembers" class="form-control" step="0.01" min="0" value="<?php echo $course ? $course['cost_nonmembers'] : ''; ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="instructor">Instructor *</label>
|
||||||
|
<input type="text" id="instructor" name="instructor" class="form-control" value="<?php echo $course ? htmlspecialchars($course['instructor']) : ''; ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="instructor_email">Instructor Email</label>
|
||||||
|
<input type="email" id="instructor_email" name="instructor_email" class="form-control" value="<?php echo $course ? htmlspecialchars($course['instructor_email']) : ''; ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12 mt-20">
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<button type="submit" class="theme-btn style-two" style="width:100%;">
|
||||||
|
<?php echo $course ? 'Update Course' : 'Create Course'; ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Course Manager Area end -->
|
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#courseForm').on('submit', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var formData = $(this).serialize();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: 'process_course',
|
||||||
|
type: 'POST',
|
||||||
|
data: formData,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.status === 'success') {
|
||||||
|
$('#responseMessage').html('<div class="alert alert-success">' + response.message + '</div>');
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = 'admin_courses';
|
||||||
|
}, 1200);
|
||||||
|
} else {
|
||||||
|
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.error('AJAX Error:', error);
|
||||||
|
$('#responseMessage').html('<div class="alert alert-danger">Error creating/updating course: ' + error + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Auto-generate course code from type and date: ABBREVIATION_MMDD
|
||||||
|
(function(){
|
||||||
|
var typeMap = {
|
||||||
|
'driver_training': 'DRVTRN',
|
||||||
|
'bush_mechanics': 'BUSHMEC',
|
||||||
|
'rescue_recovery': 'RESREC',
|
||||||
|
'ladies_driver_training': 'LADYTRN'
|
||||||
|
};
|
||||||
|
|
||||||
|
var $type = document.getElementById('course_type');
|
||||||
|
var $date = document.getElementById('date');
|
||||||
|
var $code = document.getElementById('code');
|
||||||
|
|
||||||
|
function getMMDDFromISO(isoDate) {
|
||||||
|
if (!isoDate) return '';
|
||||||
|
// expecting YYYY-MM-DD
|
||||||
|
var parts = isoDate.split('-');
|
||||||
|
if (parts.length !== 3) return '';
|
||||||
|
return parts[1] + parts[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCode() {
|
||||||
|
try {
|
||||||
|
var manual = $code.getAttribute('data-manual') === '1';
|
||||||
|
if (manual) return; // user has manually edited
|
||||||
|
var t = $type.value;
|
||||||
|
var d = $date.value;
|
||||||
|
if (!t || !d) return;
|
||||||
|
var abbr = typeMap[t] || t.toUpperCase().replace(/[^A-Z0-9]/g,'').substring(0,7);
|
||||||
|
var mmdd = getMMDDFromISO(d);
|
||||||
|
if (!mmdd) return;
|
||||||
|
var newCode = abbr + '_' + mmdd;
|
||||||
|
$code.value = newCode;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('generateCode error', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark manual when user types
|
||||||
|
$code.addEventListener('input', function(){
|
||||||
|
var val = $code.value.trim();
|
||||||
|
if (val.length === 0) {
|
||||||
|
$code.setAttribute('data-manual','0');
|
||||||
|
} else {
|
||||||
|
// if value matches auto pattern for currently selected type+date, keep as auto; otherwise mark manual
|
||||||
|
var expected = '';
|
||||||
|
try { expected = (typeMap[$type.value] || $type.value.toUpperCase().replace(/[^A-Z0-9]/g,'').substring(0,7)) + '_' + ( ($date.value) ? $date.value.split('-')[1] + $date.value.split('-')[2] : '' ); } catch(e){ expected=''; }
|
||||||
|
if (val === expected) {
|
||||||
|
$code.setAttribute('data-manual','0');
|
||||||
|
} else {
|
||||||
|
$code.setAttribute('data-manual','1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$type.addEventListener('change', generateCode);
|
||||||
|
$date.addEventListener('change', generateCode);
|
||||||
|
|
||||||
|
// generate on load if empty
|
||||||
|
if ($code.value.trim().length === 0) {
|
||||||
|
generateCode();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php include_once($rootPath . '/components/insta_footer.php'); ?>
|
||||||
@@ -7,80 +7,102 @@ require_once($rootPath . "/src/config/functions.php");
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
* Read raw request and headers (DO NOT MODIFY RAW BODY)
|
* JS-equivalent escaping (matches iKhokha docs exactly)
|
||||||
|
* ==========================================================
|
||||||
|
*/
|
||||||
|
function jsStringEscape(string $str): string
|
||||||
|
{
|
||||||
|
$str = preg_replace('/([\\\\\"\'])/', '\\\\$1', $str);
|
||||||
|
$str = str_replace("\0", "\\0", $str);
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPayloadToSign(string $path, string $body): string
|
||||||
|
{
|
||||||
|
return jsStringEscape($path . $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ==========================================================
|
||||||
|
* Read raw request body (DO NOT MODIFY)
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
*/
|
*/
|
||||||
$raw = file_get_contents('php://input');
|
$raw = file_get_contents('php://input');
|
||||||
|
|
||||||
if ($raw === false) {
|
if ($raw === false || $raw === '') {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
progress_log('iKhokha webhook: unable to read raw input');
|
progress_log('iKhokha webhook: empty body');
|
||||||
exit('No body');
|
exit('No body');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ==========================================================
|
||||||
|
* Read headers
|
||||||
|
* ==========================================================
|
||||||
|
*/
|
||||||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||||
|
|
||||||
$ikSign = $headers['ik-sign'] ?? null;
|
$ikSign = $headers['ik-sign'] ?? null;
|
||||||
$ikAppId = $headers['ik-appid'] ?? null;
|
$ikAppId = $headers['ik-appid'] ?? null;
|
||||||
|
|
||||||
/**
|
if (!$ikSign || !$ikAppId) {
|
||||||
* ==========================================================
|
|
||||||
* Basic header presence check
|
|
||||||
* ==========================================================
|
|
||||||
*/
|
|
||||||
if (empty($ikSign) || empty($ikAppId)) {
|
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
progress_log('iKhokha webhook: missing IK-SIGN or IK-APPID');
|
progress_log('iKhokha webhook: missing headers');
|
||||||
exit('Missing headers');
|
exit('Missing headers');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
* Signature verification
|
* Signature verification (JS-equivalent)
|
||||||
* HMAC_SHA256( path + raw_body, app_secret )
|
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
*/
|
*/
|
||||||
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
||||||
|
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
||||||
|
$bypass = ($_ENV['IKHOKHA_BYPASS_SIGNATURE'] ?? 'false') === 'true';
|
||||||
|
|
||||||
if (empty($secret)) {
|
if (!$secret || !$callbackUrl) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
progress_log('iKhokha webhook: app secret not configured');
|
|
||||||
exit('Server misconfigured');
|
exit('Server misconfigured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Debug logging (disable once stable)
|
|
||||||
progress_log('--- iKhokha WEBHOOK DEBUG ---');
|
progress_log('--- iKhokha WEBHOOK DEBUG ---');
|
||||||
progress_log('RAW BODY: ' . $raw);
|
progress_log('RAW BODY: ' . $raw);
|
||||||
progress_log('IK-SIGN: ' . $ikSign);
|
progress_log('IK-SIGN: ' . $ikSign);
|
||||||
|
|
||||||
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
// Decode body so we can remove `text`
|
||||||
$bypass = ($_ENV['IKHOKHA_BYPASS_SIGNATURE'] ?? 'false') === 'true';
|
$bodyArray = json_decode($raw, true);
|
||||||
|
if (!is_array($bodyArray)) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('Invalid JSON');
|
||||||
|
}
|
||||||
|
|
||||||
|
// iKhokha JS deletes `text`
|
||||||
|
unset($bodyArray['text']);
|
||||||
|
|
||||||
|
// JS-style JSON (no escaped slashes)
|
||||||
|
$jsonBody = json_encode($bodyArray, JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
|
||||||
|
// Now sign the SAME payload JS signs
|
||||||
|
$payloadToSign = createPayloadToSign($callbackUrl, $jsonBody);
|
||||||
|
|
||||||
|
$expected = generateSignature($payloadToSign, $secret);
|
||||||
|
|
||||||
|
progress_log('JS PAYLOAD: ' . $payloadToSign);
|
||||||
|
progress_log('EXPECTED SIGN: ' . $expected);
|
||||||
|
progress_log('RECEIVED SIGN: ' . $ikSign);
|
||||||
|
|
||||||
if (!$bypass) {
|
if (!$bypass) {
|
||||||
|
|
||||||
if (empty($callbackUrl)) {
|
|
||||||
http_response_code(500);
|
|
||||||
progress_log('iKhokha webhook: callback URL not configured');
|
|
||||||
exit('Server misconfigured');
|
|
||||||
}
|
|
||||||
|
|
||||||
$expected = hash_hmac(
|
|
||||||
'sha256',
|
|
||||||
$callbackUrl . $raw,
|
|
||||||
$_ENV['IKHOKHA_APP_SECRET']
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hash_equals($expected, $ikSign)) {
|
if (!hash_equals($expected, $ikSign)) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
progress_log('iKhokha webhook: signature mismatch');
|
progress_log('iKhokha webhook: signature mismatch');
|
||||||
progress_log('EXPECTED SIGN: ' . $expected);
|
|
||||||
progress_log('RECEIVED SIGN: ' . $ikSign);
|
|
||||||
// Audit signature mismatch
|
|
||||||
if (function_exists('auditLog')) {
|
if (function_exists('auditLog')) {
|
||||||
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, ['expected' => $expected, 'received' => $ikSign]);
|
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, [
|
||||||
|
'expected' => $expected,
|
||||||
|
'received' => $ikSign
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
exit('Invalid signature');
|
exit('Invalid signature');
|
||||||
}
|
}
|
||||||
@@ -95,20 +117,13 @@ if (!$bypass) {
|
|||||||
* ==========================================================
|
* ==========================================================
|
||||||
*/
|
*/
|
||||||
$payload = json_decode($raw, true);
|
$payload = json_decode($raw, true);
|
||||||
|
$data = $payload['data'] ?? $payload;
|
||||||
if (!is_array($payload)) {
|
|
||||||
http_response_code(400);
|
|
||||||
progress_log('iKhokha webhook: invalid JSON');
|
|
||||||
exit('Invalid JSON');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
* Extract data safely (iKhokha is inconsistent)
|
* Extract fields safely
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
*/
|
*/
|
||||||
$data = $payload['data'] ?? $payload;
|
|
||||||
|
|
||||||
$externalTransactionID =
|
$externalTransactionID =
|
||||||
$data['externalTransactionID']
|
$data['externalTransactionID']
|
||||||
?? $data['externalTransactionId']
|
?? $data['externalTransactionId']
|
||||||
@@ -127,11 +142,11 @@ $providerStatus =
|
|||||||
|
|
||||||
progress_log('Parsed externalTransactionID: ' . $externalTransactionID);
|
progress_log('Parsed externalTransactionID: ' . $externalTransactionID);
|
||||||
progress_log('Parsed providerPaymentId: ' . $providerPaymentId);
|
progress_log('Parsed providerPaymentId: ' . $providerPaymentId);
|
||||||
progress_log('Parsed providerStatus: ' . print_r($providerStatus, true));
|
progress_log('Parsed providerStatus: ' . $providerStatus);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
* Locate local payment
|
* Locate payment
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
*/
|
*/
|
||||||
$localPaymentId = null;
|
$localPaymentId = null;
|
||||||
@@ -146,16 +161,13 @@ if ($externalTransactionID) {
|
|||||||
WHERE payment_id = ?
|
WHERE payment_id = ?
|
||||||
LIMIT 1"
|
LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($stmt) {
|
if ($stmt) {
|
||||||
$stmt->bind_param('s', $externalTransactionID);
|
$stmt->bind_param('s', $externalTransactionID);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$res = $stmt->get_result();
|
$res = $stmt->get_result();
|
||||||
if ($row = $res->fetch_assoc()) {
|
if ($row = $res->fetch_assoc()) {
|
||||||
|
extract($row);
|
||||||
$localPaymentId = $row['payment_id'];
|
$localPaymentId = $row['payment_id'];
|
||||||
$booking_id = $row['booking_id'];
|
|
||||||
$user_id = $row['user_id'];
|
|
||||||
$description = $row['description'];
|
|
||||||
}
|
}
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
}
|
}
|
||||||
@@ -168,16 +180,13 @@ if (!$localPaymentId && $providerPaymentId) {
|
|||||||
WHERE provider_payment_id = ?
|
WHERE provider_payment_id = ?
|
||||||
LIMIT 1"
|
LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($stmt) {
|
if ($stmt) {
|
||||||
$stmt->bind_param('s', $providerPaymentId);
|
$stmt->bind_param('s', $providerPaymentId);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$res = $stmt->get_result();
|
$res = $stmt->get_result();
|
||||||
if ($row = $res->fetch_assoc()) {
|
if ($row = $res->fetch_assoc()) {
|
||||||
|
extract($row);
|
||||||
$localPaymentId = $row['payment_id'];
|
$localPaymentId = $row['payment_id'];
|
||||||
$booking_id = $row['booking_id'];
|
|
||||||
$user_id = $row['user_id'];
|
|
||||||
$description = $row['description'];
|
|
||||||
}
|
}
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
}
|
}
|
||||||
@@ -185,11 +194,6 @@ if (!$localPaymentId && $providerPaymentId) {
|
|||||||
|
|
||||||
if (!$localPaymentId) {
|
if (!$localPaymentId) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
progress_log('iKhokha webhook: payment not found');
|
|
||||||
progress_log(json_encode([$externalTransactionID, $providerPaymentId]));
|
|
||||||
if (function_exists('auditLog')) {
|
|
||||||
auditLog(null, 'IKHOKHA_PAYMENT_NOT_FOUND', 'payment', null, ['externalTransactionID' => $externalTransactionID, 'providerPaymentId' => $providerPaymentId]);
|
|
||||||
}
|
|
||||||
exit('Payment not found');
|
exit('Payment not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,72 +220,58 @@ if ($update) {
|
|||||||
);
|
);
|
||||||
$update->execute();
|
$update->execute();
|
||||||
$update->close();
|
$update->close();
|
||||||
if (function_exists('auditLog')) {
|
|
||||||
auditLog($user_id, 'PAYMENT_PROVIDER_RESPONSE_SAVED', 'payment', null, ['payment_id' => $localPaymentId, 'provider_payment_id' => $providerPaymentId, 'provider_status' => $providerStatus]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
* Normalize status and apply business logic
|
* Business logic
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
*/
|
*/
|
||||||
$normalized = strtoupper(trim((string)$providerStatus));
|
$normalized = strtoupper(trim((string)$providerStatus));
|
||||||
|
|
||||||
if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
||||||
|
|
||||||
// Mark payment as PAID
|
$conn->prepare(
|
||||||
$setPaid = $conn->prepare(
|
|
||||||
"UPDATE payments SET status = 'PAID' WHERE payment_id = ?"
|
"UPDATE payments SET status = 'PAID' WHERE payment_id = ?"
|
||||||
);
|
)->bind_param('s', $localPaymentId)->execute();
|
||||||
if ($setPaid) {
|
|
||||||
$setPaid->bind_param('s', $localPaymentId);
|
|
||||||
$setPaid->execute();
|
|
||||||
$setPaid->close();
|
|
||||||
if (function_exists('auditLog')) {
|
|
||||||
auditLog($user_id, 'PAYMENT_MARKED_PAID', 'payment', null, ['payment_id' => $localPaymentId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Booking or membership update
|
if ($booking_id) {
|
||||||
if (!empty($booking_id)) {
|
$conn->prepare(
|
||||||
$upd = $conn->prepare(
|
|
||||||
"UPDATE bookings SET status = 'PAID' WHERE booking_id = ?"
|
"UPDATE bookings SET status = 'PAID' WHERE booking_id = ?"
|
||||||
);
|
)->bind_param('i', $booking_id)->execute();
|
||||||
if ($upd) {
|
|
||||||
$upd->bind_param('i', $booking_id);
|
|
||||||
$upd->execute();
|
|
||||||
$upd->close();
|
|
||||||
sendAdminNotification('4WDCSA.co.za - New Booking - '.getFullName($user_id) , 'We have received a payment for a new booking for '.$description.' from '.getFullName($user_id));
|
|
||||||
if (function_exists('auditLog')) {
|
|
||||||
auditLog($user_id, 'BOOKING_PAYMENT_MARKED_PAID', 'bookings', $booking_id, ['payment_id' => $localPaymentId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$upd = $conn->prepare(
|
$conn->prepare(
|
||||||
"UPDATE membership_fees
|
"UPDATE membership_fees SET payment_status = 'PAID' WHERE payment_id = ?"
|
||||||
SET payment_status = 'PAID'
|
)->bind_param('s', $localPaymentId)->execute();
|
||||||
WHERE payment_id = ?"
|
|
||||||
);
|
|
||||||
if ($upd) {
|
|
||||||
$upd->bind_param('s', $localPaymentId);
|
|
||||||
$upd->execute();
|
|
||||||
$upd->close();
|
|
||||||
sendAdminNotification('4WDCSA.co.za - Membership Payment Received - '.getFullName($user_id) , 'A Membership Payment has been received from '.getFullName($user_id));
|
|
||||||
if (function_exists('auditLog')) {
|
|
||||||
auditLog($user_id, 'MEMBERSHIP_PAYMENT_MARKED_PAID', 'membership_fees', null, ['payment_id' => $localPaymentId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send confirmation email
|
sendPaymentConfirmation(
|
||||||
if (!empty($user_id)) {
|
getEmail($user_id),
|
||||||
sendPaymentConfirmation(
|
getFullName($user_id),
|
||||||
getEmail($user_id),
|
$description
|
||||||
getFullName($user_id),
|
);
|
||||||
$description
|
|
||||||
);
|
//generate $message for admin payment confirmation with payment details
|
||||||
}
|
$message = "Payment Confirmation\n\n";
|
||||||
|
$message .= "Payment ID: " . $localPaymentId . "\n";
|
||||||
|
$message .= "Amount: " . getPaymentAmount($localPaymentId) . "\n";
|
||||||
|
$message .= "Status: PAID\n";
|
||||||
|
$message .= "Description: " . $description . "\n";
|
||||||
|
$message .= "Thank you.\n";
|
||||||
|
$subject = "4WDCSA.co.za Payment Confirmation for Payment ID: " . $localPaymentId;
|
||||||
|
progress_log('Payment confirmation sent for payment ID: ' . $localPaymentId);
|
||||||
|
|
||||||
|
sendEmail(
|
||||||
|
$_ENV['FINANCE_EMAIL'],
|
||||||
|
$subject,
|
||||||
|
nl2br($message)
|
||||||
|
);
|
||||||
|
sendEmail(
|
||||||
|
'chrispintoza@gmail.com',
|
||||||
|
$subject,
|
||||||
|
nl2br($message)
|
||||||
|
);
|
||||||
|
sendAdminNotification($subject, nl2br($message));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -858,6 +858,45 @@ function createIkhokhaPayment($payment_id, $amount, $description, $publicRef)
|
|||||||
return $resp;
|
return $resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIkhokhaTransactionHistory($startDate, $endDate,)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Base requester URL: prefer explicit env var, otherwise build from request
|
||||||
|
$endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=".$startDate."&endDate=".$endDate;
|
||||||
|
// $endpoint = "https://api.ikhokha.com/public-api/v1/api/payments/history?startDate=2024-02-01&endDate=2026-03-07";
|
||||||
|
$appID = $_ENV['IKHOKHA_APP_ID'];
|
||||||
|
progress_log($appID, "IKHOKHA App ID");
|
||||||
|
$appSecret = $_ENV['IKHOKHA_APP_SECRET'];
|
||||||
|
|
||||||
|
// $stringifiedBody = json_encode($requestBody);
|
||||||
|
$payloadToSign = createPayloadToSign($endpoint, null);
|
||||||
|
progress_log($payloadToSign, "IKHOKHA Payload to Sign");
|
||||||
|
|
||||||
|
$ikSign = generateSignature($payloadToSign, $appSecret);
|
||||||
|
progress_log($ikSign, "IKHOKHA Signature");
|
||||||
|
|
||||||
|
// Initialize cURL session
|
||||||
|
$ch = curl_init($endpoint);
|
||||||
|
// Set cURL options
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
|
||||||
|
// curl_setopt($ch, CURLOPT_POSTFIELDS, $stringifiedBody);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"IK-APPID: $appID",
|
||||||
|
"IK-SIGN: $ikSign"
|
||||||
|
]);
|
||||||
|
// Execute cURL session
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
// Decode and output the response
|
||||||
|
$resp = json_decode($response, true);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function escapeString($str) {
|
function escapeString($str) {
|
||||||
$escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
$escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
||||||
$cleaned = str_replace('\/', '/', $escaped);
|
$cleaned = str_replace('\/', '/', $escaped);
|
||||||
@@ -879,6 +918,20 @@ function generateSignature($payloadToSign, $secret) {
|
|||||||
return hash_hmac('sha256', $payloadToSign, $secret);
|
return hash_hmac('sha256', $payloadToSign, $secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPaymentAmount($localPaymentId) {
|
||||||
|
$conn = openDatabaseConnection();
|
||||||
|
$stmt = $conn->prepare("SELECT amount FROM payments WHERE payment_id = ? LIMIT 1");
|
||||||
|
$stmt->bind_param("s", $localPaymentId);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($row = $result->fetch_assoc()) {
|
||||||
|
return $row['amount'];
|
||||||
|
} else {
|
||||||
|
return false; // Payment not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function processMembershipPayment($payment_id, $amount, $description)
|
function processMembershipPayment($payment_id, $amount, $description)
|
||||||
{
|
{
|
||||||
$conn = openDatabaseConnection();
|
$conn = openDatabaseConnection();
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Database Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directory
|
Database Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directory
|
||||||
49
src/processors/delete_course.php
Normal file
49
src/processors/delete_course.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
ob_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$rootPath = dirname(dirname(__DIR__));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . '/src/config/functions.php');
|
||||||
|
require_once($rootPath . '/src/config/connection.php');
|
||||||
|
|
||||||
|
// Check admin status
|
||||||
|
session_start();
|
||||||
|
if (empty($_SESSION['user_id'])) {
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_role = getUserRole();
|
||||||
|
if (!in_array($user_role, ['admin', 'superadmin'])) {
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$course_id = intval($_POST['course_id'] ?? 0);
|
||||||
|
|
||||||
|
if ($course_id <= 0) {
|
||||||
|
throw new Exception('Invalid course ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("DELETE FROM courses WHERE course_id = ?");
|
||||||
|
$stmt->bind_param("i", $course_id);
|
||||||
|
|
||||||
|
if (!$stmt->execute()) {
|
||||||
|
throw new Exception('Failed to delete course: ' . $stmt->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'success', 'message' => 'Course deleted successfully']);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
100
src/processors/process_course.php
Normal file
100
src/processors/process_course.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
ob_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$rootPath = dirname(dirname(__DIR__));
|
||||||
|
require_once($rootPath . "/src/config/env.php");
|
||||||
|
require_once($rootPath . '/src/config/functions.php');
|
||||||
|
require_once($rootPath . '/src/config/connection.php');
|
||||||
|
|
||||||
|
// Check admin status
|
||||||
|
session_start();
|
||||||
|
if (empty($_SESSION['user_id'])) {
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_role = getUserRole();
|
||||||
|
if (!in_array($user_role, ['admin', 'superadmin'])) {
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$course_id = $_POST['course_id'] ?? null;
|
||||||
|
$course_type = trim($_POST['course_type'] ?? '');
|
||||||
|
$code = trim($_POST['code'] ?? '');
|
||||||
|
$date = trim($_POST['date'] ?? '');
|
||||||
|
$capacity = intval($_POST['capacity'] ?? 0);
|
||||||
|
$cost_members = floatval($_POST['cost_members'] ?? 0);
|
||||||
|
$cost_nonmembers = floatval($_POST['cost_nonmembers'] ?? 0);
|
||||||
|
$instructor = trim($_POST['instructor'] ?? '');
|
||||||
|
$instructor_email = trim($_POST['instructor_email'] ?? '');
|
||||||
|
|
||||||
|
$allowed_types = ['driver_training','bush_mechanics','rescue_recovery','ladies_driver_training'];
|
||||||
|
|
||||||
|
if (!in_array($course_type, $allowed_types)) {
|
||||||
|
throw new Exception('Invalid course type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||||
|
throw new Exception('Invalid date format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If code not provided, generate from type + date using ABBR_MMDD format
|
||||||
|
if (empty($code)) {
|
||||||
|
$abbrMap = [
|
||||||
|
'driver_training' => 'DRVTRN',
|
||||||
|
'bush_mechanics' => 'BUSHMEC',
|
||||||
|
'rescue_recovery' => 'RESREC',
|
||||||
|
'ladies_driver_training' => 'LADYTRN'
|
||||||
|
];
|
||||||
|
|
||||||
|
$abbr = $abbrMap[$course_type] ?? strtoupper(preg_replace('/[^A-Z0-9]/', '', $course_type));
|
||||||
|
// ensure abbr fits (reserve 1 char for underscore and 4 for MMDD)
|
||||||
|
$abbr = substr($abbr, 0, 7);
|
||||||
|
$mmdd = date('md', strtotime($date));
|
||||||
|
$code = strtoupper(substr($abbr . '_' . $mmdd, 0, 12));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($capacity <= 0) {
|
||||||
|
throw new Exception('Capacity must be greater than 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($instructor)) {
|
||||||
|
throw new Exception('Instructor name is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($course_id) {
|
||||||
|
// Update
|
||||||
|
$stmt = $conn->prepare("UPDATE courses SET course_type = ?, code = ?, date = ?, capacity = ?, cost_members = ?, cost_nonmembers = ?, instructor = ?, instructor_email = ? WHERE course_id = ?");
|
||||||
|
$stmt->bind_param("sssiddssi", $course_type, $code, $date, $capacity, $cost_members, $cost_nonmembers, $instructor, $instructor_email, $course_id);
|
||||||
|
|
||||||
|
if (!$stmt->execute()) {
|
||||||
|
throw new Exception('Failed to update course: ' . $stmt->error);
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
} else {
|
||||||
|
// Insert - booked defaults to 0
|
||||||
|
$stmt = $conn->prepare("INSERT INTO courses (course_type, code, date, capacity, booked, cost_members, cost_nonmembers, instructor, instructor_email) VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param("sssiddss", $course_type, $code, $date, $capacity, $cost_members, $cost_nonmembers, $instructor, $instructor_email);
|
||||||
|
|
||||||
|
if (!$stmt->execute()) {
|
||||||
|
throw new Exception('Failed to create course: ' . $stmt->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$course_id = $conn->insert_id;
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'success', 'message' => $course_id ? 'Course saved successfully' : 'Course created successfully', 'course_id' => $course_id]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
ob_end_clean();
|
||||||
|
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
41
test.php
41
test.php
@@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
require_once("./src/config/env.php");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EXACT escape function from iKhokha docs
|
|
||||||
*/
|
|
||||||
function escapeString($str) {
|
|
||||||
$escaped = preg_replace(
|
|
||||||
['/[\\"\'\"]/u', '/\x00/'],
|
|
||||||
['\\\\$0', '\\0'],
|
|
||||||
(string)$str
|
|
||||||
);
|
|
||||||
$cleaned = str_replace('\/', '/', $escaped);
|
|
||||||
return $cleaned;
|
|
||||||
}
|
|
||||||
|
|
||||||
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
|
||||||
$path = '/src/api/ikhokha_webhook.php';
|
|
||||||
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
|
||||||
|
|
||||||
// Simulated raw webhook body (EXACT, no whitespace changes)
|
|
||||||
$raw = '{"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null}';
|
|
||||||
|
|
||||||
echo "<strong>IK-SIGN FROM WEBHOOK:</strong><br>";
|
|
||||||
echo "bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557<br><br>";
|
|
||||||
|
|
||||||
$payloadToSign = $path . $raw;
|
|
||||||
|
|
||||||
// Generate signature using hash_hmac directly on the constructed string
|
|
||||||
$expected = hash_hmac('sha256', $payloadToSign, $secret);
|
|
||||||
|
|
||||||
// --- Output debug info (UPDATED) ---
|
|
||||||
echo "<strong>DEBUG INFO</strong><br>";
|
|
||||||
echo "Callback URL: $callbackUrl<br><br>";
|
|
||||||
|
|
||||||
echo "<strong>Payload to Sign (Un-escaped):</strong><br>";
|
|
||||||
echo htmlspecialchars($payloadToSign) . "<br><br>";
|
|
||||||
|
|
||||||
echo "<strong>EXPECTED SIGNATURE:</strong><br>";
|
|
||||||
echo $expected . "<br>";
|
|
||||||
Reference in New Issue
Block a user