diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py index 1d318bbf04..dea4d81770 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py @@ -26,11 +26,19 @@ class MpesaSettings(Document): def on_update(self): create_custom_pos_fields() create_payment_gateway('Mpesa-' + self.payment_gateway_name, settings='Mpesa Settings', controller=self.payment_gateway_name) - create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone") call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name, payment_channel="Phone") + # required to fetch the bank account details from the payment gateway account + frappe.db.commit() + create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone") + def request_for_payment(self, **kwargs): - response = frappe._dict(generate_stk_push(**kwargs)) + if frappe.flags.in_test: + from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload + response = frappe._dict(get_payment_request_response_payload()) + else: + response = frappe._dict(generate_stk_push(**kwargs)) + self.handle_api_response("CheckoutRequestID", kwargs, response) def get_account_balance_info(self): @@ -39,7 +47,13 @@ class MpesaSettings(Document): reference_docname=self.name, doc_details=vars(self) ) - response = frappe._dict(get_account_balance(payload)) + + if frappe.flags.in_test: + from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_test_account_balance_response + response = frappe._dict(get_test_account_balance_response()) + else: + response = frappe._dict(get_account_balance(payload)) + self.handle_api_response("ConversationID", payload, response) def handle_api_response(self, global_id, request_dict, response): @@ -92,7 +106,6 @@ def sanitize_mobile_number(number): def verify_transaction(**kwargs): """Verify the transaction result received via callback from stk.""" transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) - frappe.logger().debug(transaction_response) checkout_id = getattr(transaction_response, "CheckoutRequestID", "") request = frappe.get_doc("Integration Request", checkout_id) @@ -148,14 +161,13 @@ def process_balance_info(**kwargs): return transaction_data = frappe._dict(loads(request.data)) - frappe.logger().debug(account_balance_response) if account_balance_response["ResultCode"] == 0: try: result_params = account_balance_response["ResultParameters"]["ResultParameter"] balance_info = fetch_param_value(result_params, "AccountBalance", "Key") - balance_info = convert_to_json(balance_info) + balance_info = format_string_to_json(balance_info) ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname) ref_doc.db_set("account_balance", balance_info) @@ -168,15 +180,15 @@ def process_balance_info(**kwargs): else: request.handle_failure(account_balance_response) -def convert_to_json(balance_info): +def format_string_to_json(balance_info): """ - Convert string to json. + Format string to json. e.g: '''Working Account|KES|481000.00|481000.00|0.00|0.00''' => {'Working Account': {'current_balance': '481000.00', 'available_balance': '481000.00', 'reserved_balance': '0.00', - 'uncleared_balance': '0.00'} + 'uncleared_balance': '0.00'}} """ balance_dict = frappe._dict() for account_info in balance_info.split("&"): diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index 4aa970ef8a..55ccff30fe 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -2,9 +2,239 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - -# import frappe +from json import dumps +import frappe import unittest +from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction +from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice class TestMpesaSettings(unittest.TestCase): - pass + def test_creation_of_payment_gateway(self): + mpesa_doc = create_mpesa_settings(payment_gateway_name="_Test") + + mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test") + self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"})) + self.assertTrue(mode_of_payment.name) + self.assertEquals(mode_of_payment.type, "Phone") + + def test_processing_of_account_balance(self): + mpesa_doc = create_mpesa_settings(payment_gateway_name="_Account Balance") + mpesa_doc.get_account_balance_info() + + callback_response = get_account_balance_callback_payload() + process_balance_info(**callback_response) + integration_request = frappe.get_doc("Integration Request", "AG_20200927_00007cdb1f9fb6494315") + + # test integration request creation and successful update of the status on receiving callback response + self.assertTrue(integration_request) + self.assertEquals(integration_request.status, "Completed") + + # test formatting of account balance received as string to json with appropriate currency symbol + mpesa_doc.reload() + self.assertEquals(mpesa_doc.account_balance, dumps({ + "Working Account": { + "current_balance": "Sh 481,000.00", + "available_balance": "Sh 481,000.00", + "reserved_balance": "Sh 0.00", + "uncleared_balance": "Sh 0.00" + } + })) + + def test_processing_of_callback_payload(self): + mpesa_doc = create_mpesa_settings(payment_gateway_name="Payment") + mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") + frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") + + pos_invoice = create_pos_invoice(do_not_submit=1) + pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500}) + pos_invoice.contact_mobile = "093456543894" + pos_invoice.currency = "KES" + pos_invoice.save() + + pr = pos_invoice.create_payment_request() + # test payment request creation + self.assertEquals(pr.payment_gateway, "Mpesa-Payment") + + callback_response = get_payment_callback_payload() + verify_transaction(**callback_response) + # test creation of integration request + integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972") + + # test integration request creation and successful update of the status on receiving callback response + self.assertTrue(integration_request) + self.assertEquals(integration_request.status, "Completed") + + pos_invoice.reload() + integration_request.reload() + self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R") + self.assertEquals(integration_request.status, "Completed") + +def create_mpesa_settings(payment_gateway_name="Express"): + if frappe.db.exists("Mpesa Settings", payment_gateway_name): + return frappe.get_doc("Mpesa Settings", payment_gateway_name) + + doc = frappe.get_doc(dict( + doctype="Mpesa Settings", + payment_gateway_name=payment_gateway_name, + consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn", + consumer_secret="VI1oS3oBGPJfh3JyvLHw", + online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd", + till_number="174379" + )) + + doc.insert(ignore_permissions=True) + return doc + +def get_test_account_balance_response(): + """Response received after calling the account balance API.""" + return { + "ResultType":0, + "ResultCode":0, + "ResultDesc":"The service request has been accepted successfully.", + "OriginatorConversationID":"10816-694520-2", + "ConversationID":"AG_20200927_00007cdb1f9fb6494315", + "TransactionID":"LGR0000000", + "ResultParameters":{ + "ResultParameter":[ + { + "Key":"ReceiptNo", + "Value":"LGR919G2AV" + }, + { + "Key":"Conversation ID", + "Value":"AG_20170727_00004492b1b6d0078fbe" + }, + { + "Key":"FinalisedTime", + "Value":20170727101415 + }, + { + "Key":"Amount", + "Value":10 + }, + { + "Key":"TransactionStatus", + "Value":"Completed" + }, + { + "Key":"ReasonType", + "Value":"Salary Payment via API" + }, + { + "Key":"TransactionReason" + }, + { + "Key":"DebitPartyCharges", + "Value":"Fee For B2C Payment|KES|33.00" + }, + { + "Key":"DebitAccountType", + "Value":"Utility Account" + }, + { + "Key":"InitiatedTime", + "Value":20170727101415 + }, + { + "Key":"Originator Conversation ID", + "Value":"19455-773836-1" + }, + { + "Key":"CreditPartyName", + "Value":"254708374149 - John Doe" + }, + { + "Key":"DebitPartyName", + "Value":"600134 - Safaricom157" + } + ] + }, + "ReferenceData":{ + "ReferenceItem":{ + "Key":"Occasion", + "Value":"aaaa" + } + } + } + +def get_payment_request_response_payload(): + """Response received after successfully calling the stk push process request API.""" + return { + "MerchantRequestID": "8071-27184008-1", + "CheckoutRequestID": "ws_CO_061020201133231972", + "ResultCode": 0, + "ResultDesc": "The service request is processed successfully.", + "CallbackMetadata": { + "Item": [ + { "Name": "Amount", "Value": 500.0 }, + { "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" }, + { "Name": "TransactionDate", "Value": 20201006113336 }, + { "Name": "PhoneNumber", "Value": 254723575670 } + ] + } + } + + +def get_payment_callback_payload(): + """Response received from the server as callback after calling the stkpush process request API.""" + return { + "Body":{ + "stkCallback":{ + "MerchantRequestID":"19465-780693-1", + "CheckoutRequestID":"ws_CO_061020201133231972", + "ResultCode":0, + "ResultDesc":"The service request is processed successfully.", + "CallbackMetadata":{ + "Item":[ + { + "Name":"Amount", + "Value":500 + }, + { + "Name":"MpesaReceiptNumber", + "Value":"LGR7OWQX0R" + }, + { + "Name":"Balance" + }, + { + "Name":"TransactionDate", + "Value":20170727154800 + }, + { + "Name":"PhoneNumber", + "Value":254721566839 + } + ] + } + } + } + } + +def get_account_balance_callback_payload(): + """Response received from the server as callback after calling the account balance API.""" + return { + "Result":{ + "ResultType": 0, + "ResultCode": 0, + "ResultDesc": "The service request is processed successfully.", + "OriginatorConversationID": "16470-170099139-1", + "ConversationID": "AG_20200927_00007cdb1f9fb6494315", + "TransactionID": "OIR0000000", + "ResultParameters": { + "ResultParameter": [ + { + "Key": "AccountBalance", + "Value": "Working Account|KES|481000.00|481000.00|0.00|0.00" + }, + { "Key": "BOCompletedTime", "Value": 20200927234123 } + ] + }, + "ReferenceData": { + "ReferenceItem": { + "Key": "QueueTimeoutURL", + "Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit" + } + } + } + } \ No newline at end of file