diff --git a/pages/callback.php b/pages/callback.php index 102e330..afd89ef 100644 --- a/pages/callback.php +++ b/pages/callback.php @@ -1,73 +1,182 @@ load('dolipesa@dolipesa'); + +// Check if this is a callback from M-Pesa (no GET params, JSON input expected) +if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($_GET)) { + // Handle M-Pesa callback + $callbackData = json_decode(file_get_contents('php://input'), true); + + if (!isset($callbackData['Body']['stkCallback'])) { + http_response_code(400); + exit('Invalid callback data'); + } + + $stkCallback = $callbackData['Body']['stkCallback']; + $resultCode = $stkCallback['ResultCode']; + $resultDesc = $stkCallback['ResultDesc']; + $checkoutRequestID = $stkCallback['CheckoutRequestID']; + + // Log the callback response (assumes llx_dolipesa_transactions table exists) + $sql = "INSERT INTO " . MAIN_DB_PREFIX . "dolipesa_transactions (checkout_request_id, result_code, result_desc, datec) "; + $sql .= "VALUES ('" . $db->escape($checkoutRequestID) . "', '" . $db->escape($resultCode) . "', "; + $sql .= "'" . $db->escape($resultDesc) . "', NOW())"; + $db->query($sql); + + // Process successful payment (ResultCode 0 = success) + if ($resultCode == 0) { + $callbackMetadata = $stkCallback['CallbackMetadata']['Item']; + $transactionId = null; + $amount = null; + $phoneNumber = null; + + foreach ($callbackMetadata as $item) { + switch ($item['Name']) { + case 'MpesaReceiptNumber': + $transactionId = $item['Value']; + break; + case 'Amount': + $amount = $item['Value']; + break; + case 'PhoneNumber': + $phoneNumber = $item['Value']; + break; + } + } + + // Find invoice linked to this CheckoutRequestID (you’d need to store this mapping) + $sql = "SELECT fk_invoice FROM " . MAIN_DB_PREFIX . "dolipesa_transactions "; + $sql .= "WHERE checkout_request_id = '" . $db->escape($checkoutRequestID) . "'"; + $resql = $db->query($sql); + if ($resql && $obj = $db->fetch_object($resql)) { + $invoice = new Facture($db); + $invoice->fetch($obj->fk_invoice); + + if ($invoice->id && $invoice->statut == 1) { // Validated invoice + // Mark invoice as paid + $invoice->setPaid($user, '', $transactionId); + // Log transaction with invoice link + $sql = "UPDATE " . MAIN_DB_PREFIX . "dolipesa_transactions "; + $sql .= "SET fk_invoice = " . $invoice->id . ", transaction_id = '" . $db->escape($transactionId) . "' "; + $sql .= "WHERE checkout_request_id = '" . $db->escape($checkoutRequestID) . "'"; + $db->query($sql); + } + } + } + + http_response_code(200); + exit('Callback processed'); +} + +// Payment initiation (assumes this is accessed via GET with invoice_id and token) $invoiceId = GETPOST('invoice_id', 'int'); $token = GETPOST('token', 'alpha'); if (!$invoiceId || !$token) { - accessforbidden(); + accessforbidden($langs->trans('MissingParameters')); } -// Validate token -$expectedToken = base64_encode(hash('sha256', $invoiceId . time())); +// Validate token (simple example; improve as needed) +$expectedToken = md5($invoiceId . $conf->global->MAIN_SECURITY_SALT); // Use a more secure method in production if ($token !== $expectedToken) { - accessforbidden(); + accessforbidden($langs->trans('InvalidToken')); +} + +// Load invoice +$invoice = new Facture($db); +if ($invoice->fetch($invoiceId) <= 0 || $invoice->statut != 1) { + accessforbidden($langs->trans('InvoiceNotFoundOrNotValidated')); } // Process payment submission if ($_SERVER['REQUEST_METHOD'] === 'POST') { $phoneNumber = GETPOST('phone_number', 'alpha'); - $amount = GETPOST('amount', 'int'); + $amount = floatval($invoice->total_ttc); // Use invoice total - $paybill = dolibarr_get_const($db, "MPESAPAY_PAYBILL"); - $passkey = dolibarr_get_const($db, "MPESAPAY_PASSKEY"); - - $payload = array( - "BusinessShortCode" => $paybill, - "Password" => base64_encode($paybill . $passkey . time()), - "Timestamp" => date('YmdHis'), - "TransactionType" => "CustomerPayBillOnline", - "Amount" => $amount, - "PartyA" => $phoneNumber, - "PartyB" => $paybill, - "PhoneNumber" => $phoneNumber, - "CallBackURL" => DOL_URL_ROOT . "/custom/mpesapay/callback.php", - "AccountReference" => $invoiceId, - "TransactionDesc" => "Payment for Invoice #$invoiceId" - ); - - $module = new modMpesapay($db); - $response = $module->sendMpesaRequest($payload); - - if ($response && $response['ResponseCode'] == "0") { - echo "

