fix(security): wallet race conditions - FOR UPDATE + atomic claims on payments, webhooks, bonuses
This commit is contained in:
@@ -54,47 +54,57 @@ if ($isSuccess !== true) {
|
||||
}
|
||||
logError("2", "Payment reported as SUCCESS by ecash.");
|
||||
|
||||
// 4. Find the original transaction in your database using the Order Reference
|
||||
// 4. Find and process the transaction atomically
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT * FROM ecash_transactions WHERE order_ref = ? LIMIT 1");
|
||||
$con->beginTransaction();
|
||||
|
||||
$stmt = $con->prepare("SELECT * FROM ecash_transactions WHERE order_ref = ? LIMIT 1 FOR UPDATE");
|
||||
$stmt->execute([$orderRef]);
|
||||
$transaction = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$transaction) {
|
||||
$con->rollBack();
|
||||
logError("3", "OrderRef not found in our database.", ["orderRef" => $orderRef]);
|
||||
http_response_code(404); // Not Found
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Security Check: Ensure this transaction hasn't already been processed
|
||||
if ($transaction['status'] !== 'pending') {
|
||||
$con->rollBack();
|
||||
logError("3.1", "Transaction already processed.", ["orderRef" => $orderRef, "status" => $transaction['status']]);
|
||||
http_response_code(200); // Acknowledge receipt, but prevent double-spending
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Atomically mark as processing to prevent concurrent webhooks
|
||||
$lockStmt = $con->prepare("UPDATE ecash_transactions SET status = 'processing' WHERE order_ref = ? AND status = 'pending'");
|
||||
$lockStmt->execute([$orderRef]);
|
||||
if ($lockStmt->rowCount() === 0) {
|
||||
$con->rollBack();
|
||||
logError("3.2", "Concurrent webhook detected, transaction already claimed.", ["orderRef" => $orderRef]);
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
$passengerId = $transaction['passenger_id'];
|
||||
$paidAmount = $transaction['amount']; // Use the amount from your DB as the source of truth
|
||||
$paidAmount = $transaction['amount'];
|
||||
logError("3", "Transaction found in DB.", ["passengerId" => $passengerId, "amount" => $paidAmount]);
|
||||
|
||||
// 5. --- Start Wallet Update Logic (from your paymet_verfy.php) ---
|
||||
|
||||
// Calculate bonus
|
||||
$finalAmount = calculateBonus($paidAmount);
|
||||
logError("4", "Bonus calculated.", ["original" => $paidAmount, "final" => $finalAmount]);
|
||||
|
||||
// Add to Passenger Wallet
|
||||
$passengerToken = generatePaymentToken($passengerId, $finalAmount);
|
||||
if ($passengerToken) {
|
||||
addToPassengerWallet($passengerId, $finalAmount, $passengerToken);
|
||||
}
|
||||
|
||||
// Add to Siro Wallet
|
||||
$paymentMethod = 'ecash'; // Or another identifier
|
||||
$paymentMethod = 'ecash';
|
||||
addToSiroWallet($passengerId, $paidAmount, $paymentMethod);
|
||||
|
||||
// 6. Mark the transaction as 'success' in your database to prevent reprocessing
|
||||
updateTransactionStatus($orderRef, 'success', $transactionNo);
|
||||
$stmtUpdate = $con->prepare("UPDATE ecash_transactions SET status = 'success', ecash_transaction_no = ?, updated_at = NOW() WHERE order_ref = ?");
|
||||
$stmtUpdate->execute([$transactionNo, $orderRef]);
|
||||
|
||||
$con->commit();
|
||||
logError("7", "Process completed successfully.");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
Reference in New Issue
Block a user