fix: process transaction callback
This commit is contained in:
parent
98658603dd
commit
8d12c3841f
@ -142,23 +142,6 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
|
|||||||
frm: cur_frm
|
frm: cur_frm
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
request_for_payment: function (frm) {
|
|
||||||
frm.save().then(() => {
|
|
||||||
frappe.dom.freeze();
|
|
||||||
frappe.call({
|
|
||||||
method: 'create_payment_request',
|
|
||||||
doc: frm.doc,
|
|
||||||
})
|
|
||||||
.fail(() => {
|
|
||||||
frappe.dom.unfreeze();
|
|
||||||
frappe.msgprint('Payment request failed');
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
frappe.msgprint('Payment request sent successfully');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
$.extend(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }))
|
$.extend(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }))
|
||||||
@ -218,5 +201,22 @@ frappe.ui.form.on('POS Invoice', {
|
|||||||
}
|
}
|
||||||
frm.set_value("loyalty_amount", loyalty_amount);
|
frm.set_value("loyalty_amount", loyalty_amount);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
request_for_payment: function (frm) {
|
||||||
|
frm.save().then(() => {
|
||||||
|
frappe.dom.freeze();
|
||||||
|
frappe.call({
|
||||||
|
method: 'create_payment_request',
|
||||||
|
doc: frm.doc,
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
frappe.dom.unfreeze();
|
||||||
|
frappe.msgprint('Payment request failed');
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
frappe.msgprint('Payment request sent successfully');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -279,8 +279,7 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Return (Credit Note)",
|
"label": "Is Return (Credit Note)",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"set_only_once": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break1",
|
"fieldname": "column_break1",
|
||||||
@ -461,7 +460,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "contact_mobile",
|
"fieldname": "contact_mobile",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Mobile No",
|
"label": "Mobile No",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@ -1579,10 +1578,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-07 12:43:09.138720",
|
"modified": "2020-09-28 16:51:24.641755",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice",
|
"name": "POS Invoice",
|
||||||
|
@ -6,6 +6,7 @@ import datetime
|
|||||||
class MpesaConnector():
|
class MpesaConnector():
|
||||||
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
|
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
|
||||||
live_url="https://safaricom.co.ke"):
|
live_url="https://safaricom.co.ke"):
|
||||||
|
"""Setup configuration for Mpesa connector and generate new access token."""
|
||||||
self.env = env
|
self.env = env
|
||||||
self.app_key = app_key
|
self.app_key = app_key
|
||||||
self.app_secret = app_secret
|
self.app_secret = app_secret
|
||||||
@ -38,6 +39,7 @@ class MpesaConnector():
|
|||||||
remarks=None, queue_timeout_url=None,result_url=None):
|
remarks=None, queue_timeout_url=None,result_url=None):
|
||||||
"""
|
"""
|
||||||
This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number).
|
This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
initiator (str): Username used to authenticate the transaction.
|
initiator (str): Username used to authenticate the transaction.
|
||||||
security_credential (str): Generate from developer portal.
|
security_credential (str): Generate from developer portal.
|
||||||
@ -73,6 +75,7 @@ class MpesaConnector():
|
|||||||
phone_number=None, description=None):
|
phone_number=None, description=None):
|
||||||
"""
|
"""
|
||||||
This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
|
This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
business_shortcode (int): The short code of the organization.
|
business_shortcode (int): The short code of the organization.
|
||||||
passcode (str): Get from developer portal
|
passcode (str): Get from developer portal
|
||||||
|
@ -2,9 +2,7 @@ import frappe
|
|||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
def create_custom_pos_fields():
|
def create_custom_pos_fields():
|
||||||
"""
|
"""Create custom fields corresponding to POS Settings and POS Invoice."""
|
||||||
Create custom fields corresponding to POS Settings and POS Invoice
|
|
||||||
"""
|
|
||||||
pos_field = {
|
pos_field = {
|
||||||
"POS Invoice": [
|
"POS Invoice": [
|
||||||
{
|
{
|
||||||
@ -14,6 +12,13 @@ def create_custom_pos_fields():
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"insert_after": "contact_email"
|
"insert_after": "contact_email"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "Mpesa Receipt Number",
|
||||||
|
"label": "mpesa_receipt_number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"read_only": 1,
|
||||||
|
"insert_after": "company"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
if not frappe.get_meta("POS Invoice").has_field("request_for_payment"):
|
if not frappe.get_meta("POS Invoice").has_field("request_for_payment"):
|
||||||
|
@ -15,7 +15,6 @@ frappe.ui.form.on('Mpesa Settings', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setup_account_balance_html: function(frm) {
|
setup_account_balance_html: function(frm) {
|
||||||
console.log(frm.doc.account_balance)
|
|
||||||
$("div").remove(".form-dashboard-section.custom");
|
$("div").remove(".form-dashboard-section.custom");
|
||||||
frm.dashboard.add_section(
|
frm.dashboard.add_section(
|
||||||
frappe.render_template('account_balance', {
|
frappe.render_template('account_balance', {
|
||||||
|
@ -9,10 +9,9 @@ from json import loads, dumps
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import call_hook_method
|
from frappe.utils import call_hook_method, fmt_money
|
||||||
from frappe.integrations.utils import create_request_log, create_payment_gateway
|
from frappe.integrations.utils import create_request_log, create_payment_gateway
|
||||||
from frappe.utils import get_request_site_address
|
from frappe.utils import get_request_site_address
|
||||||
from frappe.utils import get_request_site_address
|
|
||||||
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import create_custom_pos_fields
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import create_custom_pos_fields
|
||||||
@ -27,7 +26,7 @@ class MpesaSettings(Document):
|
|||||||
def on_update(self):
|
def on_update(self):
|
||||||
create_custom_pos_fields()
|
create_custom_pos_fields()
|
||||||
create_payment_gateway('Mpesa-' + self.payment_gateway_name, settings='Mpesa Settings', controller=self.payment_gateway_name)
|
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)
|
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")
|
call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name, payment_channel="Phone")
|
||||||
|
|
||||||
def request_for_payment(self, **kwargs):
|
def request_for_payment(self, **kwargs):
|
||||||
@ -44,6 +43,8 @@ class MpesaSettings(Document):
|
|||||||
self.handle_api_response("ConversationID", payload, response)
|
self.handle_api_response("ConversationID", payload, response)
|
||||||
|
|
||||||
def handle_api_response(self, global_id, request_dict, response):
|
def handle_api_response(self, global_id, request_dict, response):
|
||||||
|
"""Response received from API calls returns a global identifier for each transaction,
|
||||||
|
this code is returned during the callback"""
|
||||||
# check error response
|
# check error response
|
||||||
if getattr(response, "requestId"):
|
if getattr(response, "requestId"):
|
||||||
req_name = getattr(response, "requestId")
|
req_name = getattr(response, "requestId")
|
||||||
@ -59,6 +60,7 @@ class MpesaSettings(Document):
|
|||||||
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
|
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
|
||||||
|
|
||||||
def generate_stk_push(**kwargs):
|
def generate_stk_push(**kwargs):
|
||||||
|
"""Generate stk push by making a API call to the stk push API."""
|
||||||
args = frappe._dict(kwargs)
|
args = frappe._dict(kwargs)
|
||||||
try:
|
try:
|
||||||
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
|
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
|
||||||
@ -70,6 +72,8 @@ def generate_stk_push(**kwargs):
|
|||||||
app_key=mpesa_settings.consumer_key,
|
app_key=mpesa_settings.consumer_key,
|
||||||
app_secret=mpesa_settings.get_password("consumer_secret"))
|
app_secret=mpesa_settings.get_password("consumer_secret"))
|
||||||
|
|
||||||
|
mobile_number = sanitize_mobile_number(args.sender)
|
||||||
|
|
||||||
response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
|
response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
|
||||||
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
|
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
|
||||||
callback_url=callback_url, reference_code=args.payment_request_name,
|
callback_url=callback_url, reference_code=args.payment_request_name,
|
||||||
@ -81,10 +85,15 @@ def generate_stk_push(**kwargs):
|
|||||||
frappe.log_error(title=_("Mpesa Express Transaction Error"))
|
frappe.log_error(title=_("Mpesa Express Transaction Error"))
|
||||||
frappe.throw(_("Issue detected with Mpesa configuration, check the error logs for more details"), title=_("Mpesa Express Error"))
|
frappe.throw(_("Issue detected with Mpesa configuration, check the error logs for more details"), title=_("Mpesa Express Error"))
|
||||||
|
|
||||||
|
def sanitize_mobile_number(number):
|
||||||
|
"""Add country code and strip leading zeroes from the phone number."""
|
||||||
|
return "254" + str(number).lstrip("0")
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def verify_transaction(**kwargs):
|
def verify_transaction(**kwargs):
|
||||||
""" Verify the transaction result received via callback """
|
""" Verify the transaction result received via callback from stk """
|
||||||
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
||||||
|
frappe.logger().debug(transaction_response)
|
||||||
|
|
||||||
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
||||||
request = frappe.get_doc("Integration Request", checkout_id)
|
request = frappe.get_doc("Integration Request", checkout_id)
|
||||||
@ -93,9 +102,13 @@ def verify_transaction(**kwargs):
|
|||||||
if transaction_response['ResultCode'] == 0:
|
if transaction_response['ResultCode'] == 0:
|
||||||
if transaction_data.reference_doctype and transaction_data.reference_docname:
|
if transaction_data.reference_doctype and transaction_data.reference_docname:
|
||||||
try:
|
try:
|
||||||
frappe.get_doc(transaction_data.reference_doctype,
|
doc = frappe.get_doc(transaction_data.reference_doctype,
|
||||||
transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed')
|
transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed')
|
||||||
request.process_response('error', transaction_response)
|
|
||||||
|
item_response = transaction_response["CallbackMetadata"]["Item"]
|
||||||
|
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
|
||||||
|
frappe.db.set_value("POS Invoice", doc.reference_docname, "mpesa_receipt_number", mpesa_receipt)
|
||||||
|
request.process_response('output', transaction_response)
|
||||||
except Exception:
|
except Exception:
|
||||||
request.process_response('error', transaction_response)
|
request.process_response('error', transaction_response)
|
||||||
frappe.log_error(frappe.get_traceback())
|
frappe.log_error(frappe.get_traceback())
|
||||||
@ -107,7 +120,7 @@ def verify_transaction(**kwargs):
|
|||||||
docname=transaction_data.reference_docname, user=request.owner, message=transaction_response)
|
docname=transaction_data.reference_docname, user=request.owner, message=transaction_response)
|
||||||
|
|
||||||
def get_account_balance(request_payload):
|
def get_account_balance(request_payload):
|
||||||
""" Call account balance API to send the request to the Mpesa Servers """
|
"""Call account balance API to send the request to the Mpesa Servers."""
|
||||||
try:
|
try:
|
||||||
mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname"))
|
mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname"))
|
||||||
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
||||||
@ -115,8 +128,7 @@ def get_account_balance(request_payload):
|
|||||||
app_key=mpesa_settings.consumer_key,
|
app_key=mpesa_settings.consumer_key,
|
||||||
app_secret=mpesa_settings.get_password("consumer_secret"))
|
app_secret=mpesa_settings.get_password("consumer_secret"))
|
||||||
|
|
||||||
# callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info"
|
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info"
|
||||||
callback_url = "https://b014ca8e7957.ngrok.io/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info"
|
|
||||||
|
|
||||||
response = connector.get_balance(mpesa_settings.initiator_name, mpesa_settings.security_credential, mpesa_settings.till_number, 4, mpesa_settings.name, callback_url, callback_url)
|
response = connector.get_balance(mpesa_settings.initiator_name, mpesa_settings.security_credential, mpesa_settings.till_number, 4, mpesa_settings.name, callback_url, callback_url)
|
||||||
return response
|
return response
|
||||||
@ -126,7 +138,8 @@ def get_account_balance(request_payload):
|
|||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def process_balance_info(**kwargs):
|
def process_balance_info(**kwargs):
|
||||||
|
"""Process and store account balance information received via callback
|
||||||
|
from the account balance API call."""
|
||||||
account_balance_response = frappe._dict(kwargs["Result"])
|
account_balance_response = frappe._dict(kwargs["Result"])
|
||||||
|
|
||||||
conversation_id = getattr(account_balance_response, "ConversationID", "")
|
conversation_id = getattr(account_balance_response, "ConversationID", "")
|
||||||
@ -141,10 +154,9 @@ def process_balance_info(**kwargs):
|
|||||||
if account_balance_response["ResultCode"] == 0:
|
if account_balance_response["ResultCode"] == 0:
|
||||||
try:
|
try:
|
||||||
result_params = account_balance_response["ResultParameters"]["ResultParameter"]
|
result_params = account_balance_response["ResultParameters"]["ResultParameter"]
|
||||||
for param in result_params:
|
|
||||||
if param["Key"] == "AccountBalance":
|
balance_info = fetch_param_value(result_params, "AccountBalance", "Key")
|
||||||
balance_info = param["Value"]
|
balance_info = convert_to_json(balance_info)
|
||||||
balance_info = convert_to_json(balance_info)
|
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname)
|
ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname)
|
||||||
ref_doc.db_set("account_balance", balance_info)
|
ref_doc.db_set("account_balance", balance_info)
|
||||||
@ -157,13 +169,28 @@ def process_balance_info(**kwargs):
|
|||||||
request.process_response('error', account_balance_response)
|
request.process_response('error', account_balance_response)
|
||||||
|
|
||||||
def convert_to_json(balance_info):
|
def convert_to_json(balance_info):
|
||||||
|
"""
|
||||||
|
Convert 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'}
|
||||||
|
"""
|
||||||
balance_dict = frappe._dict()
|
balance_dict = frappe._dict()
|
||||||
for account_info in balance_info.split("&"):
|
for account_info in balance_info.split("&"):
|
||||||
account_info = account_info.split('|')
|
account_info = account_info.split('|')
|
||||||
balance_dict[account_info[0]] = dict(
|
balance_dict[account_info[0]] = dict(
|
||||||
current_balance=account_info[2],
|
current_balance=fmt_money(account_info[2], currency="KES"),
|
||||||
available_balance=account_info[3],
|
available_balance=fmt_money(account_info[3], currency="KES"),
|
||||||
reserved_balance=account_info[4],
|
reserved_balance=fmt_money(account_info[4], currency="KES"),
|
||||||
uncleared_balance=account_info[5]
|
uncleared_balance=fmt_money(account_info[5], currency="KES")
|
||||||
)
|
)
|
||||||
return dumps(balance_dict)
|
return dumps(balance_dict)
|
||||||
|
|
||||||
|
def fetch_param_value(response, key, key_field):
|
||||||
|
"""Fetch the specified key from list of dictionary. Key is identified via the key field"""
|
||||||
|
for param in response:
|
||||||
|
if param[key_field] == key:
|
||||||
|
return param["Value"]
|
Loading…
x
Reference in New Issue
Block a user