Payment initiated. Check your phone to complete.

"; + // Validate phone number (Kenyan format: 2547XXXXXXXX) + if (!preg_match('/^254[0-9]{9}$/', $phoneNumber)) { + setEventMessages($langs->trans('InvalidPhoneNumber'), null, 'errors'); } else { - echo "

Failed to initiate payment. Please try again.

"; - } -} else { - // Display payment form - $sql = "SELECT ref, total FROM ".MAIN_DB_PREFIX."facture WHERE rowid = $invoiceId"; - $result = $db->query($sql); - if ($result) { - $invoice = $db->fetch_object($result); - print load_fiche_titre("Pay Invoice #{$invoice->ref}"); - echo '
'; - echo '
'; - echo '
'; - echo ''; - echo '
'; - } else { - echo "

Invoice not found.

"; + // Load M-Pesa credentials + $consumerKey = $conf->global->MPESAPAY_CONSUMER_KEY; + $consumerSecret = dol_decrypt($conf->global->MPESAPAY_CONSUMER_SECRET); + $shortcode = $conf->global->MPESAPAY_SHORTCODE; + $passkey = dol_decrypt($conf->global->MPESAPAY_PASSKEY); + $callbackUrl = $conf->global->MPESAPAY_CALLBACK_URL ?: DOL_MAIN_URL_ROOT . '/custom/dolipesa/callback.php'; + + // Generate OAuth token + $credentials = base64_encode($consumerKey . ':' . $consumerSecret); + $tokenResponse = dol_http_get('https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials', [ + 'Authorization: Basic ' . $credentials + ]); + $tokenData = json_decode($tokenResponse, true); + $accessToken = $tokenData['access_token'] ?? null; + + if (!$accessToken) { + setEventMessages($langs->trans('FailedToGetMpesaToken'), null, 'errors'); + } else { + // Prepare STK Push payload + $timestamp = date('YmdHis'); + $password = base64_encode($shortcode . $passkey . $timestamp); + $payload = [ + 'BusinessShortCode' => $shortcode, + 'Password' => $password, + 'Timestamp' => $timestamp, + 'TransactionType' => 'CustomerPayBillOnline', + 'Amount' => $amount, + 'PartyA' => $phoneNumber, + 'PartyB' => $shortcode, + 'PhoneNumber' => $phoneNumber, + 'CallBackURL' => $callbackUrl, + 'AccountReference' => $invoice->ref, + 'TransactionDesc' => 'Payment for Invoice #' . $invoice->ref + ]; + + // Send STK Push request + $response = dol_http_post('https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest', json_encode($payload), [ + 'Authorization: Bearer ' . $accessToken, + 'Content-Type: application/json' + ]); + $responseData = json_decode($response, true); + + if ($responseData && $responseData['ResponseCode'] == '0') { + // Store CheckoutRequestID for callback mapping + $checkoutRequestID = $responseData['CheckoutRequestID']; + $sql = "INSERT INTO " . MAIN_DB_PREFIX . "dolipesa_transactions (checkout_request_id, fk_invoice, datec) "; + $sql .= "VALUES ('" . $db->escape($checkoutRequestID) . "', " . $invoice->id . ", NOW())"; + $db->query($sql); + + setEventMessages($langs->trans('PaymentInitiated'), null, 'mesgs'); + } else { + setEventMessages($langs->trans('PaymentInitiationFailed') . ': ' . ($responseData['errorMessage'] ?? 'Unknown error'), null, 'errors'); + } + } } } +// Display payment form +print load_fiche_titre($langs->trans('PayInvoice', $invoice->ref)); +print '
'; +print ''; +print ''; +print ''; +print ''; +print ''; +print '
'; +print '
'; +print '
'; + llxFooter(); $db->close(); -?> +?> \ No newline at end of file