added transaction table, fixed signature auth. Monitor for bugs before rmoving bypass
This commit is contained in:
@@ -7,80 +7,102 @@ require_once($rootPath . "/src/config/functions.php");
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Read raw request and headers (DO NOT MODIFY RAW BODY)
|
||||
* JS-equivalent escaping (matches iKhokha docs exactly)
|
||||
* ==========================================================
|
||||
*/
|
||||
function jsStringEscape(string $str): string
|
||||
{
|
||||
$str = preg_replace('/([\\\\\"\'])/', '\\\\$1', $str);
|
||||
$str = str_replace("\0", "\\0", $str);
|
||||
return $str;
|
||||
}
|
||||
|
||||
function createPayloadToSign(string $path, string $body): string
|
||||
{
|
||||
return jsStringEscape($path . $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Read raw request body (DO NOT MODIFY)
|
||||
* ==========================================================
|
||||
*/
|
||||
$raw = file_get_contents('php://input');
|
||||
|
||||
if ($raw === false) {
|
||||
if ($raw === false || $raw === '') {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: unable to read raw input');
|
||||
progress_log('iKhokha webhook: empty body');
|
||||
exit('No body');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Read headers
|
||||
* ==========================================================
|
||||
*/
|
||||
$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)) {
|
||||
if (!$ikSign || !$ikAppId) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: missing IK-SIGN or IK-APPID');
|
||||
progress_log('iKhokha webhook: missing headers');
|
||||
exit('Missing headers');
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Signature verification
|
||||
* HMAC_SHA256( path + raw_body, app_secret )
|
||||
* Signature verification (JS-equivalent)
|
||||
* ==========================================================
|
||||
*/
|
||||
$secret = $_ENV['IKHOKHA_APP_SECRET'] ?? null;
|
||||
$callbackUrl = $_ENV['IKHOKHA_CALLBACK_URL'] ?? null;
|
||||
$bypass = ($_ENV['IKHOKHA_BYPASS_SIGNATURE'] ?? 'false') === 'true';
|
||||
|
||||
if (empty($secret)) {
|
||||
if (!$secret || !$callbackUrl) {
|
||||
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';
|
||||
// Decode body so we can remove `text`
|
||||
$bodyArray = json_decode($raw, true);
|
||||
if (!is_array($bodyArray)) {
|
||||
http_response_code(400);
|
||||
exit('Invalid JSON');
|
||||
}
|
||||
|
||||
// iKhokha JS deletes `text`
|
||||
unset($bodyArray['text']);
|
||||
|
||||
// JS-style JSON (no escaped slashes)
|
||||
$jsonBody = json_encode($bodyArray, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
|
||||
// Now sign the SAME payload JS signs
|
||||
$payloadToSign = createPayloadToSign($callbackUrl, $jsonBody);
|
||||
|
||||
$expected = generateSignature($payloadToSign, $secret);
|
||||
|
||||
progress_log('JS PAYLOAD: ' . $payloadToSign);
|
||||
progress_log('EXPECTED SIGN: ' . $expected);
|
||||
progress_log('RECEIVED SIGN: ' . $ikSign);
|
||||
|
||||
if (!$bypass) {
|
||||
|
||||
if (empty($callbackUrl)) {
|
||||
http_response_code(500);
|
||||
progress_log('iKhokha webhook: callback URL not configured');
|
||||
exit('Server misconfigured');
|
||||
}
|
||||
|
||||
$expected = hash_hmac(
|
||||
'sha256',
|
||||
$callbackUrl . $raw,
|
||||
$_ENV['IKHOKHA_APP_SECRET']
|
||||
);
|
||||
|
||||
if (!hash_equals($expected, $ikSign)) {
|
||||
http_response_code(403);
|
||||
progress_log('iKhokha webhook: signature mismatch');
|
||||
progress_log('EXPECTED SIGN: ' . $expected);
|
||||
progress_log('RECEIVED SIGN: ' . $ikSign);
|
||||
// Audit signature mismatch
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, ['expected' => $expected, 'received' => $ikSign]);
|
||||
auditLog(null, 'IKHOKHA_SIGNATURE_MISMATCH', 'webhook', null, [
|
||||
'expected' => $expected,
|
||||
'received' => $ikSign
|
||||
]);
|
||||
}
|
||||
exit('Invalid signature');
|
||||
}
|
||||
@@ -95,20 +117,13 @@ if (!$bypass) {
|
||||
* ==========================================================
|
||||
*/
|
||||
$payload = json_decode($raw, true);
|
||||
|
||||
if (!is_array($payload)) {
|
||||
http_response_code(400);
|
||||
progress_log('iKhokha webhook: invalid JSON');
|
||||
exit('Invalid JSON');
|
||||
}
|
||||
$data = $payload['data'] ?? $payload;
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Extract data safely (iKhokha is inconsistent)
|
||||
* Extract fields safely
|
||||
* ==========================================================
|
||||
*/
|
||||
$data = $payload['data'] ?? $payload;
|
||||
|
||||
$externalTransactionID =
|
||||
$data['externalTransactionID']
|
||||
?? $data['externalTransactionId']
|
||||
@@ -127,11 +142,11 @@ $providerStatus =
|
||||
|
||||
progress_log('Parsed externalTransactionID: ' . $externalTransactionID);
|
||||
progress_log('Parsed providerPaymentId: ' . $providerPaymentId);
|
||||
progress_log('Parsed providerStatus: ' . print_r($providerStatus, true));
|
||||
progress_log('Parsed providerStatus: ' . $providerStatus);
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Locate local payment
|
||||
* Locate payment
|
||||
* ==========================================================
|
||||
*/
|
||||
$localPaymentId = null;
|
||||
@@ -146,16 +161,13 @@ if ($externalTransactionID) {
|
||||
WHERE payment_id = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $externalTransactionID);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
extract($row);
|
||||
$localPaymentId = $row['payment_id'];
|
||||
$booking_id = $row['booking_id'];
|
||||
$user_id = $row['user_id'];
|
||||
$description = $row['description'];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
@@ -168,16 +180,13 @@ if (!$localPaymentId && $providerPaymentId) {
|
||||
WHERE provider_payment_id = ?
|
||||
LIMIT 1"
|
||||
);
|
||||
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('s', $providerPaymentId);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
extract($row);
|
||||
$localPaymentId = $row['payment_id'];
|
||||
$booking_id = $row['booking_id'];
|
||||
$user_id = $row['user_id'];
|
||||
$description = $row['description'];
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
@@ -185,11 +194,6 @@ if (!$localPaymentId && $providerPaymentId) {
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -216,72 +220,58 @@ 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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ==========================================================
|
||||
* Normalize status and apply business logic
|
||||
* Business logic
|
||||
* ==========================================================
|
||||
*/
|
||||
$normalized = strtoupper(trim((string)$providerStatus));
|
||||
|
||||
if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
||||
|
||||
// Mark payment as PAID
|
||||
$setPaid = $conn->prepare(
|
||||
$conn->prepare(
|
||||
"UPDATE payments SET status = 'PAID' WHERE payment_id = ?"
|
||||
);
|
||||
if ($setPaid) {
|
||||
$setPaid->bind_param('s', $localPaymentId);
|
||||
$setPaid->execute();
|
||||
$setPaid->close();
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'PAYMENT_MARKED_PAID', 'payment', null, ['payment_id' => $localPaymentId]);
|
||||
}
|
||||
}
|
||||
)->bind_param('s', $localPaymentId)->execute();
|
||||
|
||||
// Booking or membership update
|
||||
if (!empty($booking_id)) {
|
||||
$upd = $conn->prepare(
|
||||
if ($booking_id) {
|
||||
$conn->prepare(
|
||||
"UPDATE bookings SET status = 'PAID' WHERE booking_id = ?"
|
||||
);
|
||||
if ($upd) {
|
||||
$upd->bind_param('i', $booking_id);
|
||||
$upd->execute();
|
||||
$upd->close();
|
||||
sendAdminNotification('4WDCSA.co.za - New Booking - '.getFullName($user_id) , 'We have received a payment for a new booking for '.$description.' from '.getFullName($user_id));
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'BOOKING_PAYMENT_MARKED_PAID', 'bookings', $booking_id, ['payment_id' => $localPaymentId]);
|
||||
}
|
||||
}
|
||||
)->bind_param('i', $booking_id)->execute();
|
||||
} else {
|
||||
$upd = $conn->prepare(
|
||||
"UPDATE membership_fees
|
||||
SET payment_status = 'PAID'
|
||||
WHERE payment_id = ?"
|
||||
);
|
||||
if ($upd) {
|
||||
$upd->bind_param('s', $localPaymentId);
|
||||
$upd->execute();
|
||||
$upd->close();
|
||||
sendAdminNotification('4WDCSA.co.za - Membership Payment Received - '.getFullName($user_id) , 'A Membership Payment has been received from '.getFullName($user_id));
|
||||
if (function_exists('auditLog')) {
|
||||
auditLog($user_id, 'MEMBERSHIP_PAYMENT_MARKED_PAID', 'membership_fees', null, ['payment_id' => $localPaymentId]);
|
||||
}
|
||||
}
|
||||
$conn->prepare(
|
||||
"UPDATE membership_fees SET payment_status = 'PAID' WHERE payment_id = ?"
|
||||
)->bind_param('s', $localPaymentId)->execute();
|
||||
}
|
||||
|
||||
// Send confirmation email
|
||||
if (!empty($user_id)) {
|
||||
sendPaymentConfirmation(
|
||||
getEmail($user_id),
|
||||
getFullName($user_id),
|
||||
$description
|
||||
);
|
||||
}
|
||||
sendPaymentConfirmation(
|
||||
getEmail($user_id),
|
||||
getFullName($user_id),
|
||||
$description
|
||||
);
|
||||
|
||||
//generate $message for admin payment confirmation with payment details
|
||||
$message = "Payment Confirmation\n\n";
|
||||
$message .= "Payment ID: " . $localPaymentId . "\n";
|
||||
$message .= "Amount: " . getPaymentAmount($localPaymentId) . "\n";
|
||||
$message .= "Status: PAID\n";
|
||||
$message .= "Description: " . $description . "\n";
|
||||
$message .= "Thank you.\n";
|
||||
$subject = "4WDCSA.co.za Payment Confirmation for Payment ID: " . $localPaymentId;
|
||||
progress_log('Payment confirmation sent for payment ID: ' . $localPaymentId);
|
||||
|
||||
sendEmail(
|
||||
$_ENV['FINANCE_EMAIL'],
|
||||
$subject,
|
||||
nl2br($message)
|
||||
);
|
||||
sendEmail(
|
||||
'chrispintoza@gmail.com',
|
||||
$subject,
|
||||
nl2br($message)
|
||||
);
|
||||
sendAdminNotification($subject, nl2br($message));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user