From 477c2f2e04534ad74baa8bac372fe9b99aeebd1e Mon Sep 17 00:00:00 2001
From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com>
Date: Mon, 15 Dec 2025 00:36:34 +0200
Subject: [PATCH 1/5] iKhokha integration complete
---
.htaccess | 5 +
.htaccess copy | 215 ++++++++++++++
classes/iKhokhaClient.php | 120 ++++++++
header.php | 2 +-
progress.log | 48 +++
.../001_add_payment_columns.sql | 61 ++++
.../002_migrate_efts_to_payments.sql | 14 +
scripts/ikhokha_migrations/README.md | 21 ++
src/api/ikhokha_webhook.php | 274 ++++++++++++++++++
src/api/test_log.php | 10 +
src/config/functions.php | 184 ++++++++++++
src/pages/memberships/membership_details.php | 6 +-
src/pages/memberships/membership_payment.php | 46 ++-
src/pages/memberships/renew_membership.php | 78 ++++-
src/pages/other/indemnity.php | 22 +-
src/pages/payment/cancel.php | 73 +++++
src/pages/payment/failure.php | 75 +++++
src/pages/payment/success.php | 84 ++++++
src/processors/process_application.php | 23 +-
src/processors/process_course_booking.php | 29 +-
src/processors/process_membership_payment.php | 92 ++++--
src/processors/process_signature.php | 91 +++++-
src/processors/process_trip_booking.php | 32 +-
test.php | 41 +++
test_payment.php | 60 ++++
uploads/signatures/signature_163.png | Bin 0 -> 9888 bytes
26 files changed, 1625 insertions(+), 81 deletions(-)
create mode 100644 .htaccess copy
create mode 100644 classes/iKhokhaClient.php
create mode 100644 progress.log
create mode 100644 scripts/ikhokha_migrations/001_add_payment_columns.sql
create mode 100644 scripts/ikhokha_migrations/002_migrate_efts_to_payments.sql
create mode 100644 scripts/ikhokha_migrations/README.md
create mode 100644 src/api/ikhokha_webhook.php
create mode 100644 src/api/test_log.php
create mode 100644 src/pages/payment/cancel.php
create mode 100644 src/pages/payment/failure.php
create mode 100644 src/pages/payment/success.php
create mode 100644 test.php
create mode 100644 test_payment.php
create mode 100644 uploads/signatures/signature_163.png
diff --git a/.htaccess b/.htaccess
index 68702625..89e12b95 100644
--- a/.htaccess
+++ b/.htaccess
@@ -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]
diff --git a/.htaccess copy b/.htaccess copy
new file mode 100644
index 00000000..46c835d3
--- /dev/null
+++ b/.htaccess copy
@@ -0,0 +1,215 @@
+# URL Rewrite Rules - Maps old URLs to new directory structure during migration
+
+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]
+
+
+
+php_flag display_errors On
+# php_value error_reporting -1
+RedirectMatch 403 ^/\.well-known
+Options -Indexes
+
+
+ Require all denied
+
+
+ErrorDocument 404 /404.php
+
+
+ Require all granted
+ Require not ip 4.222.252.98
+ Require not ip 4.222.252.97
+
+
+
+ Order allow,deny
+ Deny from all
+
+
+
+# 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
+
diff --git a/classes/iKhokhaClient.php b/classes/iKhokhaClient.php
new file mode 100644
index 00000000..38beb3eb
--- /dev/null
+++ b/classes/iKhokhaClient.php
@@ -0,0 +1,120 @@
+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');
+ }
+}
diff --git a/header.php b/header.php
index 6d7ded11..7d82aa03 100644
--- a/header.php
+++ b/header.php
@@ -327,7 +327,7 @@ if ($headerStyle === 'light') {
echo "
You will be redirected to iKhokha's Secure Payment Gateway.
+
+ Please upload your proof of payment below.
+
diff --git a/src/pages/memberships/renew_membership.php b/src/pages/memberships/renew_membership.php
index f6d85f38..8b7ecab2 100644
--- a/src/pages/memberships/renew_membership.php
+++ b/src/pages/memberships/renew_membership.php
@@ -1,26 +1,78 @@
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);
+// Hardcode membership start date to 2026-03-01 per request
+$membership_start_date = '2026-03-01';
+
+// Set membership_end_date to the last day of February in the following year
+$nextYear = intval(date('Y')) + 1;
+$dt = new DateTime($nextYear . '-02-28');
+$membership_end_date = $dt->format('Y-m-t');
+
+$stmt = $conn->prepare("UPDATE membership_fees SET payment_amount = ?, payment_date = ?, membership_start_date = ?, membership_end_date = ?, payment_status = 'AWAITING PAYMENT', payment_id = ? WHERE user_id = ?");
+$stmt->bind_param("dssssi", $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $payment_id, $user_id);
if ($stmt->execute()) {
// Commit the transaction
$conn->commit();
- addSubsEFT($eft_id, $user_id, $status, $payment_amount, $description);
+
+ $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;
+ if ($paylink) {
+ header('Location: membership_payment?payment_id=' . $payment_id);
+ 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 = [
diff --git a/src/pages/other/indemnity.php b/src/pages/other/indemnity.php
index c6f1c43e..3fc42185 100644
--- a/src/pages/other/indemnity.php
+++ b/src/pages/other/indemnity.php
@@ -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.php?payment_id=' + encodeURIComponent(response.payment_id);
+ 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.payment_id);
+ // }, 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('
' + response.message + '
');
diff --git a/src/pages/payment/cancel.php b/src/pages/payment/cancel.php
new file mode 100644
index 00000000..0aab44f6
--- /dev/null
+++ b/src/pages/payment/cancel.php
@@ -0,0 +1,73 @@
+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');
+?>
+
+
+
+
+
+ Payment Cancelled
+
Your payment was cancelled or you returned without completing it.
+
+
+
+
+
+
Your payment appears to have been cancelled. If this was a mistake you can try again below.
+
+ - Reference:
+ - Amount: R
+ - Description:
+
+
+
+
+ Retry Payment
+
+
+
+
+
Contact info@4wdcsa.co.za if you need assistance.
+
+
+
+
+
+

+
+
+
+
+
+
+
+
diff --git a/src/pages/payment/failure.php b/src/pages/payment/failure.php
new file mode 100644
index 00000000..1491b4ec
--- /dev/null
+++ b/src/pages/payment/failure.php
@@ -0,0 +1,75 @@
+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');
+?>
+
+
+
+
+
+ Payment Failed
+
Unfortunately your payment could not be completed.
+
+
+
+
+
+
We were unable to process your payment. You can try again or contact support for assistance.
+
+ - Reference:
+ - Amount: R
+ - Provider:
+ - Description:
+ - Status:
+
+
+
+
+ Try Again
+
+
+
+
+
Or contact info@4wdcsa.co.za for help.
+
+
+
+
+
+

+
+
+
+
+
+
+
+
diff --git a/src/pages/payment/success.php b/src/pages/payment/success.php
new file mode 100644
index 00000000..11b36dad
--- /dev/null
+++ b/src/pages/payment/success.php
@@ -0,0 +1,84 @@
+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');
+?>
+
+
+
+
+
+ Payment Successful
+
Thank you — your payment was received.
+
+
+
MEMBERSHIP STATUS: = getUserMemberStatus($user_id) ? 'ACTIVE' : 'INACTIVE'; ?>
+
+
+
+
+
+
Your payment has been processed successfully. Below are the details we received:
+
+ - Reference:
+ - Amount: R
+ - Provider:
+ - Description:
+ - Status:
+
+
+
+
+ Go to Membership Details
+
+
+
+
+ Go to my Bookings
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
diff --git a/src/processors/process_application.php b/src/processors/process_application.php
index b3347e0b..d4045c4b 100644
--- a/src/processors/process_application.php
+++ b/src/processors/process_application.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 = uniqid();
$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
@@ -203,15 +211,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
$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);
+ VALUES (?, ?, ?, ?, ?, 'AWAITING PAYMENT', ?)");
+ $stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_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);
header("Location: indemnity");
// Success message
$response = [
diff --git a/src/processors/process_course_booking.php b/src/processors/process_course_booking.php
index 098abab0..7d2af6f0 100644
--- a/src/processors/process_course_booking.php
+++ b/src/processors/process_course_booking.php
@@ -94,6 +94,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$status = "AWAITING PAYMENT";
$type = 'course';
$payment_id = uniqid();
+ $publicRef = bin2hex(random_bytes(16));
$num_vehicles = 1;
$discountAmount = 0;
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
@@ -125,11 +126,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);
- header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
- 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 legacy payment confirmation page
+ header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
+ exit();
+ }
}
} else {
// Handle error if insert fails and echo the MySQL error
diff --git a/src/processors/process_membership_payment.php b/src/processors/process_membership_payment.php
index cede548e..8d81ddfa 100644
--- a/src/processors/process_membership_payment.php
+++ b/src/processors/process_membership_payment.php
@@ -15,64 +15,92 @@ if (!$user_id) {
echo "";
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 "";
+ exit();
+ } else {
+ echo "";
+ 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();
+ }
}
diff --git a/src/processors/process_signature.php b/src/processors/process_signature.php
index c504b12c..7f1eaef9 100644
--- a/src/processors/process_signature.php
+++ b/src/processors/process_signature.php
@@ -59,13 +59,96 @@ if (isset($_POST['signature'])) {
// 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'] ?? uniqid('mem_', true);
+
+ 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);
+ $ins->execute();
+ $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;
+ } catch (Exception $e) {
+ // Log but do not fail signature save
+ error_log('iKhokha create 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
+ ];
+ if (!empty($paylink)) {
+ $response['paylinkUrl'] = $paylink;
+ }
+ if (!empty($payment_id)) {
+ $response['payment_id'] = $payment_id;
+ }
+ echo json_encode($response);
} else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
diff --git a/src/processors/process_trip_booking.php b/src/processors/process_trip_booking.php
index fe7916c8..5391d7e5 100644
--- a/src/processors/process_trip_booking.php
+++ b/src/processors/process_trip_booking.php
@@ -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
@@ -104,6 +106,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$description = $trip_name;
$type = 'trip';
$payment_id = uniqid();
+ $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));
@@ -131,11 +134,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);
- sendAdminNotification('New Trip Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
- header("Location: payment_confirmation?token=".encryptData($booking_id, $salt));
- exit(); // Ensure no further code is executed after the redirect
+ // 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();
+ }
}
} else {
// Handle error if insert fails and echo the MySQL error
diff --git a/test.php b/test.php
new file mode 100644
index 00000000..011cbbf8
--- /dev/null
+++ b/test.php
@@ -0,0 +1,41 @@
+IK-SIGN FROM WEBHOOK:
";
+echo "bb1702d488a40091ebd5414bc6f524e203e2c5e36b24a1b86e243dad440bb557
";
+
+$payloadToSign = $path . $raw;
+
+// Generate signature using hash_hmac directly on the constructed string
+$expected = hash_hmac('sha256', $payloadToSign, $secret);
+
+// --- Output debug info (UPDATED) ---
+echo "
DEBUG INFO";
+echo "Callback URL: $callbackUrl
";
+
+echo "
Payload to Sign (Un-escaped):";
+echo htmlspecialchars($payloadToSign) . "
";
+
+echo "
EXPECTED SIGNATURE:";
+echo $expected . "
";
\ No newline at end of file
diff --git a/test_payment.php b/test_payment.php
new file mode 100644
index 00000000..11759867
--- /dev/null
+++ b/test_payment.php
@@ -0,0 +1,60 @@
+ "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);
+?>
diff --git a/uploads/signatures/signature_163.png b/uploads/signatures/signature_163.png
new file mode 100644
index 0000000000000000000000000000000000000000..27bcf4f9f6ebea8ab223f75936aab0f9cb7f7bc0
GIT binary patch
literal 9888
zcmZ{KcRbba`#-0HlQ@pO4h}*>DkHM@3dxL&A}ZOka_E>D8CfYK87JAw-dhye9FEN~
zj*&f&-|O`LeE
*~`G1MR>1OyL9anN`7#m624128I`
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
Date: Mon, 15 Dec 2025 01:24:56 +0200
Subject: [PATCH 2/5] iKhokha integration completerer...
---
src/config/functions.php | 44 +++++++++++++++++++-
src/pages/memberships/membership_details.php | 20 ++++++---
src/pages/memberships/renew_membership.php | 21 ++++++----
3 files changed, 71 insertions(+), 14 deletions(-)
diff --git a/src/config/functions.php b/src/config/functions.php
index 12cf2380..e51f42bb 100644
--- a/src/config/functions.php
+++ b/src/config/functions.php
@@ -517,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);
@@ -540,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();
@@ -548,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");
@@ -3409,3 +3416,38 @@ function getPaymentLinkByPaymentId($payment_id)
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;
+}
+
diff --git a/src/pages/memberships/membership_details.php b/src/pages/memberships/membership_details.php
index a42ef44c..8a5c39d2 100644
--- a/src/pages/memberships/membership_details.php
+++ b/src/pages/memberships/membership_details.php
@@ -188,8 +188,8 @@ if (empty($application['id_number'])) {
|
|
-
- PAY NOW |
+
+ PENDING RENEWAL |
|
@@ -206,16 +206,26 @@ if (empty($application['id_number'])) {
strtotime($membership_end_date)) {
- echo '
+ 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 '