iKhokha integration complete

This commit is contained in:
twotalesanimation
2025-12-15 00:36:34 +02:00
parent a66382661d
commit 477c2f2e04
26 changed files with 1625 additions and 81 deletions

274
src/api/ikhokha_webhook.php Normal file
View File

@@ -0,0 +1,274 @@
<?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);
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]));
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();
}
/**
* ==========================================================
* 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();
}
// 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));
}
} 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 - New Membership Application - '.getFullName($user_id) , 'A new member has signed up, '.getFullName($user_id));
}
}
// 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
View 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.";

View File

@@ -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)
{
@@ -725,6 +758,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();
@@ -1984,6 +2131,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 +3374,38 @@ 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;
}

View File

@@ -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") { ?>
<td><a href='<?= $payment_link; ?>' class='theme-btn style-two style-three' style='padding: 0px 14px;'><span data-hover='PAY NOW'>PAY NOW</span></a></td>
<?php } else { ?>
<td><?php echo htmlspecialchars($membership['payment_status']); ?></td>
<?php } ?>

View File

@@ -67,7 +67,25 @@ $stmt->bind_result($user_email);
$stmt->fetch();
$stmt->close();
$conn->close();
// If request includes payment_id, fetch provider paylink from payments table
$payment_id = $_GET['payment_id'] ?? null;
$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']];
@@ -83,13 +101,25 @@ $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>
<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 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>
<p style="margin-top:10px;">You will be redirected to iKhokha's Secure Payment Gateway.</p>
<?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">

View File

@@ -1,26 +1,78 @@
<?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);
$payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed
$payment_id = uniqid();
$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');
$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);
// 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 = [

View File

@@ -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('<div class="alert alert-danger">' + response.message + '</div>');

View 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'); ?>

View 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'); ?>

View 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'); ?>

View File

@@ -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 = [

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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']);

View File

@@ -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