如何使用 Firebase Cloud Firestore 进行 M-Pesa 回调 URL?

How do I make an M-Pesa Callback URL using Firebase Cloud Firestore?

我正在尝试制作一个应用程序,可以使用 Safaricom 的“Lipa Na M-Pesa”(肯尼亚的东西)向 PayBill 号码发送付款。该调用是对 URL:

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

与 header:

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

和body:

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

我收到了一个访问令牌,生成了一个密码并成功拨打了电话,除了 CallBackURL 事情...... M-Pesa 文档这样描述他们的回调:

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.

(此处有更多信息,但您可能需要登录:https://developer.safaricom.co.ke/get-started 参见“Lipa na M-Pesa”)

我的应用托管在 Firebase Cloud Firestore 上。有什么方法可以让我与他们一起创建一个回调 URL,将他们的回调作为 Firestore collection 中的文档接收?...

或者这是不可能的,因为他们需要授权令牌和其他东西才能这样做......而且我无法影响 headers 和 body M-Pesa会发送吗?

(PS 顺便说一句,我在 Flutter/Dart 中编码,所以请不要在 Javascript 或其他任何地方回答!我会一无所知...:p Flutter/Dart 或者只是纯文本就可以了。谢谢!)

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

在 Firebase 生态系统中最常见的方法是编写一个 HTTPS Cloud Function 将由 Safaricom 服务调用。

在 Cloud Function 中,您将能够根据 POST 请求的内容更新 Firestore 文档。

类似于:

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
        })

});

我确实注意到您要求我们不要“在 Javascript 或任何内容中回答”,而是在 Flutter/Dart 中回答,但我认为您无法在 Flutter 中实现:您需要在您完全控制并公开 API 端点的环境中实施此 webhook,例如您自己的服务器或 Cloud Functions。

Cloud Functions 乍一看似乎很复杂,但实现 HTTPS Cloud Functions 并不那么复杂。我建议你阅读 Get Started documentation and watch the three videos about "JavaScript Promises" from the Firebase video series,如果你遇到任何问题,请在 SO 上提出新问题。

云函数不是基于 Dart 的。

参见下面的解决方案;

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");
});

解析函数。

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;