275 lines
7.5 KiB
PHP
275 lines
7.5 KiB
PHP
<?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 - Membership Payment Received - '.getFullName($user_id) , 'A Membership Payment has been received from '.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';
|