3 Commits

Author SHA1 Message Date
twotalesanimation
0e6ecd127f post auditlog implementation for bookings and payments 2025-12-15 10:52:09 +02:00
twotalesanimation
702e04e9bf pre auditlog implementations 2025-12-15 10:44:56 +02:00
twotalesanimation
d2c99e86b4 mostly complete payment system 2025-12-15 10:18:25 +02:00
17 changed files with 365 additions and 124 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -19,6 +19,27 @@ if (!isset($_SESSION['updates_modal_shown'])) {
$showUpdatesModal = false;
}
// Show renew membership modal for logged-in users and where membership_fees payment_status is not PENDING RENEWAL. only show once per session
$showRenewModal = isset($_SESSION['user_id']) ? true : false;
if ($showRenewModal) {
if (!isset($_SESSION['renew_modal_shown'])) {
$_SESSION['renew_modal_shown'] = true;
} else {
$showRenewModal = false;
}
$user_id = $_SESSION['user_id'];
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$stmt->bind_result($payment_status);
$stmt->fetch();
$stmt->close();
if ($payment_status === 'PENDING RENEWAL') {
$showRenewModal = false;
}
}
if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
$userId = $_SESSION['user_id'];
$stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1");
@@ -657,34 +678,72 @@ if (countUpcomingTrips() > 0) { ?>
const modal = document.getElementById('updatesModal');
const closeBtn = document.querySelector('.updates-modal-close');
const showModal = <?php echo $showUpdatesModal ? 'true' : 'false'; ?>;
if (showModal) {
// Show modal after a short delay for better UX
const showRenewModal = <?php echo $showRenewModal ? 'true' : 'false'; ?>;
if (showModal && modal) {
// Show updates modal after a short delay for better UX
setTimeout(function() {
modal.style.display = 'flex';
}, 500);
}
// Close modal when X is clicked
closeBtn.addEventListener('click', function() {
modal.style.display = 'none';
});
// Close modal when clicking outside the modal content
modal.addEventListener('click', function(event) {
if (event.target === modal) {
modal.style.display = 'none';
// Close updates modal when X is clicked
if (closeBtn) {
closeBtn.addEventListener('click', function() {
if (modal) modal.style.display = 'none';
});
}
// Close updates modal when clicking outside the modal content
if (modal) {
modal.addEventListener('click', function(event) {
if (event.target === modal) {
modal.style.display = 'none';
}
});
}
// Show renew membership Bootstrap modal for logged-in users
try {
const renewModalEl = document.getElementById('renewModal');
if (showRenewModal && renewModalEl && typeof bootstrap !== 'undefined') {
setTimeout(function() {
const renewModal = new bootstrap.Modal(renewModalEl);
renewModal.show();
}, 700);
}
});
} catch (e) {
console.warn('Renew modal show failed', e);
}
});
</script>
<!-- Updates Modal -->
<!-- Renew Membership Modal (shown to logged-in users) -->
<div class="modal fade" id="renewModal" tabindex="-1" aria-labelledby="renewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<!-- <div class="modal-header bg-secondary text-white">
<h5 class="modal-title" id="renewModalLabel">Membership Renewal Reminder</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> -->
<div class="modal-body">
Your membership will be expiring soon. Click below to renew now.
<a style="width:100%; display:block;" href="renew_membership" class="theme-btn style-two style-three mt-3">Renew Now</a>
</div>
<div class="modal-footer">
<button type="button" style="width:100%; display:block;" class="theme-btn" data-bs-dismiss="modal">Remind Me Later</button>
</div>
</div>
</div>
</div>
<!-- Updates Modal -->
<div id="updatesModal" class="updates-modal">
<div class="updates-modal-content">
<span class="updates-modal-close">&times;</span>
<div class="updates-modal-header">
<h2>What's New</h2>
<h2>What's New on 4WDCSA.co.za</h2>
</div>
<div class="updates-modal-body">
<div class="update-item">

View File

@@ -78,6 +78,10 @@ if (!$bypass) {
progress_log('iKhokha webhook: signature mismatch');
progress_log('EXPECTED SIGN: ' . $expected);
progress_log('RECEIVED SIGN: ' . $ikSign);
// Audit signature mismatch
if (function_exists('auditLog')) {
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, ['expected' => $expected, 'received' => $ikSign]);
}
exit('Invalid signature');
}
@@ -183,6 +187,9 @@ if (!$localPaymentId) {
http_response_code(404);
progress_log('iKhokha webhook: payment not found');
progress_log(json_encode([$externalTransactionID, $providerPaymentId]));
if (function_exists('auditLog')) {
auditLog(null, 'IKHOKHA_PAYMENT_NOT_FOUND', 'payment', null, ['externalTransactionID' => $externalTransactionID, 'providerPaymentId' => $providerPaymentId]);
}
exit('Payment not found');
}
@@ -209,6 +216,9 @@ if ($update) {
);
$update->execute();
$update->close();
if (function_exists('auditLog')) {
auditLog($user_id, 'PAYMENT_PROVIDER_RESPONSE_SAVED', 'payment', null, ['payment_id' => $localPaymentId, 'provider_payment_id' => $providerPaymentId, 'provider_status' => $providerStatus]);
}
}
/**
@@ -228,6 +238,9 @@ if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
$setPaid->bind_param('s', $localPaymentId);
$setPaid->execute();
$setPaid->close();
if (function_exists('auditLog')) {
auditLog($user_id, 'PAYMENT_MARKED_PAID', 'payment', null, ['payment_id' => $localPaymentId]);
}
}
// Booking or membership update
@@ -240,6 +253,9 @@ if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
$upd->execute();
$upd->close();
sendAdminNotification('4WDCSA.co.za - New Booking - '.getFullName($user_id) , 'We have received a payment for a new booking for '.$description.' from '.getFullName($user_id));
if (function_exists('auditLog')) {
auditLog($user_id, 'BOOKING_PAYMENT_MARKED_PAID', 'bookings', $booking_id, ['payment_id' => $localPaymentId]);
}
}
} else {
$upd = $conn->prepare(
@@ -251,7 +267,10 @@ if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
$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));
sendAdminNotification('4WDCSA.co.za - Membership Payment Received - '.getFullName($user_id) , 'A Membership Payment has been received from '.getFullName($user_id));
if (function_exists('auditLog')) {
auditLog($user_id, 'MEMBERSHIP_PAYMENT_MARKED_PAID', 'membership_fees', null, ['payment_id' => $localPaymentId]);
}
}
}

View File

@@ -1456,7 +1456,7 @@ function getInitialSurname($user_id)
if ($stmt->fetch()) {
$initial = strtoupper(substr($first_name, 0, 1));
return $initial . ". " . $last_name;
return $initial . "." . $last_name;
} else {
return null;
}
@@ -1467,6 +1467,89 @@ function getInitialSurname($user_id)
}
}
function generatePaymentRef(string $type, ?int $course_trip_id, int $user_id): string
{
$conn = openDatabaseConnection();
// 1. Normalize type
$type = strtoupper($type);
// 2. Build prefix
switch ($type) {
case 'SUBS':
$year = (int)date('Y');
$month = (int)date('n');
// If December, subscriptions are for next year
if ($month === 12) {
$year++;
}
$prefix = "SUBS_" . $year;
break;
case 'COURSE':
if (!$course_trip_id) {
throw new Exception("course_trip_id is required for COURSE payments");
}
$stmt = $conn->prepare(
"SELECT code FROM courses WHERE course_id = ?"
);
$stmt->bind_param("i", $course_trip_id);
$stmt->execute();
$stmt->bind_result($code);
if (!$stmt->fetch()) {
throw new Exception("Invalid course_id: {$course_trip_id}");
}
$stmt->close();
$prefix = "COURSE_" . strtoupper($code);
break;
case 'TRIP':
if (!$course_trip_id) {
throw new Exception("course_trip_id is required for TRIP payments");
}
$stmt = $conn->prepare(
"SELECT trip_code FROM trips WHERE trip_id = ?"
);
$stmt->bind_param("i", $course_trip_id);
$stmt->execute();
$stmt->bind_result($trip_code);
if (!$stmt->fetch()) {
throw new Exception("Invalid trip_id: {$course_trip_id}");
}
$stmt->close();
$prefix = "TRIP_" . strtoupper($trip_code);
break;
default:
throw new Exception("Unknown payment type: {$type}");
}
// 3. Get user initials + surname
$namePart = strtoupper(getInitialSurname($user_id));
if (!$namePart) {
throw new Exception("User not found for user_id: {$user_id}");
}
// 4. Add short entropy (trimmed for aesthetics)
$entropy = substr(shortEntropy(), -3);
return "{$prefix}_{$namePart}_{$entropy}";
}
function shortEntropy(): string {
return strtoupper(base_convert((string)(microtime(true) * 1000), 10, 36));
}
function getLastName($user_id)
{
$conn = openDatabaseConnection();

View File

@@ -114,6 +114,7 @@ $user_id = $_SESSION['user_id'];
// Loop through each row
while ($row = $result->fetch_assoc()) {
$booking_id = $row['booking_id'];
$payment_id = $row['payment_id'];
$booking_type = $row['booking_type'];
$from_date = $row['from_date'];
$to_date = $row['to_date'];
@@ -267,8 +268,8 @@ $user_id = $_SESSION['user_id'];
<div class="destination-footer">
<span class="price"><span>Booking Total: R ' . number_format($amount, 2) . '</span></span>';
if ($status == "AWAITING PAYMENT") {
echo '<a href="' . url('payment_confirmation') . '?token=' . encryptData($booking_id, $salt) . '" class="theme-btn style-two style-three">
<span data-hover="PAYMENT INFO">' . $status . '</span>
echo '<a href="' . getPaymentLinkByPaymentId($payment_id) . '" class="theme-btn style-two style-three">
<span data-hover="PAY NOW">' . $status . '</span>
</a>';
} else {
echo '<a href="" class="theme-btn style-two style-three">

View File

@@ -177,7 +177,7 @@ $page_id = 'driver_training';
</div>
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php
$button_text = "Book Now";
$button_text = "PROCEED TO PAYMENT";
$button_disabled = "";
if (!$result || $result->num_rows == 0) {
$button_text = "No booking dates available";
@@ -189,8 +189,9 @@ $page_id = 'driver_training';
<i class="fal fa-arrow-right"></i>
</button>
<div class="text-center">
<a href="contact">Need some help?</a>
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div>
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form>
</div>

View File

@@ -205,30 +205,30 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</nav>
</div>
<!-- Draft Notice for Admin -->
<?php if ($is_admin && isset($row['published']) && $row['published'] == 0): ?>
<div class="alert alert-warning mt-3" role="alert">
<strong><i class="fas fa-exclamation-triangle"></i> Draft Trip</strong><br>
This trip is currently in draft status and is not visible to regular users. Only admins and superadmins can preview it.
</div>
<?php endif; ?>
<!-- Publish/Unpublish Button -->
<?php
$user_role = getUserRole();
if (in_array($user_role, ['admin', 'superadmin'])):
// Use published status from the main query
$is_published = $row['published'] ?? 0;
?>
<div class="admin-actions mt-20">
<button type="button" class="theme-btn" style="width: 100%; id="publishBtn" onclick="toggleTripPublished(<?php echo $trip_id; ?>)">
<?php if ($is_published): ?>
<i class="fas fa-eye-slash"></i> Unpublish Trip
<?php else: ?>
<i class="fas fa-eye"></i> Publish Trip
<?php endif; ?>
</button>
</div>
<?php endif; ?>
<?php if ($is_admin && isset($row['published']) && $row['published'] == 0): ?>
<div class="alert alert-warning mt-3" role="alert">
<strong><i class="fas fa-exclamation-triangle"></i> Draft Trip</strong><br>
This trip is currently in draft status and is not visible to regular users. Only admins and superadmins can preview it.
</div>
<?php endif; ?>
<!-- Publish/Unpublish Button -->
<?php
$user_role = getUserRole();
if (in_array($user_role, ['admin', 'superadmin'])):
// Use published status from the main query
$is_published = $row['published'] ?? 0;
?>
<div class="admin-actions mt-20">
<button type="button" class="theme-btn" style="width: 100%; id=" publishBtn" onclick="toggleTripPublished(<?php echo $trip_id; ?>)">
<?php if ($is_published): ?>
<i class="fas fa-eye-slash"></i> Unpublish Trip
<?php else: ?>
<i class="fas fa-eye"></i> Publish Trip
<?php endif; ?>
</button>
</div>
<?php endif; ?>
</div>
</section>
@@ -296,8 +296,8 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</div>
<span class="subtitle mb-15"><?php echo $badge_text; ?></span>
</div>
<!-- <div class="col-xl-4 col-lg-5 text-lg-end" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
<div class="tour-header-social mb-10">
@@ -558,33 +558,33 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</li>
</ul>
<div style="margin: 20px 0;">
<div id="indemnityBox" style="border: 1px solid #ccc; padding: 10px; height: 150px; overflow-y: scroll; background: #f9f9f9; font-size: 12px;">
<p><strong>INDEMNITY AND WAIVER</strong></p>
<p>1. I agree to abide by the Code of Conduct as listed below, as well as any reasonable instructions given by any Member of the Committee of the Club, or any person appointed by the Club to organise or control any event (Club Officer).</p>
<p>2. I acknowledge that driving the off-road track is inherently dangerous, and that I am fully aware of the dangers thereof. I warrant that I will make all members of my party aware of such dangers prior to driving the track.</p>
<p>3. While I, or any member of my party, enjoy the facilities at Base 4 including overnight camping, picnicking, driving the track, using the swimming pool facility or activity or any other activity while at Base 4, I agree that under no circumstances shall the Club be liable for any loss or damage of any kind whatsoever (including consequential loss) which I or any of my party may suffer, regardless of how such loss or damage may have been caused or sustained, and whether or not as a result of the negligence or breach of contract (whether fundamental or otherwise) or other wrongful act of the Club, or any Club Officer, or any of the Clubs agents or contractors, and I hereby indemnify and hold harmless the Club and any Club Officer against all such loss or damage.</p>
<p>4. The expression, member of my party, means all persons who accompany me or attending any event at my specific invitation, request or suggestion, and includes without limitation, members of family, guests and invitees.</p>
<p>5. I understand that I am responsible for ensuring my vehicle and equipment and that all members of my party have adequate health and medical insurance to cover any and all likely occurrences.</p>
<p>6. This indemnity is irrevocable and shall apply to me and the members of my party for any Club events in which I may participate or attend.</p>
<p><strong>BASE 4 CODE OF CONDUCT</strong></p>
<p>1. No motorbikes or quadbikes.</p>
<p>2. No loud music (unless authorised by the Committee or its representatives).</p>
<p>3. Dogs to be controlled by their owners who take full responsibility for the animals behaviour.</p>
<p>4. No dogs belonging to non-members are allowed at Base 4 unless with the express permission of the Committee.</p>
<p>5. No person in the rear of open vehicles when driving on obstacles.</p>
<p>6. When driving the obstacles stay on the tracks.</p>
<p>7. Engage 4WD when driving the obstacles to minimise wear and damage to the track.</p>
<p>8. No alcohol to be consumed while driving the track.</p>
<p>9. No littering (please pick up cigarette butts etc.)</p>
<p>10. All rubbish is to be taken away with you when leaving. Dustbins and refuse collection is not provided.</p>
<p>11. Use water sparingly. Please bring your own water and a little extra for the Club.</p>
<p>I am a member of the Four Wheel Drive Club of Southern Africa and I will strive to uphold these Codes.</p>
</div>
<div style="margin-top: 10px;">
<input type="checkbox" id="agreeCheckbox" name="agree" disabled required>
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
</div>
<div id="indemnityBox" style="border: 1px solid #ccc; padding: 10px; height: 150px; overflow-y: scroll; background: #f9f9f9; font-size: 12px;">
<p><strong>INDEMNITY AND WAIVER</strong></p>
<p>1. I agree to abide by the Code of Conduct as listed below, as well as any reasonable instructions given by any Member of the Committee of the Club, or any person appointed by the Club to organise or control any event (Club Officer).</p>
<p>2. I acknowledge that driving the off-road track is inherently dangerous, and that I am fully aware of the dangers thereof. I warrant that I will make all members of my party aware of such dangers prior to driving the track.</p>
<p>3. While I, or any member of my party, enjoy the facilities at Base 4 including overnight camping, picnicking, driving the track, using the swimming pool facility or activity or any other activity while at Base 4, I agree that under no circumstances shall the Club be liable for any loss or damage of any kind whatsoever (including consequential loss) which I or any of my party may suffer, regardless of how such loss or damage may have been caused or sustained, and whether or not as a result of the negligence or breach of contract (whether fundamental or otherwise) or other wrongful act of the Club, or any Club Officer, or any of the Clubs agents or contractors, and I hereby indemnify and hold harmless the Club and any Club Officer against all such loss or damage.</p>
<p>4. The expression, member of my party, means all persons who accompany me or attending any event at my specific invitation, request or suggestion, and includes without limitation, members of family, guests and invitees.</p>
<p>5. I understand that I am responsible for ensuring my vehicle and equipment and that all members of my party have adequate health and medical insurance to cover any and all likely occurrences.</p>
<p>6. This indemnity is irrevocable and shall apply to me and the members of my party for any Club events in which I may participate or attend.</p>
<p><strong>BASE 4 CODE OF CONDUCT</strong></p>
<p>1. No motorbikes or quadbikes.</p>
<p>2. No loud music (unless authorised by the Committee or its representatives).</p>
<p>3. Dogs to be controlled by their owners who take full responsibility for the animals behaviour.</p>
<p>4. No dogs belonging to non-members are allowed at Base 4 unless with the express permission of the Committee.</p>
<p>5. No person in the rear of open vehicles when driving on obstacles.</p>
<p>6. When driving the obstacles stay on the tracks.</p>
<p>7. Engage 4WD when driving the obstacles to minimise wear and damage to the track.</p>
<p>8. No alcohol to be consumed while driving the track.</p>
<p>9. No littering (please pick up cigarette butts etc.)</p>
<p>10. All rubbish is to be taken away with you when leaving. Dustbins and refuse collection is not provided.</p>
<p>11. Use water sparingly. Please bring your own water and a little extra for the Club.</p>
<p>I am a member of the Four Wheel Drive Club of Southern Africa and I will strive to uphold these Codes.</p>
</div>
<div style="margin-top: 10px;">
<input type="checkbox" id="agreeCheckbox" name="agree" disabled required>
<label for="agreeCheckbox" id="agreeLabel" style="color: #888;">I have read and agree to the indemnity terms</label>
</div>
</div>
<h6>Total: <span id="booking_total" class="price">-</span></h6>
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php if ($remaining_places < 1): ?>
@@ -594,13 +594,14 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
</button>
<?php else: ?>
<button type="submit" class="theme-btn style-two w-100 mt-15 mb-5">
<span data-hover="Book Now">Book Now</span>
<span data-hover="PROCEED TO PAYMENT">PROCEED TO PAYMENT</span>
<i class="fal fa-arrow-right"></i>
</button>
<?php endif; ?>
<div class="text-center">
<a href="contact">Need some help?</a>
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div>
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form>
</div>
@@ -727,7 +728,7 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
// Update button and status badge
const publishBtn = $('#publishBtn');
const statusBadge = $('#publishStatus');
if (response.published === 1) {
publishBtn.html('<i class="fas fa-eye-slash"></i> Unpublish Trip');
statusBadge.html('<span class="badge bg-success">Published</span>');
@@ -735,7 +736,7 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
publishBtn.html('<i class="fas fa-eye"></i> Publish Trip');
statusBadge.html('<span class="badge bg-warning">Draft</span>');
}
// Show success message
alert(response.message);
} else {
@@ -750,4 +751,4 @@ include_once(dirname(dirname(dirname(__DIR__))) . '/header.php');
}
</script>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php') ?>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php') ?>

View File

@@ -68,7 +68,15 @@ $stmt->fetch();
$stmt->close();
// If request includes payment_id, fetch provider paylink from payments table
$payment_id = $_GET['payment_id'] ?? null;
if (!isset($_GET['token']) || empty($_GET['token'])) {
header("Location: membership_details");
exit();
}
$token = $_GET['token'];
// echo $token;
// Sanitize the trip_id to prevent SQL injection
$payment_id = decryptData($token, $_ENV['SALT']);
$payment_link = null;
if ($payment_id) {
$pstmt = $conn->prepare("SELECT payment_link, amount, status, provider FROM payments WHERE payment_id = ? LIMIT 1");
@@ -90,36 +98,39 @@ if ($payment_id) {
$pageTitle = 'Membership Payment';
$breadcrumbs = [['Home' => 'index.php'], ['Membership' => 'membership.php']];
require_once($rootPath . '/components/banner.php');
?>
?>
<!-- Contact Form Area start -->
<section class="about-us-area py-100 rpb-90 rel z-1">
<div class="container">
<div class="row">
<div class="col-lg-6">
<div class="section-title mb-25">
<span class="h2 mb-15">New Membership Payment:</span>
<?php echo
'<h5>Membership Start Date: ' . $membership_start_date . '<br>Membership Renewal Date: ' . $membership_end_date . '</h5>'; ?>
<div class="section-title mb-25">
<span class="h2 mb-15">New Membership Payment:</span>
<?php echo
'<h5>Membership Start Date: ' . $membership_start_date . '<br>Membership Renewal Date: ' . $membership_end_date . '</h5>'; ?>
</div>
<?php if (!empty($payment_link)) { ?>
<h5>Payment Details:</h5>
<p>Amount: R <?php echo number_format($payment_amount, 2); ?></p>
<p>Reference: <?php echo htmlspecialchars($payment_id); ?></p>
<a href="<?php echo htmlspecialchars($payment_link); ?>" class="theme-btn style-two style-three" style="width:100%;" target="_blank" rel="noopener noreferrer">
<span data-hover="Pay Now with iKhokha">Pay Now with iKhokha</span>
<i class="fal fa-arrow-right"></i>
</a>
<div class="text-center">
<p>You will be redirected to iKhokha's Secure payment gateway.</p>
</div>
<?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 } ?>
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
<?php } else { ?>
<p>Please upload your proof of payment below.</p>
<h5>Payment Details:</h5>
<p>The Four Wheel Drive Club of Southern Africa<br>FNB<br>Account Number: 58810022334<br>Branch code: 250655<br>Reference: <?php echo htmlspecialchars($eft_id); ?><br>Amount: R <?php echo number_format($payment_amount, 2); ?></p>
<a href="submit_pop" class="theme-btn style-two style-three" style="width:100%;">
<span data-hover="Submit Proof of Payment">Submit Proof of Payment</span>
<i class="fal fa-arrow-right"></i>
</a>
<?php } ?>
</div>
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
@@ -132,4 +143,4 @@ if ($payment_id) {
</div>
</section>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>

View File

@@ -11,8 +11,20 @@ if (isset($_SESSION['user_id'])) {
exit(); // Stop further script execution
}
//if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$stmt->bind_result($payment_status);
$stmt->fetch();
$stmt->close();
$payment_id = uniqid();
if ($payment_status === 'PENDING RENEWAL') {
header("Location: membership_details.php");
exit();
}
$payment_id = generatePaymentRef('SUBS', null, $user_id);
$payment_amount = getPriceByDescription('membership_fees');
$payment_date = date('Y-m-d');
$renewal_period_end = getMembershipEndDate($user_id);
@@ -29,6 +41,11 @@ if ($stmt->execute()) {
// Commit the transaction
$conn->commit();
// Audit: user initiated membership renewal
if (function_exists('auditLog')) {
auditLog($user_id, 'MEMBERSHIP_RENEWAL_INITIATED', 'membership_fees', null, ['payment_id' => $payment_id, 'amount' => $payment_amount]);
}
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
if ($checkP) {
$checkP->bind_param('s', $payment_id);
@@ -65,8 +82,9 @@ if ($stmt->execute()) {
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
$token = encryptData($payment_id, $_ENV['SALT']);
if ($paylink) {
header('Location: membership_payment?payment_id=' . $payment_id);
header('Location: membership_payment?token=' . $token);
exit();
} else {
header("Location: membership_details");

View File

@@ -156,7 +156,7 @@ $page_id = 'bush_mechanics';
</div>
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php
$button_text = "Book Now";
$button_text = "PROCEED TO PAYMENT";
$button_disabled = "";
if (!$result || $result->num_rows == 0) {
$button_text = "No booking dates available";
@@ -168,8 +168,9 @@ $page_id = 'bush_mechanics';
<i class="fal fa-arrow-right"></i>
</button>
<div class="text-center">
<a href="contact">Need some help?</a>
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div>
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form>
</div>

View File

@@ -107,14 +107,14 @@ if (isset($_SESSION['user_id'])) {
if (response.status === 'success') {
// If provider returned a direct paylink, go there immediately
if (response.paylinkUrl) {
window.location.href = 'membership_payment.php?payment_id=' + encodeURIComponent(response.payment_id);
window.location.href = 'membership_payment?token=' + encodeURIComponent(response.token);
return;
}
// If we have a payment_id, redirect to membership_payment with it
// if (response.payment_id) {
// setTimeout(function() {
// window.location.href = 'membership_payment.php?payment_id=' + encodeURIComponent(response.payment_id);
// window.location.href = 'membership_payment.php?payment_id=' + encodeURIComponent(response.token);
// }, 800);
// return;
// }

View File

@@ -154,7 +154,7 @@ $page_id = 'rescue_recovery';
</div>
</div>
<?php
$button_text = "Book Now";
$button_text = "PROCEED TO PAYMENT";
$button_disabled = "";
if (!$result || $result->num_rows == 0) {
$button_text = "No booking dates available";
@@ -165,9 +165,11 @@ $page_id = 'rescue_recovery';
<span data-hover="<?php echo $button_text; ?>"><?php echo $button_text; ?></span>
<i class="fal fa-arrow-right"></i>
</button>
<div class="text-center">
<a href="mailto:info@4wdcsa.co.za">Need some help?</a>
<a href="contact">You will be redirected to iKhokha's Secure payment gateway.</a>
</div>
<img src="assets/images/logos/ikhokha.png"alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
</form>
</div>

View File

@@ -6,7 +6,7 @@ require_once($rootPath . "/src/config/connection.php");
require_once($rootPath . "/src/config/functions.php");
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
$payment_id = uniqid();
$payment_id = generatePaymentRef('SUBS', null, $user_id);
$status = 'AWAITING PAYMENT';
// If current month is December, attribute the membership year to the next year
$currentYear = intval(date('Y'));
@@ -210,9 +210,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
->format('Y-m-d');
}
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id)
VALUES (?, ?, ?, ?, ?, 'AWAITING PAYMENT', ?)");
$stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $payment_id);
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, renewal_period_end, payment_status, payment_id)
VALUES (?, ?, ?, ?, ?, ?, 'AWAITING PAYMENT', ?)");
$stmt->bind_param("idsssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $membership_end_date, $payment_id);
if ($stmt->execute()) {
// Commit the transaction
@@ -221,6 +221,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Optionally send an invoice referencing the internal payment id
// sendInvoice(getEmail($user_id), getFullName($user_id), $payment_id, formatCurrency($payment_amount), $description);
// sendAdminNotification('4WDCSA.co.za - New Membership Application - '.$last_name , 'A new member has signed up, '.$first_name.' '.$last_name);
// Audit: membership application submitted
if (function_exists('auditLog')) {
auditLog($user_id, 'MEMBERSHIP_APPLICATION_SUBMITTED', 'membership_application', null, ['payment_id' => $payment_id, 'amount' => $payment_amount ?? null]);
}
header("Location: indemnity");
// Success message
$response = [

View File

@@ -79,6 +79,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->bind_param('sissiiiidd', $type, $user_id, $from_date, $to_date, $num_vehicles, $num_adults, $num_children, $add_firewood, $total_amount, $discount_amount);
if ($stmt->execute()) {
// Get booking id and audit
$booking_id = $conn->insert_id;
if (function_exists('auditLog')) {
auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]);
}
// Redirect to success page or display success message
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
} else {

View File

@@ -93,11 +93,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$status = "AWAITING PAYMENT";
$type = 'course';
$payment_id = uniqid();
$payment_id = generatePaymentRef('COURSE', $course_id, $user_id);
$publicRef = bin2hex(random_bytes(16));
$num_vehicles = 1;
$discountAmount = 0;
$eft_id = strtoupper("COURSE ".date("m-d", strtotime($date))." ".getInitialSurname($user_id));
$eft_id = $payment_id;
$notes = "";
if ($pending_member){
$notes = "Membership Payment pending at time of booking. Please confirm payment has been received.";
@@ -118,6 +118,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($stmt->execute()) {
$booking_id = $conn->insert_id;
// Audit booking creation
if (function_exists('auditLog')) {
auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
}
if ($payment_amount < 1) {
if (processZeroPayment($payment_id, $payment_amount, $description)) {
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";

View File

@@ -56,6 +56,10 @@ if (isset($_POST['signature'])) {
$stmt->bind_param('si', $display_path, $user_id);
if ($stmt->execute()) {
// Audit: signature saved
if (function_exists('auditLog')) {
auditLog($user_id, 'SIGNATURE_SAVED', 'membership_application', null, ['path' => $display_path]);
}
// Check the payment status
$paymentStatus = checkMembershipPaymentStatus($user_id) ? 'PAID' : 'NOT_PAID';
@@ -77,7 +81,7 @@ if (isset($_POST['signature'])) {
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);
$payment_id = $mf['payment_id'] ?? generatePaymentRef('SUBS', null, $user_id);;
if (empty($mf['payment_id'])) {
// Persist generated payment_id back to membership_fees
@@ -116,21 +120,34 @@ if (isset($_POST['signature'])) {
$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();
if ($ins->execute()) {
// Audit: payment row created for membership
if (function_exists('auditLog')) {
auditLog($user_id, 'MEMBERSHIP_PAYMENT_CREATED', 'payments', null, ['payment_id' => $payment_id, 'amount' => $amount]);
}
}
$ins->close();
}
}
// Create iKhokha paylink via helper (functions.php)
try {
try {
$publicRef = $publicRef ?? bin2hex(random_bytes(16));
$resp = createIkhokhaPayment($payment_id, $amount, $desc ?? ('Membership Fee ' . date('Y')), $publicRef);
$paylink = $resp['paylinkUrl'] ?? $resp['paylinkURL'] ?? $resp['paylink_url'] ?? null;
// After creating paylink, update paymentStatus to AWAITING PAYMENT
$paymentStatus = $paylink ? 'AWAITING PAYMENT' : $paymentStatus;
$token = encryptData($payment_id, $_ENV['SALT']);
// Audit: paylink created (or attempted)
if (function_exists('auditLog')) {
auditLog($user_id, 'IKHOKHA_PAYLINK_CREATED', 'payments', null, ['payment_id' => $payment_id, 'paylink' => $paylink]);
}
} catch (Exception $e) {
// Log but do not fail signature save
error_log('iKhokha create error: ' . $e->getMessage());
if (function_exists('auditLog')) {
auditLog($user_id, 'IKHOKHA_PAYLINK_FAILED', 'payments', null, ['payment_id' => $payment_id, 'error' => $e->getMessage()]);
}
}
}
}
@@ -140,7 +157,8 @@ if (isset($_POST['signature'])) {
$response = [
'status' => 'success',
'message' => 'Signature saved successfully!',
'paymentStatus' => $paymentStatus
'paymentStatus' => $paymentStatus,
'token' => $token ?? null
];
if (!empty($paylink)) {
$response['paylinkUrl'] = $paylink;
@@ -150,6 +168,10 @@ if (isset($_POST['signature'])) {
}
echo json_encode($response);
} else {
// Audit: signature save failed
if (function_exists('auditLog')) {
auditLog($user_id, 'SIGNATURE_SAVE_FAILED', 'membership_application', null, ['user_id' => $user_id]);
}
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
}
@@ -161,6 +183,10 @@ if (isset($_POST['signature'])) {
echo json_encode(['status' => 'error', 'message' => 'Failed to save signature']);
}
} else {
// Audit: no signature provided in request
if (function_exists('auditLog') && isset($_SESSION['user_id'])) {
auditLog($_SESSION['user_id'], 'SIGNATURE_NOT_PROVIDED', 'membership_application', null, ['endpoint' => 'process_signature.php']);
}
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Signature not provided']);
}

View File

@@ -105,10 +105,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$status = "AWAITING PAYMENT";
$description = $trip_name;
$type = 'trip';
$payment_id = uniqid();
$payment_id = generatePaymentRef('TRIP', $trip_id, $user_id);
$publicRef = bin2hex(random_bytes(16));
// $eft_id = strtoupper(base_convert(time(), 10, 36)); // Convert timestamp to base36
$eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
// $eft_id = strtoupper($trip_code." ".getInitialSurname($user_id));
// Insert booking into the database
@@ -126,6 +126,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get the generated booking_id
$booking_id = $conn->insert_id;
// Audit booking creation
if (function_exists('auditLog')) {
auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
}
if ($payment_amount < 1) {
if (processZeroPayment($payment_id, $payment_amount, $description)) {
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";