How can I utilize firebase cloud functions as a callbackurl to receive Mpesa payment responses?

98 Views Asked by At

I'm trying to utilize firebase cloud functions as a callbackurl to receive Mpesa payment responses from Safaricom.

So far consuming the mpesa api and prompting the user to input their pin works.

I have tried to go further and write some cloud function that would receive the payment response, however it doesn't seen to work.

This is my first time working with the mpesa api.

This method call the mpesa api:

  Future mPesaPaymentInAction() async {
    print("mpesa in action!");
    try {
      Mpesa mpesa = Mpesa(
        clientKey: myClientKey, //
        clientSecret: myClientSecret, //
        passKey: myPassKey, //
        environment: myEnvironment, //
      );
      await mpesa
          .lipaNaMpesa(
        amount: 1, //
        phoneNumber: phoneNumberForPayment.text.toString(), //
        businessShortCode: myBusinessShortCode, //
        transactionDescription: transactionDescription,
        callbackUrl: callBackUrl, //
      )
          .then((result) async {
        final data = await getMpesaData(callBackUrl);
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          duration: const Duration(seconds: 5),
          backgroundColor: Colors.greenAccent,
          content: Text(
            "Mpesa Pin request sent by Safaricom, Please wait.",
            textAlign: TextAlign.center,
            style: GoogleFonts.lato(
              fontSize: 14,
            ),
          ),
        ));

        if (result.ResponseCode.toString() == "0" &&
            result.CustomerMessage.toString() ==
                "Success. Request accepted for processing" &&
            result.ResponseDescription ==
                "Success. Request accepted for processing") {
          setState(() {
            mCheckoutRequestID_waterServices = result.CheckoutRequestID;
            // paymentDone_waterServices = "True";
          });
          try {
            final prefs = await SharedPreferences.getInstance();
            await prefs.setString("mCheckoutRequestID_waterServices",
                mCheckoutRequestID_waterServices.toString());
            await prefs.setString(
                "paymentDone_waterServices", paymentDone_waterServices);
          } catch (e) {
            print(e.toString());
          }
          print("mpesa done!");
          // getMpesaData(callBackUrl);
          // await cacheOrderToSharedPrefs();
        } else {
          throw Error();
        }
      }).catchError((error) {
        print(error);
        setState(() {
          loadingScreen = false;
        });
      });
      // await freeMemoryAfterPay();
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        duration: const Duration(seconds: 5),
        backgroundColor: Colors.redAccent,
        content: Text(
          "Please try again.",
          textAlign: TextAlign.center,
          style: GoogleFonts.lato(
            fontSize: 14,
          ),
        ),
      ));
      setState(() {
        loadingScreen = false;
        phoneNumberForPayment.clear();
      });
      if (kDebugMode) {
        print("Was inside mpesa in action:- ${e.toString()}");
      }
    }
  }

This method gets the response data from the callbackurl:

  Future getMpesaData(String callbackUrl) async {
    try {
      print("GETTING DATA");
      final response = await http.post(Uri.parse(callbackUrl));
      print(response.body.toString());

      if (response.statusCode == 200) {
        final data = json.decode(response.body);

        final initiatorName = data['InitiatorName'];
        final securityCredential = data['SecurityCredential'];
        final commandID = data['CommandID'];
        final amount = data['Amount'];
        final mpesaReceiptNumber = data['MpesaReceiptNumber'];
        final transactionID = data['TransactionID'];
        final transactionDate = data['TransactionDate'];
        final phoneNumber = data['PhoneNumber'];
        final resultCode = data['ResultCode'];
        final resultDesc = data['ResultDesc'];

        // Use the extracted data as needed
        print('Initiator Name: $initiatorName');
        print('Security Credential: $securityCredential');
        print('Command ID: $commandID');
        print('Amount: $amount');
        print('M-Pesa Receipt Number: $mpesaReceiptNumber');
        print('Transaction ID: $transactionID');
        print('Transaction Date: $transactionDate');
        print('Phone Number: $phoneNumber');
        print('Result Code: $resultCode');
        print('Result Description: $resultDesc');
        return initiatorName;
      } else {
        throw Exception('Failed to get M-Pesa data: ${response.reasonPhrase}');
      }
    } catch (e) {
      print("Failed to get M-Pesa data");
    }
  }

This is my firebase cloud function:

const functions = require("firebase-functions");
const express = require("express");
const app = express();
const cors = require("cors")({ origin: true });
const bodyParser = require("body-parser");
const axios = require("axios");

// Set up middleware
app.use(cors);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// Set up constants for your MPESA API credentials
const CONSUMER_KEY = ""; 
const CONSUMER_SECRET = """; 
const PASSKEY = ";
const SHORTCODE = "174379";

// Define your Cloud Function
exports.mpesaCallback = functions.https.onRequest((req, res) => {
    // Get the request body data from MPESA
    const data = req.body;

    // Check if the request is valid
    if (!data || !data.Body || !data.Body.stkCallback) {
        return res.status(400).json({ message: "Invalid request data" });
    }

    // Get the payment details from the request data
    const payment = data.Body.stkCallback;

    // Verify that the payment is valid
    if (!payment || payment.ResultCode != 0) {
        return res.status(400).json({ message: "Invalid payment data" });
    }

    // Send a confirmation message to MPESA
    axios({
        url: "https://api.safaricom.co.ke/mpesa/b2c/v1/paymentresulthttp",
        method: "post",
        headers: {
            Authorization:
                "Basic " +
                Buffer.from(CONSUMER_KEY + ":" + CONSUMER_SECRET).toString("base64"),
            "Content-Type": "application/json"
        },
        data: {
            InitiatorName: "",
            SecurityCredential: "",
            CommandID: "PaymentConfirmation",
            Amount: payment.CallbackMetadata.Item[0].Value,
            MpesaReceiptNumber: payment.CallbackMetadata.Item[1].Value,
            TransactionID: payment.CallbackMetadata.Item[2].Value,
            TransactionDate: payment.CallbackMetadata.Item[3].Value,
            PhoneNumber: payment.CallbackMetadata.Item[4].Value,
            ResultCode: 0,
            ResultDesc: "Payment successful"
        }
    })
        .then(response => {
            console.log(response.data);
            return res.status(200).json({ message: "Payment confirmed" });
        })
        .catch(error => {
            console.log(error);
            return res.status(500).json({ message: "Error sending confirmation" });
        });
});

// Export the express app as a Cloud Function
exports.app = functions.https.onRequest(app);

Printing response from http:

I/flutter (13136): {"message":"Invalid request data"}
I/flutter (13136): Failed to get M-Pesa data

GCP Cloud functions logs 'sample':

{
insertId: "1ny6725faqkk59"
labels: {1}
logName: "insertName"
receiveTimestamp: "2023-03-28T11:56:32.298610539Z"
resource: {2}
severity: "DEBUG"
textPayload: "Function execution took 73 ms, finished with status: 'error'"
timestamp: "2023-03-28T11:56:32.028351659Z"
trace: "projects/insertName-6de2c/traces/f1fa60c5b51f6ac7f8d2e16958dacf96"
}
1

There are 1 best solutions below

0
Kevin Owino On

i might be wrong, but i think the callback (function) should reside in a server (have an ip i.e. https://10.100.50.21/api/call_back_function)