2

I'm trying to make an app that can send payments to PayBill numbers with Safaricom's "Lipa Na M-Pesa" (a Kenyan thing). The call is a POST request to URL:

https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest

with header:

{
        'Host': 'sandbox.safaricom.co.ke',
        'Authorization': 'Bearer ${await mpesaAccessToken}',
        'Content-Type': 'application/json',
      }

and body:

{
        "BusinessShortCode": "$businessShortCode",
        "Password": "${generateLnmPassword(timeStamp)}",
        "Timestamp": "$timeStamp",
        "TransactionType": "CustomerPayBillOnline",
        "Amount": "10",
        "PartyA": "$userPhoneNumber",
        "PartyB": "$businessShortCode",
        "PhoneNumber": "$userPhoneNumber",
        "CallBackURL": "?????????????????????????????",
        "AccountReference": "account",
        "TransactionDesc": "test",
      }

I've received an access token, generated a password and made the call successfully, except for that CallBackURL thing... The M-Pesa docs describe their callback like this:

CallBackURL This is the endpoint where you want the results of the transaction delivered. Same rules for Register URL API callbacks apply.

all API callbacks from transactional requests are POST requests, do not expect GET requests for callbacks. Also, the data is not formatted into application/x-www-form-urlencoded format, it is application/json, so do not expect the data in the usual POST fields/variables of your language, read the results directly from the incoming input stream.

(More info here, but you may need to be logged in: https://developer.safaricom.co.ke/get-started see "Lipa na M-Pesa")

My app is hosted on Firebase Cloud Firestore. Is there any way I can create a callback URL with them that will receive their callback as a document in a Firestore collection?...

Or would this be impossible, given that they would need authorization tokens and stuff to do so... and I can't influence what headers and body M-Pesa will send?

(PS Btw, I code in Flutter/Dart so plz don't answer in Javascript or anything! I'll be clueless... :p Flutter/Dart or just plain text will be fine. Thanks!)

2 Answers2

1

Is there any way I can create a callback URL with them that will receive their callback as a document in a Firestore collection?...

The most common way to do that in the Firebase ecosystem is to write an HTTPS Cloud Function that will be called by the Safaricom service.

Within the Cloud Function you will be able to update the Firestore document, based on the content of the POST request.

Something like:

exports.safaricom = functions.https.onRequest((req, res) => {
    // Get the header and body through the req variable
    // See https://firebase.google.com/docs/functions/http-events#read_values_from_the_request

    return admin.firestore().collection('...').doc('...').update({ foo: bar })
        .then(() => {
            res.status(200).send("OK");
        })
        .catch(error => {
            // ...
            // See https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3
        })

});

I did note that you ask us to not "answer in Javascript or anything" but in Flutter/Dart, but I don't think you will able to implement that in Flutter: you need to implement this webhook in an environment that you fully control and that exposes an API endpoint, like your own server or a Cloud Function.

Cloud Functions may seem complex at first sight, but implementing an HTTPS Cloud Functions is not that complicated. I suggest you read the Get Started documentation and watch the three videos about "JavaScript Promises" from the Firebase video series, and if you encounter any problem, ask a new question on SO.

Renaud Tarnec
  • 53,666
  • 7
  • 52
  • 80
0

Cloud functions are not Dart-based.

See below solution;

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const parse = require("./parse");

admin.initializeApp();

exports.lmno_callback_url = functions.https.onRequest(async (req, res) => {
    const callbackData = req.body.Body.stkCallback;
    const parsedData = parse(callbackData);

    let lmnoResponse = admin.firestore().collection('lmno_responses').doc('/' + parsedData.checkoutRequestID + '/');
    let transaction = admin.firestore().collection('transactions').doc('/' + parsedData.checkoutRequestID + '/');
    let wallets = admin.firestore().collection('wallets');

    if ((await lmnoResponse.get()).exists) {
        await lmnoResponse.update(parsedData);
    } else {
        await lmnoResponse.set(parsedData);
    }
    if ((await transaction.get()).exists) {
        await transaction.update({
            'amount': parsedData.amount,
            'confirmed': true
        });
    } else {
        await transaction.set({
            'moneyType': 'money',
            'type': 'deposit',
            'amount': parsedData.amount,
            'confirmed': true
        });
    }
    let walletId = await transaction.get().then(value => value.data().toUserId);

    let wallet = wallets.doc('/' + walletId + '/');

    if ((await wallet.get()).exists) {
        let balance = await wallet.get().then(value => value.data().moneyBalance);
        await wallet.update({
            'moneyBalance': parsedData.amount + balance
        })
    } else {
        await wallet.set({
            'moneyBalance': parsedData.amount
        })
    }

    res.send("Completed");
});

Parse function.

const moment = require("moment");

function parse(responseData) {
    const parsedData = {};
    parsedData.merchantRequestID = responseData.MerchantRequestID;
    parsedData.checkoutRequestID = responseData.CheckoutRequestID;
    parsedData.resultDesc = responseData.ResultDesc;
    parsedData.resultCode = responseData.ResultCode;

    if (parsedData.resultCode === 0) {
        responseData.CallbackMetadata.Item.forEach(element => {
            switch (element.Name) {
                case "Amount":
                    parsedData.amount = element.Value;
                    break;
                case "MpesaReceiptNumber":
                    parsedData.mpesaReceiptNumber = element.Value;
                    break;
                case "TransactionDate":
                    parsedData.transactionDate = moment(
                        element.Value,
                        "YYYYMMDDhhmmss"
                    ).unix();
                    break;
                case "PhoneNumber":
                    parsedData.phoneNumber = element.Value;
                    break;
            }
        });
    }

    return parsedData;
}

module.exports = parse;
Tim Kariuki
  • 191
  • 2
  • 8