Compare commits
6 Commits
a66382661d
...
5768d8a7af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5768d8a7af | ||
|
|
0e6ecd127f | ||
|
|
702e04e9bf | ||
|
|
d2c99e86b4 | ||
|
|
f4934e9c13 | ||
|
|
477c2f2e04 |
@@ -80,6 +80,11 @@ RewriteRule ^indemnity_waiver$ src/pages/other/indemnity_waiver.php [L]
|
||||
RewriteRule ^basic_indemnity$ src/pages/other/basic_indemnity.php [L]
|
||||
RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L]
|
||||
|
||||
# === PAYMENT RETURN PAGES ===
|
||||
RewriteRule ^success$ src/pages/payment/success.php [L]
|
||||
RewriteRule ^failure$ src/pages/payment/failure.php [L]
|
||||
RewriteRule ^cancel$ src/pages/payment/cancel.php [L]
|
||||
|
||||
# === ADMIN PAGES ===
|
||||
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
|
||||
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
||||
|
||||
215
.htaccess copy
Normal file
215
.htaccess copy
Normal file
@@ -0,0 +1,215 @@
|
||||
# URL Rewrite Rules - Maps old URLs to new directory structure during migration
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Don't rewrite existing files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# === STRIP .PHP EXTENSION ===
|
||||
# Redirect /page.php to /page (301 permanent redirect)
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
|
||||
# Internally rewrite /page to /page.php if page.php exists
|
||||
RewriteCond %{REQUEST_FILENAME}\.php -f
|
||||
RewriteRule ^(.+)$ $1.php [L]
|
||||
|
||||
# === AUTH PAGES ===
|
||||
RewriteRule ^login$ src/pages/auth/login.php [L]
|
||||
RewriteRule ^register$ src/pages/auth/register.php [L]
|
||||
RewriteRule ^forgot_password$ src/pages/auth/forgot_password.php [L]
|
||||
RewriteRule ^reset_password$ src/pages/auth/reset_password.php [L]
|
||||
RewriteRule ^verify$ src/pages/auth/verify.php [L]
|
||||
RewriteRule ^resend_verification$ src/pages/auth/resend_verification.php [L]
|
||||
RewriteRule ^change_password$ src/pages/auth/change_password.php [L]
|
||||
RewriteRule ^update_password$ src/pages/auth/update_password.php [L]
|
||||
|
||||
# === MEMBERSHIP PAGES ===
|
||||
RewriteRule ^membership$ src/pages/memberships/membership.php [L]
|
||||
RewriteRule ^membership_details$ src/pages/memberships/membership_details.php [L]
|
||||
RewriteRule ^membership_application$ src/pages/memberships/membership_application.php [L]
|
||||
RewriteRule ^membership_payment$ src/pages/memberships/membership_payment.php [L]
|
||||
RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L]
|
||||
RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
|
||||
|
||||
# === BOOKING PAGES ===
|
||||
RewriteRule ^bookings$ src/pages/bookings/bookings.php [L]
|
||||
RewriteRule ^campsites$ src/pages/bookings/campsites.php [L]
|
||||
RewriteRule ^campsite_booking$ src/pages/bookings/campsite_booking.php [L]
|
||||
RewriteRule ^add_campsite$ src/pages/add_campsite.php [L]
|
||||
RewriteRule ^trips$ src/pages/bookings/trips.php [L]
|
||||
RewriteRule ^trip-details$ src/pages/bookings/trip-details.php [L]
|
||||
RewriteRule ^course_details$ src/pages/bookings/course_details.php [L]
|
||||
RewriteRule ^driver_training$ src/pages/bookings/driver_training.php [L]
|
||||
|
||||
# === SHOP PAGES ===
|
||||
RewriteRule ^view_cart$ src/pages/shop/view_cart.php [L]
|
||||
RewriteRule ^add_to_cart$ src/pages/shop/add_to_cart.php [L]
|
||||
RewriteRule ^bar_tabs$ src/pages/shop/bar_tabs.php [L]
|
||||
RewriteRule ^payment_confirmation$ src/pages/shop/payment_confirmation.php [L]
|
||||
RewriteRule ^confirm$ src/pages/shop/confirm.php [L]
|
||||
RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L]
|
||||
|
||||
# === GALLERY PAGES ===
|
||||
RewriteRule ^gallery$ src/pages/gallery/gallery.php [L]
|
||||
RewriteRule ^create_album$ src/pages/gallery/create_album.php [L]
|
||||
RewriteRule ^edit_album$ src/pages/gallery/create_album.php [L]
|
||||
RewriteRule ^view_album$ src/pages/gallery/view_album.php [L]
|
||||
|
||||
# === EVENTS & BLOG PAGES ===
|
||||
RewriteRule ^events$ src/pages/events/events.php [L]
|
||||
RewriteRule ^blog$ src/pages/blog/blog.php [L]
|
||||
RewriteRule ^blog_details$ src/pages/blog/blog_details.php [L]
|
||||
RewriteRule ^best_of_the_eastern_cape_2024$ src/pages/events/best_of_the_eastern_cape_2024.php [L]
|
||||
RewriteRule ^2025_agm_minutes$ src/pages/events/2025_agm_minutes.php [L]
|
||||
RewriteRule ^agm_content$ src/pages/events/agm_content.php [L]
|
||||
RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
||||
|
||||
# === OTHER PAGES ===
|
||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
||||
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
||||
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
||||
RewriteRule ^track-map$ src/pages/track-map.php [L]
|
||||
RewriteRule ^404$ src/pages/other/404.php [L]
|
||||
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
|
||||
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
|
||||
RewriteRule ^bush_mechanics$ src/pages/other/bush_mechanics.php [L]
|
||||
RewriteRule ^indemnity$ src/pages/other/indemnity.php [L]
|
||||
RewriteRule ^indemnity_waiver$ src/pages/other/indemnity_waiver.php [L]
|
||||
RewriteRule ^basic_indemnity$ src/pages/other/basic_indemnity.php [L]
|
||||
RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L]
|
||||
|
||||
# === ADMIN PAGES ===
|
||||
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
|
||||
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
||||
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
|
||||
RewriteRule ^admin_events$ src/admin/admin_events.php [L]
|
||||
RewriteRule ^admin_course_bookings$ src/admin/admin_course_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_visitors$ src/admin/admin_visitors.php [L]
|
||||
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
|
||||
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
|
||||
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
|
||||
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
|
||||
|
||||
# === API/AJAX ENDPOINTS ===
|
||||
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
|
||||
RewriteRule ^fetch_drinks$ src/api/fetch_drinks.php [L]
|
||||
RewriteRule ^fetch_bar_tabs$ src/api/fetch_bar_tabs.php [L]
|
||||
RewriteRule ^get_campsites$ src/api/get_campsites.php [L]
|
||||
RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
|
||||
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
|
||||
|
||||
# === PROCESSORS ===
|
||||
RewriteRule ^validate_login$ src/processors/validate_login.php [L]
|
||||
RewriteRule ^register_user$ src/processors/register_user.php [L]
|
||||
RewriteRule ^process_application$ src/processors/process_application.php [L]
|
||||
RewriteRule ^process_booking$ src/processors/process_booking.php [L]
|
||||
RewriteRule ^process_camp_booking$ src/processors/process_camp_booking.php [L]
|
||||
RewriteRule ^process_course_booking$ src/processors/process_course_booking.php [L]
|
||||
RewriteRule ^process_trip_booking$ src/processors/process_trip_booking.php [L]
|
||||
RewriteRule ^process_membership_payment$ src/processors/process_membership_payment.php [L]
|
||||
RewriteRule ^process_payments$ src/processors/process_payments.php [L]
|
||||
RewriteRule ^process_eft$ src/processors/process_eft.php [L]
|
||||
RewriteRule ^submit_order$ src/processors/submit_order.php [L]
|
||||
RewriteRule ^submit_pop$ src/processors/submit_pop.php [L]
|
||||
RewriteRule ^process_signature$ src/processors/process_signature.php [L]
|
||||
RewriteRule ^create_bar_tab$ src/processors/create_bar_tab.php [L]
|
||||
RewriteRule ^update_application$ src/processors/update_application.php [L]
|
||||
RewriteRule ^update_user$ src/processors/update_user.php [L]
|
||||
RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [L]
|
||||
RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L]
|
||||
RewriteRule ^logout$ src/processors/logout.php [L]
|
||||
RewriteRule ^process_trip$ src/processors/process_trip.php [L]
|
||||
RewriteRule ^process_event$ src/processors/process_event.php [L]
|
||||
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
|
||||
RewriteRule ^toggle_event_published$ src/processors/toggle_event_published.php [L]
|
||||
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
|
||||
RewriteRule ^delete_event$ src/processors/delete_event.php [L]
|
||||
RewriteRule ^save_album$ src/processors/save_album.php [L]
|
||||
RewriteRule ^update_album$ src/processors/update_album.php [L]
|
||||
RewriteRule ^delete_album$ src/processors/delete_album.php [L]
|
||||
RewriteRule ^delete_photo$ src/processors/delete_photo.php [L]
|
||||
RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
|
||||
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
|
||||
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
||||
|
||||
# Blog routes
|
||||
RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L]
|
||||
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
|
||||
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
|
||||
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
|
||||
RewriteRule ^blog_create$ src/processors/blog/blog_create.php [L]
|
||||
RewriteRule ^blog_delete$ src/processors/blog/blog_delete.php [L]
|
||||
RewriteRule ^publish_blog$ src/processors/blog/publish_blog.php [L]
|
||||
RewriteRule ^blog_unpublish$ src/processors/blog/blog_unpublish.php [L]
|
||||
RewriteRule ^submit_blog$ src/processors/blog/submit_blog.php [L]
|
||||
RewriteRule ^upload_blog_image$ src/processors/blog/upload_blog_image.php [L]
|
||||
RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
||||
|
||||
</IfModule>
|
||||
|
||||
php_flag display_errors On
|
||||
# php_value error_reporting -1
|
||||
RedirectMatch 403 ^/\.well-known
|
||||
Options -Indexes
|
||||
|
||||
<FilesMatch "\.(env|sql|bak|zip|tar|gz|ini)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
ErrorDocument 404 /404.php
|
||||
|
||||
<RequireAll>
|
||||
Require all granted
|
||||
Require not ip 4.222.252.98
|
||||
Require not ip 4.222.252.97
|
||||
</RequireAll>
|
||||
|
||||
<Files .env>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
|
||||
# ALL CUSTOM ENTRIES SHOULD GO ABOVE THIS LINE
|
||||
# BEGIN IWORX header
|
||||
# This file was created by InterWorx-CP
|
||||
# You may modify this file, but any changes made between
|
||||
# BEGIN IWORX and END IWORX tags may be lost on future
|
||||
# updates. Additionally, changes NOT made between these
|
||||
# tags will not be recognized in the SiteWorx interface.
|
||||
# END IWORX header
|
||||
|
||||
# BEGIN IWORX accesscontrol
|
||||
# END IWORX accesscontrol
|
||||
|
||||
# BEGIN IWORX errordocs
|
||||
# END IWORX errordocs
|
||||
|
||||
# BEGIN IWORX mimetypes
|
||||
# END IWORX mimetypes
|
||||
|
||||
# BEGIN IWORX handlers
|
||||
# END IWORX handlers
|
||||
|
||||
# BEGIN IWORX charset
|
||||
# END IWORX charset
|
||||
|
||||
# BEGIN IWORX redirects
|
||||
# END IWORX redirects
|
||||
|
||||
# BEGIN IWORX phpvars
|
||||
# END IWORX phpvars
|
||||
|
||||
# BEGIN IWORX dirindex
|
||||
# END IWORX dirindex
|
||||
|
||||
# BEGIN IWORX hotlink
|
||||
# END IWORX hotlink
|
||||
|
||||
# BEGIN IWORX passwordprotection
|
||||
# END IWORX passwordprotection
|
||||
|
||||
BIN
assets/images/logos/ikhokha.png
Normal file
BIN
assets/images/logos/ikhokha.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
120
classes/iKhokhaClient.php
Normal file
120
classes/iKhokhaClient.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
class IkhokhaClient {
|
||||
private string $appId;
|
||||
private string $appSecret;
|
||||
private string $apiUrl;
|
||||
|
||||
public function __construct() {
|
||||
// Try getenv first, then fallback to $_ENV if available
|
||||
$this->appId = getenv('IKHOKHA_APP_ID') ?: ($_ENV['IKHOKHA_APP_ID'] ?? '');
|
||||
$this->appSecret = getenv('IKHOKHA_APP_SECRET') ?: ($_ENV['IKHOKHA_APP_SECRET'] ?? '');
|
||||
$this->apiUrl = getenv('IKHOKHA_API_URL') ?: ($_ENV['IKHOKHA_API_URL'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the iKhokha API. Signs the payload per API docs.
|
||||
* $endpoint should be the path portion starting with '/public-api/...'
|
||||
*/
|
||||
private function request(string $endpoint, array $data, string $method = 'POST') {
|
||||
// Validate apiUrl
|
||||
if (empty($this->apiUrl)) {
|
||||
return ['error' => true, 'errno' => 3, 'message' => 'IKHOKHA_API_URL is not configured in environment'];
|
||||
}
|
||||
|
||||
// If the configured API URL already contains the endpoint path, use it as-is.
|
||||
if ((function_exists('str_ends_with') && str_ends_with($this->apiUrl, $endpoint)) ||
|
||||
(substr_compare($this->apiUrl, $endpoint, -strlen($endpoint)) === 0)) {
|
||||
$url = $this->apiUrl;
|
||||
} else {
|
||||
$url = rtrim($this->apiUrl, '/') . $endpoint;
|
||||
}
|
||||
$body = json_encode($data);
|
||||
|
||||
// Build payload to sign: path + body and apply escape rules per iKhokha docs
|
||||
$parsed = parse_url($url);
|
||||
$path = $parsed['path'] ?? $endpoint;
|
||||
$payloadToSign = $path . $body;
|
||||
|
||||
// Escape function from iKhokha example
|
||||
$escapeString = function ($str) {
|
||||
$escaped = preg_replace(['/[\\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
||||
$cleaned = str_replace('\/', '/', $escaped);
|
||||
return $cleaned;
|
||||
};
|
||||
|
||||
$escapedPayload = $escapeString($payloadToSign);
|
||||
$signature = hash_hmac('sha256', $escapedPayload, $this->appSecret);
|
||||
|
||||
$ch = curl_init($url);
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
"IK-APPID: {$this->appId}",
|
||||
"IK-SIGN: {$signature}"
|
||||
];
|
||||
|
||||
// Optional debug logging to logs/ikhokha.log when IKHOKHA_DEBUG_LOG is true
|
||||
$debugLog = getenv('IKHOKHA_DEBUG_LOG') ?: ($_ENV['IKHOKHA_DEBUG_LOG'] ?? null);
|
||||
if ($debugLog) {
|
||||
$logPath = dirname(__DIR__) . '/logs/ikhokha.log';
|
||||
$logEntry = [
|
||||
'time' => date('c'),
|
||||
'url' => $url,
|
||||
'headers' => $headers,
|
||||
'body' => $data,
|
||||
'signature' => $signature
|
||||
];
|
||||
@file_put_contents($logPath, json_encode(['request' => $logEntry]) . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
if (strtoupper($method) === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$errno = curl_errno($ch);
|
||||
$error = curl_error($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// Log response if debug enabled
|
||||
if (!empty($debugLog)) {
|
||||
$logPath = dirname(__DIR__) . '/logs/ikhokha.log';
|
||||
$respEntry = [
|
||||
'time' => date('c'),
|
||||
'http_code' => $httpCode,
|
||||
'errno' => $errno,
|
||||
'error' => $error,
|
||||
'response' => $response
|
||||
];
|
||||
@file_put_contents($logPath, json_encode(['response' => $respEntry]) . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
if ($response === false) {
|
||||
return ['error' => true, 'message' => $error, 'errno' => $errno];
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payment link using the iKhokha create payment endpoint.
|
||||
* $body must match iKhokha request schema (amount in smallest unit, urls, externalTransactionID, etc.)
|
||||
*/
|
||||
public function createPaymentLink(array $body) {
|
||||
return $this->request('/public-api/v1/api/payment', $body, 'POST');
|
||||
}
|
||||
|
||||
public function getPaymentStatus($paymentId) {
|
||||
// Use the GET status endpoint
|
||||
$endpoint = '/public-api/v1/api/getStatus/' . urlencode($paymentId);
|
||||
return $this->request($endpoint, [], 'GET');
|
||||
}
|
||||
}
|
||||
@@ -327,7 +327,7 @@ if ($headerStyle === 'light') {
|
||||
echo "<li><a href=\"membership\">My Blog Posts</a><i class='fal fa-lock'></i></li>";
|
||||
}
|
||||
?>
|
||||
<li><a href="submit_pop">Submit P.O.P</a></li>
|
||||
<!-- <li><a href="submit_pop">Submit P.O.P</a></li> -->
|
||||
<li><a href="logout">Log Out</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
71
index.php
71
index.php
@@ -19,6 +19,27 @@ if (!isset($_SESSION['updates_modal_shown'])) {
|
||||
$showUpdatesModal = false;
|
||||
}
|
||||
|
||||
// Show renew membership modal for logged-in users and where membership_fees payment_status is not PENDING RENEWAL. only show once per session
|
||||
$showRenewModal = isset($_SESSION['user_id']) ? true : false;
|
||||
if ($showRenewModal) {
|
||||
if (!isset($_SESSION['renew_modal_shown'])) {
|
||||
$_SESSION['renew_modal_shown'] = true;
|
||||
} else {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($payment_status);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
if ($payment_status === 'PENDING RENEWAL') {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
|
||||
$userId = $_SESSION['user_id'];
|
||||
$stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1");
|
||||
@@ -657,34 +678,72 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
const modal = document.getElementById('updatesModal');
|
||||
const closeBtn = document.querySelector('.updates-modal-close');
|
||||
const showModal = <?php echo $showUpdatesModal ? 'true' : 'false'; ?>;
|
||||
const showRenewModal = <?php echo $showRenewModal ? 'true' : 'false'; ?>;
|
||||
|
||||
if (showModal) {
|
||||
// Show modal after a short delay for better UX
|
||||
if (showModal && modal) {
|
||||
// Show updates modal after a short delay for better UX
|
||||
setTimeout(function() {
|
||||
modal.style.display = 'flex';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Close modal when X is clicked
|
||||
// Close updates modal when X is clicked
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', function() {
|
||||
modal.style.display = 'none';
|
||||
if (modal) modal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal when clicking outside the modal content
|
||||
// Close updates modal when clicking outside the modal content
|
||||
if (modal) {
|
||||
modal.addEventListener('click', function(event) {
|
||||
if (event.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show renew membership Bootstrap modal for logged-in users
|
||||
try {
|
||||
const renewModalEl = document.getElementById('renewModal');
|
||||
if (showRenewModal && renewModalEl && typeof bootstrap !== 'undefined') {
|
||||
setTimeout(function() {
|
||||
const renewModal = new bootstrap.Modal(renewModalEl);
|
||||
renewModal.show();
|
||||
}, 700);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Renew modal show failed', e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Updates Modal -->
|
||||
<!-- Renew Membership Modal (shown to logged-in users) -->
|
||||
<div class="modal fade" id="renewModal" tabindex="-1" aria-labelledby="renewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<!-- <div class="modal-header bg-secondary text-white">
|
||||
<h5 class="modal-title" id="renewModalLabel">Membership Renewal Reminder</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div> -->
|
||||
<div class="modal-body">
|
||||
Your membership will be expiring soon. Click below to renew now.
|
||||
<a style="width:100%; display:block;" href="renew_membership" class="theme-btn style-two style-three mt-3">Renew Now</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" style="width:100%; display:block;" class="theme-btn" data-bs-dismiss="modal">Remind Me Later</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Updates Modal -->
|
||||
<div id="updatesModal" class="updates-modal">
|
||||
<div class="updates-modal-content">
|
||||
<span class="updates-modal-close">×</span>
|
||||
<div class="updates-modal-header">
|
||||
<h2>✨ What's New</h2>
|
||||
<h2>What's New on 4WDCSA.co.za</h2>
|
||||
</div>
|
||||
<div class="updates-modal-body">
|
||||
<div class="update-item">
|
||||
|
||||
48
progress.log
Normal file
48
progress.log
Normal file
@@ -0,0 +1,48 @@
|
||||
[2025-12-14 16:27:25] Testing Log Entry at 2025-12-14 16:27:25
|
||||
[2025-12-14 16:28:03] Testing Log Entry at 2025-12-14 16:28:03
|
||||
[2025-12-14 16:28:18] Testing Log Entry at 2025-12-14 16:28:18
|
||||
[2025-12-14 18:30:36] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:30:36] PATH: /src/api/ikhokha_webhook.php
|
||||
[2025-12-14 18:30:36] RAW BODY: {"paylinkID":"cwm225jy497j0qc","status":"SUCCESS","externalTransactionID":"693ee604c8e38","responseCode":"00","text":null}
|
||||
[2025-12-14 18:30:36] IK-SIGN: 3a2b8c9da4f1aff63c011adec10552450c5b6605f587e82ab1272914715fd4cf
|
||||
[2025-12-14 18:30:36] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:30:36] EXPECTED SIGN: f8356ee6b609e9bd44221fa71851b06171bbb7523c52dca4bde605e3bd93f654
|
||||
[2025-12-14 18:30:37] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:30:37] PATH: /src/api/ikhokha_webhook.php
|
||||
[2025-12-14 18:30:37] RAW BODY: {"paylinkID":"cwm225jy497j0qc","status":"SUCCESS","externalTransactionID":"693ee604c8e38","responseCode":"00","text":null}
|
||||
[2025-12-14 18:30:37] IK-SIGN: 3a2b8c9da4f1aff63c011adec10552450c5b6605f587e82ab1272914715fd4cf
|
||||
[2025-12-14 18:30:37] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:30:37] EXPECTED SIGN: f8356ee6b609e9bd44221fa71851b06171bbb7523c52dca4bde605e3bd93f654
|
||||
[2025-12-14 18:36:19] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:36:19] PATH: /api/ikhokha_webhook.php
|
||||
[2025-12-14 18:36:19] RAW BODY: {"paylinkID":"xx6225jyhx6x5n1","status":"SUCCESS","externalTransactionID":"693ee75cec963","responseCode":"00","text":null}
|
||||
[2025-12-14 18:36:19] IK-SIGN: 8fb11ceb8ea6b2cd6fec1773719927889f02764b0f04ad3c8543edbc9f74cf43
|
||||
[2025-12-14 18:36:19] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:36:19] EXPECTED SIGN: 7a594957e7dc8775a6f9e1f8e3a1292f4dcd10268facaec57735f937b1cd9949
|
||||
[2025-12-14 18:36:19] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:36:19] PATH: /api/ikhokha_webhook.php
|
||||
[2025-12-14 18:36:19] RAW BODY: {"paylinkID":"xx6225jyhx6x5n1","status":"SUCCESS","externalTransactionID":"693ee75cec963","responseCode":"00","text":null}
|
||||
[2025-12-14 18:36:19] IK-SIGN: 8fb11ceb8ea6b2cd6fec1773719927889f02764b0f04ad3c8543edbc9f74cf43
|
||||
[2025-12-14 18:36:19] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:36:19] EXPECTED SIGN: 7a594957e7dc8775a6f9e1f8e3a1292f4dcd10268facaec57735f937b1cd9949
|
||||
[2025-12-14 18:55:49] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:55:49] RAW BODY: {"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
||||
[2025-12-14 18:55:49] IK-SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[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-14 18:55:49] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:55:49] EXPECTED SIGN: 75c491d05ac5006b3ad4fcec20c870e860a66ea03ed5a3e8900d6e29cd601445
|
||||
[2025-12-14 18:55:49] RECEIVED SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[2025-12-14 18:55:49] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 18:55:49] RAW BODY: {"paylinkID":"433225jzs9rvk5n","status":"SUCCESS","externalTransactionID":"693eebf1a9f75","responseCode":"00","text":null}
|
||||
[2025-12-14 18:55:49] IK-SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[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-14 18:55:49] iKhokha webhook: signature mismatch
|
||||
[2025-12-14 18:55:49] EXPECTED SIGN: 75c491d05ac5006b3ad4fcec20c870e860a66ea03ed5a3e8900d6e29cd601445
|
||||
[2025-12-14 18:55:49] RECEIVED SIGN: 5cb01840f3516c964bdffc34cb1da41130bd668deffc2e4ea5a9f4cbb89b9807
|
||||
[2025-12-14 20:15:55] --- iKhokha WEBHOOK DEBUG ---
|
||||
[2025-12-14 20:15:55] RAW BODY: {"paylinkID":"ys5225k4z56x0mm","status":"SUCCESS","externalTransactionID":"693efeaca71a9","responseCode":"00","text":null}
|
||||
[2025-12-14 20:15:55] IK-SIGN: bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557
|
||||
[2025-12-14 20:15:55] ⚠️ IKHOKHA SIGNATURE CHECK BYPASSED
|
||||
[2025-12-14 20:15:55] Parsed externalTransactionID: 693efeaca71a9
|
||||
[2025-12-14 20:15:55] Parsed providerPaymentId: ys5225k4z56x0mm
|
||||
[2025-12-14 20:15:55] Parsed providerStatus: SUCCESS
|
||||
61
scripts/ikhokha_migrations/001_add_payment_columns.sql
Normal file
61
scripts/ikhokha_migrations/001_add_payment_columns.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- Migration: add iKhokha / provider metadata to payments table
|
||||
-- Migration: add iKhokha / provider metadata to payments table
|
||||
-- Compatible with MySQL versions that do not support `ADD COLUMN IF NOT EXISTS`.
|
||||
-- Run on staging first. Make a DB backup before running on production.
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE add_payment_columns_if_missing()
|
||||
BEGIN
|
||||
-- provider
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider` VARCHAR(50) NULL AFTER `status`;
|
||||
END IF;
|
||||
|
||||
-- provider_payment_id
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider_payment_id') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider_payment_id` VARCHAR(128) NULL AFTER `provider`;
|
||||
END IF;
|
||||
|
||||
-- payment_link
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'payment_link') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `payment_link` VARCHAR(512) NULL AFTER `provider_payment_id`;
|
||||
END IF;
|
||||
|
||||
-- provider_status
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider_status') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider_status` VARCHAR(50) NULL AFTER `payment_link`;
|
||||
END IF;
|
||||
|
||||
-- provider_response (JSON)
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'provider_response') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `provider_response` JSON NULL AFTER `provider_status`;
|
||||
END IF;
|
||||
|
||||
-- booking_id
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND COLUMN_NAME = 'booking_id') = 0 THEN
|
||||
ALTER TABLE `payments` ADD COLUMN `booking_id` INT NULL AFTER `user_id`;
|
||||
END IF;
|
||||
|
||||
-- index idx_provider_payment_id
|
||||
IF (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'payments' AND INDEX_NAME = 'idx_provider_payment_id') = 0 THEN
|
||||
ALTER TABLE `payments` ADD INDEX `idx_provider_payment_id` (`provider_payment_id`(128));
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL add_payment_columns_if_missing();
|
||||
DROP PROCEDURE IF EXISTS add_payment_columns_if_missing;
|
||||
|
||||
-- Notes:
|
||||
-- 1) This script creates a short stored procedure which performs existence checks
|
||||
-- against INFORMATION_SCHEMA before applying each ALTER TABLE. Run in the
|
||||
-- MySQL client or via your migration tool. It avoids syntax not supported on
|
||||
-- older MySQL versions.
|
||||
-- 2) Test on staging and make a DB dump before running on production.
|
||||
14
scripts/ikhokha_migrations/002_migrate_efts_to_payments.sql
Normal file
14
scripts/ikhokha_migrations/002_migrate_efts_to_payments.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Migration: copy existing efts records into payments for historical continuity
|
||||
-- This inserts EFT records into payments table as payments with provider='eft'.
|
||||
-- Run only after verifying step 001 has been applied and a backup exists.
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
INSERT IGNORE INTO `payments` (`payment_id`, `user_id`, `amount`, `status`, `date`, `description`, `provider`, `provider_payment_id`, `provider_status`)
|
||||
SELECT `eft_id`, `user_id`, `amount`, `status`, `timestamp`, `description`, 'eft', `eft_id`, `status` FROM `efts`;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Notes:
|
||||
-- 1) `INSERT IGNORE` prevents duplicate primary-key errors (payments.payment_id is PK).
|
||||
-- 2) After running, review migrated rows and ensure admin workflows still operate.
|
||||
21
scripts/ikhokha_migrations/README.md
Normal file
21
scripts/ikhokha_migrations/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# iKhokha Migration SQL
|
||||
|
||||
This folder contains SQL migration files to add iKhokha/provider metadata to the `payments` table and to migrate legacy `efts` records.
|
||||
|
||||
Order to run:
|
||||
|
||||
1. Backup your database (mysqldump or preferred tool).
|
||||
2. Apply `001_add_payment_columns.sql` on staging and verify schema changes.
|
||||
3. (Optional) Apply `002_migrate_efts_to_payments.sql` to copy legacy `efts` into `payments`.
|
||||
|
||||
Commands (example using MySQL client):
|
||||
|
||||
```bash
|
||||
mysql -u dbuser -p databasename < scripts/ikhokha_migrations/001_add_payment_columns.sql
|
||||
mysql -u dbuser -p databasename < scripts/ikhokha_migrations/002_migrate_efts_to_payments.sql
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Always test on staging before running in production.
|
||||
- `provider_response` is a JSON column used to store raw provider responses for audit.
|
||||
- If you prefer not to migrate `efts`, skip step 3 and keep legacy POP handling.
|
||||
293
src/api/ikhokha_webhook.php
Normal file
293
src/api/ikhokha_webhook.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?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");
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Read raw request and headers (DO NOT MODIFY RAW BODY)
|
||||
* ==========================================================
|
||||
*/
|
||||
$raw = file_get_contents('php://input');
|
||||
|
||||
if ($raw === false) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: unable to read raw input');
|
||||
exit('No body');
|
||||
}
|
||||
|
||||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
$headers = array_change_key_case($headers, CASE_LOWER);
|
||||
|
||||
$ikSign = $headers['ik-sign'] ?? null;
|
||||
$ikAppId = $headers['ik-appid'] ?? null;
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Basic header presence check
|
||||
* ==========================================================
|
||||
*/
|
||||
if (empty($ikSign) || empty($ikAppId)) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: missing IK-SIGN or IK-APPID');
|
||||
exit('Missing headers');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Signature verification
|
||||
* HMAC_SHA256( path + raw_body, app_secret )
|
||||
* ==========================================================
|
||||
*/
|
||||
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
||||
|
||||
if (empty($secret)) {
|
||||
http_response_code(500);
|
||||
progress_log('iKhokha webhook: app secret not configured');
|
||||
exit('Server misconfigured');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Debug logging (disable once stable)
|
||||
progress_log('--- iKhokha WEBHOOK DEBUG ---');
|
||||
progress_log('RAW BODY: ' . $raw);
|
||||
progress_log('IK-SIGN: ' . $ikSign);
|
||||
|
||||
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
||||
$bypass = ($_ENV['IKHOKHA_BYPASS_SIGNATURE'] ?? 'false') === 'true';
|
||||
|
||||
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)) {
|
||||
http_response_code(403);
|
||||
progress_log('iKhokha webhook: signature mismatch');
|
||||
progress_log('EXPECTED SIGN: ' . $expected);
|
||||
progress_log('RECEIVED SIGN: ' . $ikSign);
|
||||
// Audit signature mismatch
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, ['expected' => $expected, 'received' => $ikSign]);
|
||||
}
|
||||
exit('Invalid signature');
|
||||
}
|
||||
|
||||
} else {
|
||||
progress_log('⚠️ IKHOKHA SIGNATURE CHECK BYPASSED');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Decode payload
|
||||
* ==========================================================
|
||||
*/
|
||||
$payload = json_decode($raw, true);
|
||||
|
||||
if (!is_array($payload)) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: invalid JSON');
|
||||
exit('Invalid JSON');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Extract data safely (iKhokha is inconsistent)
|
||||
* ==========================================================
|
||||
*/
|
||||
$data = $payload['data'] ?? $payload;
|
||||
|
||||
$externalTransactionID =
|
||||
$data['externalTransactionID']
|
||||
?? $data['externalTransactionId']
|
||||
?? $data['externalTxId']
|
||||
?? null;
|
||||
|
||||
$providerPaymentId =
|
||||
$data['paylinkID']
|
||||
?? $data['id']
|
||||
?? null;
|
||||
|
||||
$providerStatus =
|
||||
$data['status']
|
||||
?? $payload['status']
|
||||
?? null;
|
||||
|
||||
progress_log('Parsed externalTransactionID: ' . $externalTransactionID);
|
||||
progress_log('Parsed providerPaymentId: ' . $providerPaymentId);
|
||||
progress_log('Parsed providerStatus: ' . print_r($providerStatus, true));
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Locate local payment
|
||||
* ==========================================================
|
||||
*/
|
||||
$localPaymentId = null;
|
||||
$booking_id = null;
|
||||
$user_id = null;
|
||||
$description = null;
|
||||
|
||||
if ($externalTransactionID) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT payment_id, user_id, booking_id, description
|
||||
FROM payments
|
||||
WHERE payment_id = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $externalTransactionID);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$localPaymentId = $row['payment_id'];
|
||||
$booking_id = $row['booking_id'];
|
||||
$user_id = $row['user_id'];
|
||||
$description = $row['description'];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$localPaymentId && $providerPaymentId) {
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT payment_id, user_id, booking_id, description
|
||||
FROM payments
|
||||
WHERE provider_payment_id = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $providerPaymentId);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$localPaymentId = $row['payment_id'];
|
||||
$booking_id = $row['booking_id'];
|
||||
$user_id = $row['user_id'];
|
||||
$description = $row['description'];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$localPaymentId) {
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Persist provider response
|
||||
* ==========================================================
|
||||
*/
|
||||
$update = $conn->prepare(
|
||||
"UPDATE payments
|
||||
SET provider_payment_id = ?,
|
||||
provider_status = ?,
|
||||
provider_response = ?
|
||||
WHERE payment_id = ?"
|
||||
);
|
||||
|
||||
if ($update) {
|
||||
$update->bind_param(
|
||||
'ssss',
|
||||
$providerPaymentId,
|
||||
$providerStatus,
|
||||
$raw,
|
||||
$localPaymentId
|
||||
);
|
||||
$update->execute();
|
||||
$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
|
||||
* ==========================================================
|
||||
*/
|
||||
$normalized = strtoupper(trim((string)$providerStatus));
|
||||
|
||||
if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
||||
|
||||
// Mark payment as PAID
|
||||
$setPaid = $conn->prepare(
|
||||
"UPDATE payments SET status = 'PAID' WHERE payment_id = ?"
|
||||
);
|
||||
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 (!empty($booking_id)) {
|
||||
$upd = $conn->prepare(
|
||||
"UPDATE bookings SET status = 'PAID' WHERE booking_id = ?"
|
||||
);
|
||||
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 {
|
||||
$upd = $conn->prepare(
|
||||
"UPDATE membership_fees
|
||||
SET payment_status = 'PAID'
|
||||
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
|
||||
if (!empty($user_id)) {
|
||||
sendPaymentConfirmation(
|
||||
getEmail($user_id),
|
||||
getFullName($user_id),
|
||||
$description
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Acknowledge webhook
|
||||
* ==========================================================
|
||||
*/
|
||||
http_response_code(200);
|
||||
echo 'OK';
|
||||
10
src/api/test_log.php
Normal file
10
src/api/test_log.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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");
|
||||
|
||||
echo "Test log entry attempt...";
|
||||
progress_log('Testing Log Entry at ' . date('Y-m-d H:i:s'));
|
||||
echo "Test log entry created.";
|
||||
@@ -29,6 +29,39 @@ function openDatabaseConnection()
|
||||
return $conn;
|
||||
}
|
||||
|
||||
function progress_log($message, $context = null)
|
||||
{
|
||||
try {
|
||||
// Site root (same logic you already use elsewhere)
|
||||
$rootPath = dirname(dirname(__DIR__));
|
||||
$logFile = $rootPath . '/progress.log';
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
|
||||
// Normalize message
|
||||
if (is_array($message) || is_object($message)) {
|
||||
$message = json_encode($message, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
// Normalize context (optional extra data)
|
||||
if ($context !== null) {
|
||||
if (is_array($context) || is_object($context)) {
|
||||
$context = json_encode($context, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
$message .= ' | CONTEXT: ' . $context;
|
||||
}
|
||||
|
||||
$line = "[{$timestamp}] {$message}" . PHP_EOL;
|
||||
|
||||
// Append atomically
|
||||
file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
// Never allow logging failures to break execution
|
||||
// Silent by design
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getPriceByDescription($description)
|
||||
{
|
||||
@@ -484,7 +517,7 @@ function getUserMemberStatus($user_id)
|
||||
}
|
||||
|
||||
// Step 3: Check membership fees table for valid payment status and membership_end_date
|
||||
$queryFees = "SELECT payment_status, membership_end_date FROM membership_fees WHERE user_id = ?";
|
||||
$queryFees = "SELECT payment_status, membership_end_date, renewal_period_end FROM membership_fees WHERE user_id = ?";
|
||||
$stmtFees = $conn->prepare($queryFees);
|
||||
if (!$stmtFees) {
|
||||
error_log("Failed to prepare fees query: " . $conn->error);
|
||||
@@ -507,6 +540,7 @@ function getUserMemberStatus($user_id)
|
||||
$fees = $resultFees->fetch_assoc();
|
||||
$payment_status = $fees['payment_status'];
|
||||
$membership_end_date = $fees['membership_end_date'];
|
||||
$renewal_period_end = $fees['renewal_period_end'];
|
||||
|
||||
// Validate payment status and membership_end_date
|
||||
$current_date = new DateTime();
|
||||
@@ -515,6 +549,12 @@ function getUserMemberStatus($user_id)
|
||||
if ($payment_status === "PAID" && $current_date <= $membership_end_date_obj) {
|
||||
$conn->close();
|
||||
return true; // Direct membership is active
|
||||
}elseif ($payment_status === "PENDING RENEWAL") {
|
||||
$renewal_period_end_obj = DateTime::createFromFormat('Y-m-d', $renewal_period_end);
|
||||
if ($current_date <= $renewal_period_end_obj) {
|
||||
$conn->close();
|
||||
return true; // Direct membership is in renewal period
|
||||
}
|
||||
} else {
|
||||
// Direct membership is not active, check if user is linked to another active membership
|
||||
error_log("Direct membership not active for user_id: $user_id - checking linked memberships");
|
||||
@@ -725,6 +765,120 @@ function processPayment($payment_id, $amount, $description)
|
||||
|
||||
}
|
||||
|
||||
function createIkhokhaPayment($payment_id, $amount, $description, $publicRef)
|
||||
{
|
||||
|
||||
// Base requester URL: prefer explicit env var, otherwise build from request
|
||||
$baseUrl = rtrim($_ENV['IKHOKHA_REQUESTER_URL'] ?? ($_SERVER['REQUEST_SCHEME'] ?? 'https') . '://' . ($_SERVER['HTTP_HOST'] ?? ''), '/');
|
||||
|
||||
$endpoint = $_ENV['IKHOKHA_ENDPOINT'];
|
||||
$appID = $_ENV['IKHOKHA_APP_ID'];
|
||||
$appSecret = $_ENV['IKHOKHA_APP_SECRET'];
|
||||
$requestBody = [
|
||||
"entityID" => $payment_id,
|
||||
"externalEntityID" => $payment_id,
|
||||
"amount" => $amount * 100,
|
||||
"currency" => "ZAR",
|
||||
"requesterUrl" => $_ENV['IKHOKHA_REQUESTER_URL'] ?? $baseUrl,
|
||||
"description" => $description,
|
||||
"paymentReference" => $description,
|
||||
"mode" => $_ENV['IKHOKHA_MODE'] ?? 'live',
|
||||
"externalTransactionID" => $payment_id,
|
||||
"urls" => [
|
||||
"callbackUrl" => $_ENV['IKHOKHA_CALLBACK_URL'],
|
||||
|
||||
"successPageUrl" => $_ENV['IKHOKHA_SUCCESS_URL']
|
||||
. "?ref=" . urlencode($publicRef),
|
||||
|
||||
"failurePageUrl" => $_ENV['IKHOKHA_FAILURE_URL']
|
||||
. "?ref=" . urlencode($publicRef),
|
||||
|
||||
"cancelUrl" => $_ENV['IKHOKHA_CANCEL_URL']
|
||||
. "?ref=" . urlencode($publicRef),
|
||||
]
|
||||
|
||||
];
|
||||
$stringifiedBody = json_encode($requestBody);
|
||||
$payloadToSign = createPayloadToSign($endpoint, $stringifiedBody);
|
||||
$ikSign = generateSignature($payloadToSign, $appSecret);
|
||||
// Initialize cURL session
|
||||
$ch = curl_init($endpoint);
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
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);
|
||||
|
||||
// Persist provider metadata into payments table if we have a response
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$provider = 'ikhokha';
|
||||
$provider_payment_id = $resp['paylinkID'] ?? $resp['paylinkId'] ?? $resp['paylink_id'] ?? null;
|
||||
$payment_link = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
$provider_status = $resp['responseCode'] ?? ($resp['status'] ?? null);
|
||||
$provider_response = json_encode($resp);
|
||||
|
||||
// Update payments row with provider info. If a paylink was created (responseCode == '00'), keep status awaiting payment.
|
||||
$newStatus = null;
|
||||
if (!empty($payment_link) && ($provider_status === '00' || $provider_status === '0' || $provider_status === 0)) {
|
||||
$newStatus = 'AWAITING PAYMENT';
|
||||
}
|
||||
|
||||
if ($newStatus) {
|
||||
$stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ?, status = ? WHERE payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('sssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $newStatus, $payment_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
} else {
|
||||
$stmt = $conn->prepare("UPDATE payments SET provider = ?, provider_payment_id = ?, payment_link = ?, provider_status = ?, provider_response = ? WHERE payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ssssss', $provider, $provider_payment_id, $payment_link, $provider_status, $provider_response, $payment_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
|
||||
return $resp;
|
||||
}
|
||||
|
||||
function escapeString($str) {
|
||||
$escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
||||
$cleaned = str_replace('\/', '/', $escaped);
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
function createPayloadToSign($urlPath, $body) {
|
||||
$parsedUrl = parse_url($urlPath);
|
||||
$basePath = $parsedUrl['path'];
|
||||
if (!$basePath) {
|
||||
throw new Exception("No path present in the URL");
|
||||
}
|
||||
$payload = $basePath . $body;
|
||||
$escapedPayloadString = escapeString($payload);
|
||||
return $escapedPayloadString;
|
||||
}
|
||||
|
||||
function generateSignature($payloadToSign, $secret) {
|
||||
return hash_hmac('sha256', $payloadToSign, $secret);
|
||||
}
|
||||
|
||||
function processMembershipPayment($payment_id, $amount, $description)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
@@ -1302,7 +1456,7 @@ function getInitialSurname($user_id)
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
$initial = strtoupper(substr($first_name, 0, 1));
|
||||
return $initial . ". " . $last_name;
|
||||
return $initial . "." . $last_name;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -1313,6 +1467,89 @@ function getInitialSurname($user_id)
|
||||
}
|
||||
}
|
||||
|
||||
function generatePaymentRef(string $type, ?int $course_trip_id, int $user_id): string
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
|
||||
// 1. Normalize type
|
||||
$type = strtoupper($type);
|
||||
|
||||
// 2. Build prefix
|
||||
switch ($type) {
|
||||
case 'SUBS':
|
||||
$year = (int)date('Y');
|
||||
$month = (int)date('n');
|
||||
|
||||
// If December, subscriptions are for next year
|
||||
if ($month === 12) {
|
||||
$year++;
|
||||
}
|
||||
|
||||
$prefix = "SUBS_" . $year;
|
||||
break;
|
||||
|
||||
case 'COURSE':
|
||||
if (!$course_trip_id) {
|
||||
throw new Exception("course_trip_id is required for COURSE payments");
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT code FROM courses WHERE course_id = ?"
|
||||
);
|
||||
$stmt->bind_param("i", $course_trip_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($code);
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
throw new Exception("Invalid course_id: {$course_trip_id}");
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$prefix = "COURSE_" . strtoupper($code);
|
||||
break;
|
||||
|
||||
case 'TRIP':
|
||||
if (!$course_trip_id) {
|
||||
throw new Exception("course_trip_id is required for TRIP payments");
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare(
|
||||
"SELECT trip_code FROM trips WHERE trip_id = ?"
|
||||
);
|
||||
$stmt->bind_param("i", $course_trip_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($trip_code);
|
||||
|
||||
if (!$stmt->fetch()) {
|
||||
throw new Exception("Invalid trip_id: {$course_trip_id}");
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$prefix = "TRIP_" . strtoupper($trip_code);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown payment type: {$type}");
|
||||
}
|
||||
|
||||
// 3. Get user initials + surname
|
||||
$namePart = strtoupper(getInitialSurname($user_id));
|
||||
|
||||
if (!$namePart) {
|
||||
throw new Exception("User not found for user_id: {$user_id}");
|
||||
}
|
||||
|
||||
// 4. Add short entropy (trimmed for aesthetics)
|
||||
$entropy = substr(shortEntropy(), -3);
|
||||
|
||||
return "{$prefix}_{$namePart}_{$entropy}";
|
||||
}
|
||||
|
||||
function shortEntropy(): string {
|
||||
return strtoupper(base_convert((string)(microtime(true) * 1000), 10, 36));
|
||||
}
|
||||
|
||||
|
||||
function getLastName($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
@@ -1984,6 +2221,8 @@ function processLegacyMembership($user_id) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* SECURITY WARNING: This function uses dynamic table/column names which makes it vulnerable to SQL injection.
|
||||
* ONLY call this function with whitelisted table and column names.
|
||||
@@ -3225,3 +3464,73 @@ function unlinkSecondaryUser($link_id, $primary_user_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the payment_link for a given internal payment_id from the payments table.
|
||||
* Returns the payment_link string on success or null if not found / on error.
|
||||
*
|
||||
* @param string $payment_id
|
||||
* @return string|null
|
||||
*/
|
||||
function getPaymentLinkByPaymentId($payment_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT payment_link FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('s', $payment_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($payment_link);
|
||||
$found = $stmt->fetch();
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
if ($found) {
|
||||
return $payment_link;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the membership_end_date for a given user_id from membership_fees.
|
||||
* Returns the date string (Y-m-d) or null if not found.
|
||||
*
|
||||
* @param int $user_id
|
||||
* @return string|null
|
||||
*/
|
||||
function getMembershipEndDate($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT membership_end_date FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($membership_end_date);
|
||||
$found = $stmt->fetch();
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
if ($found) {
|
||||
return $membership_end_date;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ $user_id = $_SESSION['user_id'];
|
||||
// Loop through each row
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$booking_id = $row['booking_id'];
|
||||
$payment_id = $row['payment_id'];
|
||||
$booking_type = $row['booking_type'];
|
||||
$from_date = $row['from_date'];
|
||||
$to_date = $row['to_date'];
|
||||
@@ -267,8 +268,8 @@ $user_id = $_SESSION['user_id'];
|
||||
<div class="destination-footer">
|
||||
<span class="price"><span>Booking Total: R ' . number_format($amount, 2) . '</span></span>';
|
||||
if ($status == "AWAITING PAYMENT") {
|
||||
echo '<a href="' . url('payment_confirmation') . '?token=' . encryptData($booking_id, $salt) . '" class="theme-btn style-two style-three">
|
||||
<span data-hover="PAYMENT INFO">' . $status . '</span>
|
||||
echo '<a href="' . getPaymentLinkByPaymentId($payment_id) . '" class="theme-btn style-two style-three">
|
||||
<span data-hover="PAY NOW">' . $status . '</span>
|
||||
</a>';
|
||||
} else {
|
||||
echo '<a href="" class="theme-btn style-two style-three">
|
||||
|
||||
@@ -177,7 +177,7 @@ $page_id = 'driver_training';
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$button_text = "PROCEED TO PAYMENT";
|
||||
$button_disabled = "";
|
||||
if (!$result || $result->num_rows == 0) {
|
||||
$button_text = "No booking dates available";
|
||||
@@ -189,8 +189,9 @@ $page_id = 'driver_training';
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<a href="contact">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
|
||||
$is_published = $row['published'] ?? 0;
|
||||
?>
|
||||
<div class="admin-actions mt-20">
|
||||
<button type="button" class="theme-btn" style="width: 100%; id="publishBtn" onclick="toggleTripPublished(<?php echo $trip_id; ?>)">
|
||||
<button type="button" class="theme-btn" style="width: 100%; id=" publishBtn" onclick="toggleTripPublished(<?php echo $trip_id; ?>)">
|
||||
<?php if ($is_published): ?>
|
||||
<i class="fas fa-eye-slash"></i> Unpublish Trip
|
||||
<?php else: ?>
|
||||
@@ -594,13 +594,14 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
|
||||
<span data-hover="Book Now">Book Now</span>
|
||||
<span data-hover="PROCEED TO PAYMENT">PROCEED TO PAYMENT</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<div class="text-center">
|
||||
<a href="contact">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,8 @@ $result = $stmt->get_result();
|
||||
// Fetch single record
|
||||
$membership = $result->fetch_assoc();
|
||||
|
||||
$payment_link = getPaymentLinkByPaymentId($membership['payment_id']);
|
||||
|
||||
// Fetch membership application data using mysqli
|
||||
$query = "SELECT * FROM membership_application WHERE user_id = ?";
|
||||
$stmt = $conn->prepare($query);
|
||||
@@ -186,8 +188,8 @@ if (empty($application['id_number'])) {
|
||||
|
||||
<td><?php echo htmlspecialchars($membership['payment_amount']); ?></td>
|
||||
<td><?php echo htmlspecialchars($membership['payment_id']); ?></td>
|
||||
<?php if ($membership['payment_status'] == "PENDING") { ?>
|
||||
<td><a href='membership_payment' class='theme-btn style-two style-three' style='padding: 0px 14px;'><span data-hover='VIEW PAYMENT INFO'>AWAITING PAYMENT</span></a></td>
|
||||
<?php if ($membership['payment_status'] == "AWAITING PAYMENT" || $membership['payment_status'] == "PENDING RENEWAL") { ?>
|
||||
<td><a href='<?= $payment_link; ?>' class='theme-btn style-two style-three' style='padding: 0px 14px;'><span data-hover='PAY NOW'>PENDING RENEWAL</span></a></td>
|
||||
<?php } else { ?>
|
||||
<td><?php echo htmlspecialchars($membership['payment_status']); ?></td>
|
||||
<?php } ?>
|
||||
@@ -204,17 +206,27 @@ if (empty($application['id_number'])) {
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Check if membership has expired
|
||||
// Show renew button when current date is within 3 months of membership end
|
||||
$membership_end_date = $membership ? $membership['membership_end_date'] : null;
|
||||
$today = date('Y-m-d');
|
||||
|
||||
if ($membership_end_date && strtotime($today) > strtotime($membership_end_date)) {
|
||||
if ($membership_end_date) {
|
||||
try {
|
||||
$end = new DateTime($membership_end_date);
|
||||
$threeMonthsBefore = (clone $end)->modify('-3 months')->format('Y-m-d');
|
||||
} catch (Exception $e) {
|
||||
// Fallback using strtotime if DateTime parsing fails
|
||||
$threeMonthsBefore = date('Y-m-d', strtotime($membership_end_date . ' -3 months'));
|
||||
}
|
||||
|
||||
if (strtotime($today) >= strtotime($threeMonthsBefore)) {
|
||||
echo '
|
||||
<a href="renew_membership" class="theme-btn style-two bgc-secondary" style="width:100%; margin-top: 20px; background-color: #63ab45; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<span data-hover="Renew Membership">Renew Membership</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
|
||||
@@ -67,12 +67,38 @@ $stmt->bind_result($user_email);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
$conn->close();
|
||||
// If request includes payment_id, fetch provider paylink from payments table
|
||||
if (!isset($_GET['token']) || empty($_GET['token'])) {
|
||||
header("Location: membership_details");
|
||||
exit();
|
||||
}
|
||||
$token = $_GET['token'];
|
||||
// echo $token;
|
||||
|
||||
// Sanitize the trip_id to prevent SQL injection
|
||||
$payment_id = decryptData($token, $_ENV['SALT']);
|
||||
$payment_link = null;
|
||||
if ($payment_id) {
|
||||
$pstmt = $conn->prepare("SELECT payment_link, amount, status, provider FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('s', $payment_id);
|
||||
$pstmt->execute();
|
||||
$pres = $pstmt->get_result();
|
||||
if ($prow = $pres->fetch_assoc()) {
|
||||
$payment_link = $prow['payment_link'];
|
||||
// prefer payments.amount if present
|
||||
if (!empty($prow['amount'])) {
|
||||
$payment_amount = $prow['amount'];
|
||||
}
|
||||
}
|
||||
$pstmt->close();
|
||||
}
|
||||
}
|
||||
?><?php
|
||||
$pageTitle = 'Membership Payment';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Membership' => 'membership.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
?>
|
||||
<!-- Contact Form Area start -->
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
@@ -83,13 +109,28 @@ $conn->close();
|
||||
<?php echo
|
||||
'<h5>Membership Start Date: ' . $membership_start_date . '<br>Membership Renewal Date: ' . $membership_end_date . '</h5>'; ?>
|
||||
</div>
|
||||
<p>Your invoice has been sent to <b><?php echo htmlspecialchars($user_email); ?></b>. Please upload your proof of payment below.</p>
|
||||
|
||||
<?php if (!empty($payment_link)) { ?>
|
||||
<h5>Payment Details:</h5>
|
||||
<p>Amount: R <?php echo number_format($payment_amount, 2); ?></p>
|
||||
<p>Reference: <?php echo htmlspecialchars($payment_id); ?></p>
|
||||
<a href="<?php echo htmlspecialchars($payment_link); ?>" class="theme-btn style-two style-three" style="width:100%;" target="_blank" rel="noopener noreferrer">
|
||||
<span data-hover="Pay Now with iKhokha">Pay Now with iKhokha</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<div class="text-center">
|
||||
<p>You will be redirected to iKhokha's Secure payment gateway.</p>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
<?php } else { ?>
|
||||
<p>Please upload your proof of payment below.</p>
|
||||
<h5>Payment Details:</h5>
|
||||
<p>The Four Wheel Drive Club of Southern Africa<br>FNB<br>Account Number: 58810022334<br>Branch code: 250655<br>Reference: <?php echo htmlspecialchars($eft_id); ?><br>Amount: R <?php echo number_format($payment_amount, 2); ?></p>
|
||||
<a href="submit_pop" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Submit Proof of Payment">Submit Proof of Payment</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
|
||||
@@ -1,26 +1,101 @@
|
||||
<?php
|
||||
require_once("env.php");
|
||||
require_once("session.php");
|
||||
require_once("connection.php");
|
||||
require_once("functions.php");
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$eft_id = strtoupper("SUBS " . date("Y") . " " . getLastName($user_id));
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees ' . date("Y") . " " . getLastName($user_id);
|
||||
$is_logged_in = isset($_SESSION['user_id']);
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
} else {
|
||||
header('Location: login.php');
|
||||
exit(); // Stop further script execution
|
||||
}
|
||||
|
||||
$payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed
|
||||
//if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
|
||||
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($payment_status);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
if ($payment_status === 'PENDING RENEWAL') {
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$payment_id = generatePaymentRef('SUBS', null, $user_id);
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
$payment_date = date('Y-m-d');
|
||||
$membership_start_date = date('Y-01-01');
|
||||
$membership_end_date = date('Y-12-31');
|
||||
$renewal_period_end = getMembershipEndDate($user_id);
|
||||
// Hardcode membership start date to 2026-03-01 per request
|
||||
$renewed_membership_start_date = '2026-03-01';
|
||||
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_amount = ?, payment_date = ?, membership_start_date = ?, membership_end_date = ?, payment_status = 'PENDING', payment_id = ? WHERE user_id = ?");
|
||||
$stmt->bind_param("dssssi", $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id, $user_id);
|
||||
// Set membership_end_date to the last day of February in the following year
|
||||
$renewed_membership_end_date = '2027-02-28';
|
||||
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_amount = ?, payment_date = ?, membership_start_date = ?, membership_end_date = ?, renewal_period_end = ?, payment_status = 'PENDING RENEWAL', payment_id = ? WHERE user_id = ?");
|
||||
$stmt->bind_param("dsssssi", $payment_amount, $payment_date, $renewed_membership_start_date, $renewed_membership_end_date, $renewal_period_end, $payment_id, $user_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Commit the transaction
|
||||
$conn->commit();
|
||||
addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description);
|
||||
|
||||
// Audit: user initiated membership renewal
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'MEMBERSHIP_RENEWAL_INITIATED', 'membership_fees', null, ['payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||
}
|
||||
|
||||
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
if ($checkP) {
|
||||
$checkP->bind_param('s', $payment_id);
|
||||
$checkP->execute();
|
||||
$r = $checkP->get_result()->fetch_assoc();
|
||||
$exists = intval($r['cnt']) > 0;
|
||||
$checkP->close();
|
||||
} else {
|
||||
$exists = false;
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
// If current month is December, attribute the membership year to the next year
|
||||
$currentYear = intval(date('Y'));
|
||||
$month = intval(date('n'));
|
||||
if ($month === 12) {
|
||||
$membershipYear = $currentYear + 1;
|
||||
} else {
|
||||
$membershipYear = $currentYear;
|
||||
}
|
||||
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$ins = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if ($ins) {
|
||||
$ins->bind_param('sidsss', $payment_id, $user_id, $payment_amount, $status, $description, $publicRef);
|
||||
$ins->execute();
|
||||
$ins->close();
|
||||
}
|
||||
}
|
||||
|
||||
// Create iKhokha paylink via helper (functions.php)
|
||||
try {
|
||||
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
$token = encryptData($payment_id, $_ENV['SALT']);
|
||||
if ($paylink) {
|
||||
header('Location: membership_payment?token=' . $token);
|
||||
exit();
|
||||
} else {
|
||||
header("Location: membership_details");
|
||||
exit();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Log but do not fail signature save
|
||||
error_log('iKhokha create error: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
header("Location:membership_payment.php");
|
||||
// Success message
|
||||
$response = [
|
||||
|
||||
@@ -156,7 +156,7 @@ $page_id = 'bush_mechanics';
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$button_text = "PROCEED TO PAYMENT";
|
||||
$button_disabled = "";
|
||||
if (!$result || $result->num_rows == 0) {
|
||||
$button_text = "No booking dates available";
|
||||
@@ -168,8 +168,9 @@ $page_id = 'bush_mechanics';
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<a href="contact">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -105,17 +105,29 @@ if (isset($_SESSION['user_id'])) {
|
||||
response = JSON.parse(response);
|
||||
}
|
||||
if (response.status === 'success') {
|
||||
// Check if the user has paid
|
||||
// If provider returned a direct paylink, go there immediately
|
||||
if (response.paylinkUrl) {
|
||||
window.location.href = 'membership_payment?token=' + encodeURIComponent(response.token);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a payment_id, redirect to membership_payment with it
|
||||
// if (response.payment_id) {
|
||||
// setTimeout(function() {
|
||||
// window.location.href = 'membership_payment.php?payment_id=' + encodeURIComponent(response.token);
|
||||
// }, 800);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Fallback behaviour: check paymentStatus
|
||||
if (response.paymentStatus === 'PAID') {
|
||||
// Redirect to membership_details.php if paid
|
||||
setTimeout(function() {
|
||||
window.location.href = 'membership_details.php';
|
||||
}, 2000); // 2-second delay before redirecting
|
||||
}, 1200);
|
||||
} else {
|
||||
// Redirect to membership_payment.php if not paid
|
||||
setTimeout(function() {
|
||||
window.location.href = 'membership_payment.php';
|
||||
}, 2000); // 2-second delay before redirecting
|
||||
}, 1200);
|
||||
}
|
||||
} else {
|
||||
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
|
||||
|
||||
@@ -154,7 +154,7 @@ $page_id = 'rescue_recovery';
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$button_text = "Book Now";
|
||||
$button_text = "PROCEED TO PAYMENT";
|
||||
$button_disabled = "";
|
||||
if (!$result || $result->num_rows == 0) {
|
||||
$button_text = "No booking dates available";
|
||||
@@ -165,9 +165,11 @@ $page_id = 'rescue_recovery';
|
||||
<span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="mailto:info@4wdcsa.co.za">Need some help?</a>
|
||||
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
73
src/pages/payment/cancel.php
Normal file
73
src/pages/payment/cancel.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$ref = $_GET['ref'] ?? null;
|
||||
$payment = null;
|
||||
$error_message = null;
|
||||
|
||||
if ($ref) {
|
||||
$stmt = $conn->prepare("SELECT payment_id, amount, payment_link, status, provider, provider_payment_id, public_ref, description FROM payments WHERE public_ref = ? OR payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ss', $ref, $ref);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$payment = $row;
|
||||
} else {
|
||||
$error_message = 'Payment record not found for the supplied reference.';
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error_message = 'Database error: ' . $conn->error;
|
||||
}
|
||||
} else {
|
||||
$error_message = 'No reference supplied.';
|
||||
}
|
||||
|
||||
$pageTitle = 'Payment Cancelled';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Payment' => 'membership_payment.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Payment Cancelled</span>
|
||||
<h5>Your payment was cancelled or you returned without completing it.</h5>
|
||||
</div>
|
||||
|
||||
<?php if ($error_message) { ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($error_message); ?></div>
|
||||
<?php } else { ?>
|
||||
<p>Your payment appears to have been cancelled. If this was a mistake you can try again below.</p>
|
||||
<ul>
|
||||
<li><strong>Reference:</strong> <?php echo htmlspecialchars($payment['payment_id'] ?? $payment['public_ref']); ?></li>
|
||||
<li><strong>Amount:</strong> R <?php echo number_format($payment['amount'] ?? 0, 2); ?></li>
|
||||
<li><strong>Description:</strong> <?php echo htmlspecialchars($payment['description'] ?? ''); ?></li>
|
||||
</ul>
|
||||
|
||||
<?php if (!empty($payment['payment_id'])) { ?>
|
||||
<a href="<?php echo $payment['payment_link']; ?>" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Retry Payment">Retry Payment</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
<p style="margin-top:10px;">Contact <a href="mailto:info@4wdcsa.co.za">info@4wdcsa.co.za</a> if you need assistance.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="/assets/images/logos/weblogo.png" alt="Logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
75
src/pages/payment/failure.php
Normal file
75
src/pages/payment/failure.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$ref = $_GET['ref'] ?? null;
|
||||
$payment = null;
|
||||
$error_message = null;
|
||||
|
||||
if ($ref) {
|
||||
$stmt = $conn->prepare("SELECT payment_id, amount, payment_link, status, provider, provider_payment_id, public_ref, description FROM payments WHERE public_ref = ? OR payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ss', $ref, $ref);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$payment = $row;
|
||||
} else {
|
||||
$error_message = 'Payment record not found for the supplied reference.';
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error_message = 'Database error: ' . $conn->error;
|
||||
}
|
||||
} else {
|
||||
$error_message = 'No reference supplied.';
|
||||
}
|
||||
|
||||
$pageTitle = 'Payment Failed';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Payment' => 'membership_payment.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Payment Failed</span>
|
||||
<h5>Unfortunately your payment could not be completed.</h5>
|
||||
</div>
|
||||
|
||||
<?php if ($error_message) { ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($error_message); ?></div>
|
||||
<?php } else { ?>
|
||||
<p>We were unable to process your payment. You can try again or contact support for assistance.</p>
|
||||
<ul>
|
||||
<li><strong>Reference:</strong> <?php echo htmlspecialchars($payment['payment_id'] ?? $payment['public_ref']); ?></li>
|
||||
<li><strong>Amount:</strong> R <?php echo number_format($payment['amount'] ?? 0, 2); ?></li>
|
||||
<li><strong>Provider:</strong> <?php echo htmlspecialchars($payment['provider'] ?? ''); ?></li>
|
||||
<li><strong>Description:</strong> <?php echo htmlspecialchars($payment['description'] ?? ''); ?></li>
|
||||
<li><strong>Status:</strong> <?php echo htmlspecialchars($payment['status'] ?? ''); ?></li>
|
||||
</ul>
|
||||
|
||||
<?php if (!empty($payment['payment_id'])) { ?>
|
||||
<a href="<?php echo htmlspecialchars($payment['payment_link']); ?>" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Try Again">Try Again</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
|
||||
<p style="margin-top:10px;">Or contact <a href="mailto:info@4wdcsa.co.za">info@4wdcsa.co.za</a> for help.</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="/assets/images/logos/weblogo.png" alt="Logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
84
src/pages/payment/success.php
Normal file
84
src/pages/payment/success.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
|
||||
$ref = $_GET['ref'] ?? null;
|
||||
$payment = null;
|
||||
$error_message = null;
|
||||
|
||||
if ($ref) {
|
||||
$stmt = $conn->prepare("SELECT payment_id, amount, payment_link, status, provider, provider_payment_id, public_ref, description, booking_id FROM payments WHERE public_ref = ? OR payment_id = ? LIMIT 1");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('ss', $ref, $ref);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$payment = $row;
|
||||
} else {
|
||||
$error_message = 'Payment record not found for the supplied reference.';
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error_message = 'Database error: ' . $conn->error;
|
||||
}
|
||||
} else {
|
||||
$error_message = 'No reference supplied.';
|
||||
}
|
||||
|
||||
$pageTitle = 'Payment Successful';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Payment' => 'membership_payment.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Payment Successful</span>
|
||||
<h5>Thank you — your payment was received.</h5>
|
||||
</div>
|
||||
<?php
|
||||
$booking_id = $payment['booking_id'] ?? null;
|
||||
|
||||
if($booking_id == null) { ?>
|
||||
<h5>MEMBERSHIP STATUS: <?= getUserMemberStatus($user_id) ? 'ACTIVE' : 'INACTIVE'; ?></h5>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($error_message) { ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($error_message); ?></div>
|
||||
<?php } else { ?>
|
||||
<p>Your payment has been processed successfully. Below are the details we received:</p>
|
||||
<ul>
|
||||
<li><strong>Reference:</strong> <?php echo htmlspecialchars($payment['payment_id'] ?? $payment['public_ref']); ?></li>
|
||||
<li><strong>Amount:</strong> R <?php echo number_format($payment['amount'] ?? 0, 2); ?></li>
|
||||
<li><strong>Provider:</strong> <?php echo htmlspecialchars($payment['provider'] ?? ''); ?></li>
|
||||
<li><strong>Description:</strong> <?php echo htmlspecialchars($payment['description'] ?? ''); ?></li>
|
||||
<li><strong>Status:</strong> <?php echo htmlspecialchars($payment['status'] ?? ''); ?></li>
|
||||
</ul>
|
||||
|
||||
<?php if($booking_id == null) { ?>
|
||||
<a href="/membership_details.php" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Go to Membership Details">Go to Membership Details</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a href="/bookings.php" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Go to my Bookings">Go to my Bookings</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
<?php }
|
||||
}?>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="/assets/images/logos/weblogo.png" alt="Logo">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
@@ -6,9 +6,17 @@ require_once($rootPath . "/src/config/connection.php");
|
||||
require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$eft_id = strtoupper($user_id." SUBS ".date("Y")." ".getInitialSurname($user_id));
|
||||
$payment_id = generatePaymentRef('SUBS', null, $user_id);
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$description = 'Membership Fees '.date("Y")." ".getInitialSurname($user_id);
|
||||
// If current month is December, attribute the membership year to the next year
|
||||
$currentYear = intval(date('Y'));
|
||||
$month = intval(date('n'));
|
||||
if ($month === 12) {
|
||||
$membershipYear = $currentYear + 1;
|
||||
} else {
|
||||
$membershipYear = $currentYear;
|
||||
}
|
||||
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// CSRF Token Validation
|
||||
@@ -202,16 +210,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
->format('Y-m-d');
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id)
|
||||
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)");
|
||||
$stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id);
|
||||
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, renewal_period_end, payment_status, payment_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'AWAITING PAYMENT', ?)");
|
||||
$stmt->bind_param("idsssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $membership_end_date, $payment_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Commit the transaction
|
||||
$conn->commit();
|
||||
addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name);
|
||||
// Do not create legacy EFTs. Create a payments-ready membership fee and notify admin.
|
||||
// Optionally send an invoice referencing the internal payment id
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $payment_id, formatCurrency($payment_amount), $description);
|
||||
// sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name);
|
||||
// Audit: membership application submitted
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'MEMBERSHIP_APPLICATION_SUBMITTED', 'membership_application', null, ['payment_id' => $payment_id, 'amount' => $payment_amount ?? null]);
|
||||
}
|
||||
header("Location: indemnity");
|
||||
// Success message
|
||||
$response = [
|
||||
|
||||
@@ -79,6 +79,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt->bind_param('sissiiiidd', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $discount_amount);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Get booking id and audit
|
||||
$booking_id = $conn->insert_id;
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]);
|
||||
}
|
||||
// Redirect to success page or display success message
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
|
||||
} else {
|
||||
|
||||
@@ -93,10 +93,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$status = "AWAITING PAYMENT";
|
||||
$type = 'course';
|
||||
$payment_id = uniqid();
|
||||
$payment_id = generatePaymentRef('COURSE', $course_id, $user_id);
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
$num_vehicles = 1;
|
||||
$discountAmount = 0;
|
||||
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
|
||||
$eft_id = $payment_id;
|
||||
$notes = "";
|
||||
if ($pending_member){
|
||||
$notes = "Membership Payment pending at time of booking. Please confirm payment has been received.";
|
||||
@@ -117,6 +118,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($stmt->execute()) {
|
||||
$booking_id = $conn->insert_id;
|
||||
|
||||
// Audit booking creation
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||
}
|
||||
|
||||
if ($payment_amount < 1) {
|
||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
@@ -125,11 +131,31 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
// Create payments row
|
||||
$pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, booking_id, public_ref) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('sidssis', $payment_id, $user_id, $payment_amount, $status, $description, $booking_id, $publicRef);
|
||||
$pstmt->execute();
|
||||
$pstmt->close();
|
||||
}
|
||||
|
||||
// Create iKhokha payment link
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
|
||||
// Send invoice and admin notification (keep for records)
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
|
||||
// Redirect user to payment link if available
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
if ($paylink) {
|
||||
header('Location: ' . $paylink);
|
||||
exit();
|
||||
} else {
|
||||
// Fallback: redirect to legacy payment confirmation page
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle error if insert fails and echo the MySQL error
|
||||
|
||||
@@ -15,64 +15,92 @@ if (!$user_id) {
|
||||
echo "<script>alert('User is not logged in. Please log in to make a booking.'); window.location.href = 'login.php';</script>";
|
||||
exit();
|
||||
}
|
||||
$is_member = getUserMemberStatus($user_id);
|
||||
|
||||
$query = "SELECT payment_amount, payment_status, membership_end_date FROM membership_fees WHERE user_id = ?";
|
||||
// Fetch the membership fee record for this user
|
||||
$query = "SELECT fee_id, payment_amount, payment_status, membership_end_date FROM membership_fees WHERE user_id = ?";
|
||||
$stmt = $conn->prepare($query);
|
||||
if (!$stmt) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Server error preparing statement']);
|
||||
exit();
|
||||
}
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
// Check if trip exists
|
||||
// Check if membership fee exists
|
||||
if ($result->num_rows === 0) {
|
||||
$response = ['error' => 'Application Fee not found.'];
|
||||
$response = ['error' => 'Membership fee not found.'];
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch trip details
|
||||
// Fetch fee details
|
||||
$fee = $result->fetch_assoc();
|
||||
$fee_id = isset($fee['fee_id']) ? intval($fee['fee_id']) : null;
|
||||
$payment_status = $fee['payment_status'];
|
||||
$membership_end_date = $fee['membership_end_date'];
|
||||
$payment_amount = intval($fee['payment_amount']);
|
||||
$payment_amount = floatval($fee['payment_amount']);
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
|
||||
$description = "4WDCSA: Membership Fee " . getFullName($user_id) . " " . date("Y");
|
||||
$payment_id = uniqid();
|
||||
$eft_id = "SUBS 2025 ".getLastName($user_id);
|
||||
|
||||
// Update the membership_fees table to set payment_id
|
||||
$stmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE user_id = ?");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("ss", $payment_id, $user_id);
|
||||
|
||||
if (!$stmt->execute()) {
|
||||
throw new Exception("Failed to update membership_fees table.");
|
||||
// Persist the generated payment_id back to the membership_fees row (use fee_id to be precise)
|
||||
$updateStmt = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?");
|
||||
if ($updateStmt) {
|
||||
$updateStmt->bind_param("si", $payment_id, $fee_id);
|
||||
if (!$updateStmt->execute()) {
|
||||
throw new Exception("Failed to update membership_fees table: " . $updateStmt->error);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
$updateStmt->close();
|
||||
} else {
|
||||
throw new Exception("Failed to prepare statement for membership_fees table: " . $conn->error);
|
||||
}
|
||||
|
||||
// Get the current date
|
||||
$current_date = new DateTime();
|
||||
// If the amount is zero, treat as paid immediately
|
||||
if ($payment_amount < 1) {
|
||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
// Update membership_fees status to PAID
|
||||
$paidStmt = $conn->prepare("UPDATE membership_fees SET payment_status = 'PAID' WHERE fee_id = ?");
|
||||
if ($paidStmt) {
|
||||
$paidStmt->bind_param('i', $fee_id);
|
||||
$paidStmt->execute();
|
||||
$paidStmt->close();
|
||||
}
|
||||
echo "<script>alert('Membership payment recorded.'); window.location.href = 'memberships.php';</script>";
|
||||
exit();
|
||||
} else {
|
||||
echo "<script>alert('Failed to process membership payment.'); window.location.href = 'memberships.php';</script>";
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
// Create payments row
|
||||
$status = "AWAITING PAYMENT";
|
||||
$pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('sidsss', $payment_id, $user_id, $payment_amount, $status, $description, $publicRef);
|
||||
$pstmt->execute();
|
||||
$pstmt->close();
|
||||
}
|
||||
|
||||
// Convert $membership_end_date to a DateTime object
|
||||
$membership_end_date_obj = DateTime::createFromFormat('Y-m-d', $membership_end_date);
|
||||
// Create iKhokha payment link
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
|
||||
// Check if the current date is after membership_end_date
|
||||
// OR if the current date is before or on membership_end_date AND payment_status is "PENDING"
|
||||
if (
|
||||
$current_date > $membership_end_date_obj ||
|
||||
($current_date <= $membership_end_date_obj && $payment_status === "PENDING")
|
||||
) {
|
||||
// Send invoice and admin notification if desired
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), 'MEMBERSHIP-'.date('Y'), formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('Membership Payment Initiated - '.getFullName($user_id), getFullName($user_id).' initiated a membership payment.');
|
||||
|
||||
// Call the processMembershipPayment function
|
||||
// processMembershipPayment($payment_id, $payment_amount, $description);
|
||||
addMembershipEFT($eft_id, $user_id, $status, $amount, $description, $membershipfee_id);
|
||||
header("Location: payment_confirmation?booking_id=" . $booking_id);
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
// Redirect user to payment link if available
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
if ($paylink) {
|
||||
header('Location: ' . $paylink);
|
||||
exit();
|
||||
} else {
|
||||
// Fallback: redirect to a membership page with an encrypted token
|
||||
header("Location: membership_confirmation?token=" . encryptData($payment_id, $salt));
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,17 +56,122 @@ if (isset($_POST['signature'])) {
|
||||
$stmt->bind_param('si', $display_path, $user_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Audit: signature saved
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'SIGNATURE_SAVED', 'membership_application', null, ['path' => $display_path]);
|
||||
}
|
||||
// Check the payment status
|
||||
$paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID';
|
||||
|
||||
// Respond with the appropriate redirect URL based on the payment status
|
||||
// If not paid, create a payments row (if missing) and initiate iKhokha paylink
|
||||
$paylink = null;
|
||||
if ($paymentStatus !== 'PAID') {
|
||||
// Fetch the membership fee row to get amount and payment_id
|
||||
$mfStmt = $conn->prepare("SELECT fee_id, payment_amount, payment_id FROM membership_fees WHERE user_id = ? ORDER BY fee_id DESC LIMIT 1");
|
||||
if ($mfStmt) {
|
||||
$mfStmt->bind_param('i', $user_id);
|
||||
$mfStmt->execute();
|
||||
$mfRes = $mfStmt->get_result();
|
||||
$mf = $mfRes->fetch_assoc();
|
||||
$mfStmt->close();
|
||||
} else {
|
||||
$mf = null;
|
||||
}
|
||||
|
||||
if ($mf && isset($mf['payment_amount'])) {
|
||||
$amount = floatval($mf['payment_amount']);
|
||||
// Use existing payment_id or generate one
|
||||
$payment_id = $mf['payment_id'] ?? generatePaymentRef('SUBS', null, $user_id);;
|
||||
|
||||
if (empty($mf['payment_id'])) {
|
||||
// Persist generated payment_id back to membership_fees
|
||||
$u = $conn->prepare("UPDATE membership_fees SET payment_id = ? WHERE fee_id = ?");
|
||||
if ($u) {
|
||||
$u->bind_param('si', $payment_id, $mf['fee_id']);
|
||||
$u->execute();
|
||||
$u->close();
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure a payments row exists
|
||||
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
|
||||
if ($checkP) {
|
||||
$checkP->bind_param('s', $payment_id);
|
||||
$checkP->execute();
|
||||
$r = $checkP->get_result()->fetch_assoc();
|
||||
$exists = intval($r['cnt']) > 0;
|
||||
$checkP->close();
|
||||
} else {
|
||||
$exists = false;
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
// If current month is December, attribute the membership year to the next year
|
||||
$currentYear = intval(date('Y'));
|
||||
$month = intval(date('n'));
|
||||
if ($month === 12) {
|
||||
$membershipYear = $currentYear + 1;
|
||||
} else {
|
||||
$membershipYear = $currentYear;
|
||||
}
|
||||
$description = 'Membership Fees ' . $membershipYear . ' ' . getInitialSurname($user_id);
|
||||
$status = 'AWAITING PAYMENT';
|
||||
$ins = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, public_ref) VALUES (?, ?, ?, ?, ?, ?)");
|
||||
if ($ins) {
|
||||
$ins->bind_param('sidsss', $payment_id, $user_id, $amount, $status, $description, $publicRef);
|
||||
if ($ins->execute()) {
|
||||
// Audit: payment row created for membership
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'MEMBERSHIP_PAYMENT_CREATED', 'payments', null, ['payment_id' => $payment_id, 'amount' => $amount]);
|
||||
}
|
||||
}
|
||||
$ins->close();
|
||||
}
|
||||
}
|
||||
|
||||
// Create iKhokha paylink via helper (functions.php)
|
||||
try {
|
||||
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
|
||||
$resp = createIkhokhaPayment($payment_id, $amount, $desc ?? ('Membership Fee ' . date('Y')), $publicRef);
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
// After creating paylink, update paymentStatus to AWAITING PAYMENT
|
||||
$paymentStatus = $paylink ? 'AWAITING PAYMENT' : $paymentStatus;
|
||||
$token = encryptData($payment_id, $_ENV['SALT']);
|
||||
// Audit: paylink created (or attempted)
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'IKHOKHA_PAYLINK_CREATED', 'payments', null, ['payment_id' => $payment_id, 'paylink' => $paylink]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Log but do not fail signature save
|
||||
error_log('iKhokha create error: ' . $e->getMessage());
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'IKHOKHA_PAYLINK_FAILED', 'payments', null, ['payment_id' => $payment_id, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Respond with the appropriate redirect URL and paylink (if created)
|
||||
ob_end_clean();
|
||||
echo json_encode([
|
||||
$response = [
|
||||
'status' => 'success',
|
||||
'message' => 'Signature saved successfully!',
|
||||
'paymentStatus' => $paymentStatus // Send payment status
|
||||
]);
|
||||
'paymentStatus' => $paymentStatus,
|
||||
'token' => $token ?? null
|
||||
];
|
||||
if (!empty($paylink)) {
|
||||
$response['paylinkUrl'] = $paylink;
|
||||
}
|
||||
if (!empty($payment_id)) {
|
||||
$response['payment_id'] = $payment_id;
|
||||
}
|
||||
echo json_encode($response);
|
||||
} else {
|
||||
// Audit: signature save failed
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'SIGNATURE_SAVE_FAILED', 'membership_application', null, ['user_id' => $user_id]);
|
||||
}
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
|
||||
}
|
||||
@@ -78,6 +183,10 @@ if (isset($_POST['signature'])) {
|
||||
echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']);
|
||||
}
|
||||
} else {
|
||||
// Audit: no signature provided in request
|
||||
if (function_exists('auditLog') && isset($_SESSION['user_id'])) {
|
||||
auditLog($_SESSION['user_id'], 'SIGNATURE_NOT_PROVIDED', 'membership_application', null, ['endpoint' => 'process_signature.php']);
|
||||
}
|
||||
ob_end_clean();
|
||||
echo json_encode(['status' => 'error', 'message' => 'Signature not provided']);
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$member_discount = $cost_nonmembers - $cost_members;
|
||||
$member_discount_pensioner = $cost_pensioner - $cost_pensioner_member;
|
||||
$booking_fee = $trip['booking_fee'];
|
||||
// Radio option (boolean/int) — ensure defined from POST
|
||||
$radio = isset($_POST['radio']) ? intval($_POST['radio']) : 0;
|
||||
$radioCost = $radio ? 50 : 0;
|
||||
$start_date = $trip['start_date']; // Start date of the trip
|
||||
$end_date = $trip['end_date']; // End date of the trip
|
||||
@@ -103,9 +105,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$status = "AWAITING PAYMENT";
|
||||
$description = $trip_name;
|
||||
$type = 'trip';
|
||||
$payment_id = uniqid();
|
||||
$payment_id = generatePaymentRef('TRIP', $trip_id, $user_id);
|
||||
$publicRef = bin2hex(random_bytes(16));
|
||||
// $eft_id = strtoupper(base_convert(time(), 10, 36)); // Convert timestamp to base36
|
||||
$eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
|
||||
// $eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
|
||||
|
||||
|
||||
// Insert booking into the database
|
||||
@@ -123,6 +126,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Get the generated booking_id
|
||||
$booking_id = $conn->insert_id;
|
||||
|
||||
// Audit booking creation
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||
}
|
||||
|
||||
if ($payment_amount < 1) {
|
||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||
@@ -131,11 +139,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
echo "Error processing booking: $error_message";
|
||||
}
|
||||
} else {
|
||||
addEFT($eft_id, $booking_id, $user_id, $status, $payment_amount, $description);
|
||||
sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
// Create payments row
|
||||
$pstmt = $conn->prepare("INSERT INTO payments (payment_id, user_id, amount, status, description, booking_id, public_ref) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
if ($pstmt) {
|
||||
$pstmt->bind_param('sidssis', $payment_id, $user_id, $payment_amount, $status, $description, $booking_id, $publicRef);
|
||||
$pstmt->execute();
|
||||
$pstmt->close();
|
||||
}
|
||||
|
||||
// Create iKhokha payment link
|
||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||
|
||||
// Send invoice and admin notification
|
||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
||||
sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||
|
||||
// Redirect to payment link if available
|
||||
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
|
||||
if ($paylink) {
|
||||
header('Location: ' . $paylink);
|
||||
exit();
|
||||
} else {
|
||||
header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
|
||||
exit(); // Ensure no further code is executed after the redirect
|
||||
exit();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle error if insert fails and echo the MySQL error
|
||||
|
||||
41
test.php
Normal file
41
test.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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>";
|
||||
60
test_payment.php
Normal file
60
test_payment.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
$endpoint = "https://api.ikhokha.com/public-api/v1/api/payment";
|
||||
$appID = "IKFLESZTKFM4HWWS76131L8HK9BYF96P";
|
||||
$appSecret = "gfoQTvXRXuzq6ArPHUS2CBFxtHtH1bxM";
|
||||
$requestBody = [
|
||||
"entityID" => "4",
|
||||
"externalEntityID" => "4",
|
||||
"amount" => 1000,
|
||||
"currency" => "ZAR",
|
||||
"requesterUrl" => "https://beta.4wdcsa.co.za/requester",
|
||||
"description" => "Test Description 1",
|
||||
"paymentReference" => "4",
|
||||
"mode" => "sandbox",
|
||||
"externalTransactionID" => "5",
|
||||
"urls" => [
|
||||
"callbackUrl" => "https://beta.4wdcsa.co.za/callback",
|
||||
"successPageUrl" => "https://beta.4wdcsa.co.za/success",
|
||||
"failurePageUrl" => "https://beta.4wdcsa.co.za/failure",
|
||||
"cancelUrl" => "https://beta.4wdcsa.co.za/cancel"
|
||||
]
|
||||
];
|
||||
function escapeString($str) {
|
||||
$escaped = preg_replace(['/[\\"\'\"]/u', '/\x00/'], ['\\\\$0', '\\0'], (string)$str);
|
||||
$cleaned = str_replace('\/', '/', $escaped);
|
||||
return $cleaned;
|
||||
}
|
||||
function createPayloadToSign($urlPath, $body) {
|
||||
$parsedUrl = parse_url($urlPath);
|
||||
$basePath = $parsedUrl['path'];
|
||||
if (!$basePath) {
|
||||
throw new Exception("No path present in the URL");
|
||||
}
|
||||
$payload = $basePath . $body;
|
||||
$escapedPayloadString = escapeString($payload);
|
||||
return $escapedPayloadString;
|
||||
}
|
||||
function generateSignature($payloadToSign, $secret) {
|
||||
return hash_hmac('sha256', $payloadToSign, $secret);
|
||||
}
|
||||
$stringifiedBody = json_encode($requestBody);
|
||||
$payloadToSign = createPayloadToSign($endpoint, $stringifiedBody);
|
||||
$ikSign = generateSignature($payloadToSign, $appSecret);
|
||||
// Initialize cURL session
|
||||
$ch = curl_init($endpoint);
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
|
||||
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
|
||||
$responseData = json_decode($response, true);
|
||||
echo print_r($responseData, true);
|
||||
?>
|
||||
BIN
uploads/signatures/signature_163.png
Normal file
BIN
uploads/signatures/signature_163.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
Reference in New Issue
Block a user