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($langs->trans('MissingParameters')); } // 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($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 = floatval($invoice->total_ttc); // Use invoice total // Validate phone number (Kenyan format: 2547XXXXXXXX) if (!preg_match('/^254[0-9]{9}$/', $phoneNumber)) { setEventMessages($langs->trans('InvalidPhoneNumber'), null, 'errors'); } else { // 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(); ?>