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 00000000..27bcf4f9
Binary files /dev/null and b/uploads/signatures/signature_163.png differ