fix: handle api changes from callbacks
This commit is contained in:
parent
a3ac4bf681
commit
97ab96c8bf
@ -83,8 +83,17 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
elif self.payment_channel == "Phone":
|
elif self.payment_channel == "Phone":
|
||||||
controller = get_payment_gateway_controller(self.payment_gateway)
|
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||||
print(vars(self))
|
payment_record = dict(
|
||||||
controller.request_for_payment(**vars(self))
|
reference_doctype=self.reference_doctype,
|
||||||
|
reference_docname=self.reference_name,
|
||||||
|
grand_total=self.grand_total,
|
||||||
|
sender=self.email_to,
|
||||||
|
payment_request_name=self.name,
|
||||||
|
currency=self.currency,
|
||||||
|
payment_gateway=self.payment_gateway
|
||||||
|
)
|
||||||
|
controller.validate_transaction_currency(self.currency)
|
||||||
|
controller.request_for_payment(**payment_record)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.check_if_payment_entry_exists()
|
self.check_if_payment_entry_exists()
|
||||||
@ -354,7 +363,6 @@ def make_payment_request(**args):
|
|||||||
def get_amount(ref_doc, payment_account=None):
|
def get_amount(ref_doc, payment_account=None):
|
||||||
"""get amount based on doctype"""
|
"""get amount based on doctype"""
|
||||||
dt = ref_doc.doctype
|
dt = ref_doc.doctype
|
||||||
print(dt)
|
|
||||||
if dt in ["Sales Order", "Purchase Order"]:
|
if dt in ["Sales Order", "Purchase Order"]:
|
||||||
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
|
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
|
||||||
|
|
||||||
|
@ -794,7 +794,7 @@ def get_children(doctype, parent, company, is_root=False):
|
|||||||
|
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
def create_payment_gateway_account(gateway):
|
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||||
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
||||||
|
|
||||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||||
@ -829,7 +829,8 @@ def create_payment_gateway_account(gateway):
|
|||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"payment_gateway": gateway,
|
"payment_gateway": gateway,
|
||||||
"payment_account": bank_account.name,
|
"payment_account": bank_account.name,
|
||||||
"currency": bank_account.account_currency
|
"currency": bank_account.account_currency,
|
||||||
|
"payment_channel": payment_channel
|
||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import base64
|
||||||
import requests
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
class MpesaConnector():
|
class MpesaConnector():
|
||||||
@ -7,6 +9,110 @@ class MpesaConnector():
|
|||||||
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
|
||||||
self.sandbox_url = sandbox_url
|
if env == "sandbox":
|
||||||
self.live_url = live_url
|
self.base_url = sandbox_url
|
||||||
|
else:
|
||||||
|
self.base_url = live_url
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
"""
|
||||||
|
To make Mpesa API calls, you will need to authenticate your app. This method is used to fetch the access token
|
||||||
|
required by Mpesa. Mpesa supports client_credentials grant type. To authorize your API calls to Mpesa,
|
||||||
|
you will need a Basic Auth over HTTPS authorization token. The Basic Auth string is a base64 encoded string
|
||||||
|
of your app's client key and client secret.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa.
|
||||||
|
"""
|
||||||
|
authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials"
|
||||||
|
authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri)
|
||||||
|
r = requests.get(
|
||||||
|
authenticate_url,
|
||||||
|
auth=HTTPBasicAuth(self.app_key, self.app_secret)
|
||||||
|
)
|
||||||
|
self.authentication_token = r.json()['access_token']
|
||||||
|
return r.json()['access_token']
|
||||||
|
|
||||||
|
def get_balance(self, initiator=None, security_credential=None, party_a=None, identifier_type=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).
|
||||||
|
Args:
|
||||||
|
initiator (str): Username used to authenticate the transaction.
|
||||||
|
security_credential (str): Generate from developer portal.
|
||||||
|
command_id (str): AccountBalance.
|
||||||
|
party_a (int): Till number being queried.
|
||||||
|
identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code)
|
||||||
|
remarks (str): Comments that are sent along with the transaction(maximum 100 characters).
|
||||||
|
queue_timeout_url (str): The url that handles information of timed out transactions.
|
||||||
|
result_url (str): The url that receives results from M-Pesa api call.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OriginatorConverstionID (str): The unique request ID for tracking a transaction.
|
||||||
|
ConversationID (str): The unique request ID returned by mpesa for each request made
|
||||||
|
ResponseDescription (str): Response Description message
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Initiator": initiator,
|
||||||
|
"SecurityCredential": security_credential,
|
||||||
|
"CommandID": "AccountBalance",
|
||||||
|
"PartyA": party_a,
|
||||||
|
"IdentifierType": identifier_type,
|
||||||
|
"Remarks": remarks,
|
||||||
|
"QueueTimeOutURL": queue_timeout_url,
|
||||||
|
"ResultURL": result_url
|
||||||
|
}
|
||||||
|
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
|
||||||
|
saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query")
|
||||||
|
r = requests.post(saf_url, headers=headers, json=payload)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def stk_push(self, business_shortcode=None, passcode=None, amount=None, callback_url=None, reference_code=None,
|
||||||
|
phone_number=None, description=None):
|
||||||
|
"""
|
||||||
|
This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
|
||||||
|
Args:
|
||||||
|
business_shortcode (int): The short code of the organization.
|
||||||
|
passcode (str): Get from developer portal
|
||||||
|
amount (int): The amount being transacted
|
||||||
|
callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API.
|
||||||
|
reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type.
|
||||||
|
phone_number(int): The Mobile Number to receive the STK Pin Prompt.
|
||||||
|
description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters
|
||||||
|
|
||||||
|
Success Response:
|
||||||
|
CustomerMessage(str): Messages that customers can understand.
|
||||||
|
CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request.
|
||||||
|
ResponseDescription(str): Describes Success or failure
|
||||||
|
MerchantRequestID(str): This is a global unique Identifier for any submitted payment request.
|
||||||
|
ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03
|
||||||
|
|
||||||
|
Error Reponse:
|
||||||
|
requestId(str): This is a unique requestID for the payment request
|
||||||
|
errorCode(str): This is a predefined code that indicates the reason for request failure.
|
||||||
|
errorMessage(str): This is a predefined code that indicates the reason for request failure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
time = str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
|
||||||
|
password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time)
|
||||||
|
encoded = base64.b64encode(bytes(password, encoding='utf8'))
|
||||||
|
payload = {
|
||||||
|
"BusinessShortCode": business_shortcode,
|
||||||
|
"Password": encoded.decode("utf-8"),
|
||||||
|
"Timestamp": time,
|
||||||
|
"TransactionType": "CustomerPayBillOnline",
|
||||||
|
"Amount": amount,
|
||||||
|
"PartyA": int(phone_number),
|
||||||
|
"PartyB": business_shortcode,
|
||||||
|
"PhoneNumber": int(phone_number),
|
||||||
|
"CallBackURL": callback_url,
|
||||||
|
"AccountReference": reference_code,
|
||||||
|
"TransactionDesc": description
|
||||||
|
}
|
||||||
|
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
|
||||||
|
|
||||||
|
saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest")
|
||||||
|
r = requests.post(saf_url, headers=headers, json=payload)
|
||||||
|
return r.json()
|
@ -3,6 +3,5 @@
|
|||||||
|
|
||||||
frappe.ui.form.on('Mpesa Settings', {
|
frappe.ui.form.on('Mpesa Settings', {
|
||||||
// refresh: function(frm) {
|
// refresh: function(frm) {
|
||||||
|
|
||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
@ -15,17 +15,13 @@ from frappe.utils import get_url, call_hook_method, cint, flt, cstr
|
|||||||
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.password import get_decrypted_password
|
from frappe.utils.password import get_decrypted_password
|
||||||
|
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
|
||||||
|
|
||||||
class MpesaSettings(Document):
|
class MpesaSettings(Document):
|
||||||
supported_currencies = ["KSh"]
|
supported_currencies = ["KES"]
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
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)
|
|
||||||
call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name)
|
|
||||||
|
|
||||||
def validate_transaction_currency(self, currency):
|
def validate_transaction_currency(self, currency):
|
||||||
if currency not in self.supported_currencies:
|
if currency not in self.supported_currencies:
|
||||||
@ -33,3 +29,71 @@ 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_mode_of_payment('Mpesa-' + self.payment_gateway_name)
|
||||||
|
call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name, payment_channel="Phone")
|
||||||
|
|
||||||
|
def request_for_payment(self, **kwargs):
|
||||||
|
response = frappe._dict(generate_stk_push(**kwargs))
|
||||||
|
# check error response
|
||||||
|
if hasattr(response, "requestId"):
|
||||||
|
req_name = getattr(response, "requestId")
|
||||||
|
error = response
|
||||||
|
else:
|
||||||
|
# global checkout id used as request name
|
||||||
|
req_name = getattr(response, "CheckoutRequestID")
|
||||||
|
error = None
|
||||||
|
|
||||||
|
create_request_log(kwargs, "Host", "Mpesa", req_name, error)
|
||||||
|
if error:
|
||||||
|
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
|
||||||
|
|
||||||
|
def generate_stk_push(**kwargs):
|
||||||
|
args = frappe._dict(kwargs)
|
||||||
|
try:
|
||||||
|
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
|
||||||
|
|
||||||
|
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
|
||||||
|
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
||||||
|
|
||||||
|
connector = MpesaConnector(env=env,
|
||||||
|
app_key=mpesa_settings.consumer_key,
|
||||||
|
app_secret=mpesa_settings.get_password("consumer_secret"))
|
||||||
|
|
||||||
|
response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
|
||||||
|
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
|
||||||
|
callback_url=callback_url, reference_code=args.payment_request_name,
|
||||||
|
phone_number=args.sender, description="POS Payment")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
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.whitelist(allow_guest=True)
|
||||||
|
def verify_transaction(**kwargs):
|
||||||
|
""" Verify the transaction result received via callback """
|
||||||
|
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
||||||
|
|
||||||
|
checkout_id = getattr(transaction_response, "CheckoutRequestID")
|
||||||
|
request = frappe.get_doc("Integration Request", checkout_id)
|
||||||
|
transaction_data = frappe._dict(json.loads(request.data))
|
||||||
|
|
||||||
|
if transaction_response['ResultCode'] == 0:
|
||||||
|
if transaction_data.reference_doctype and transaction_data.reference_docname:
|
||||||
|
try:
|
||||||
|
frappe.get_doc(transaction_data.reference_doctype,
|
||||||
|
transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed')
|
||||||
|
request.db_set('output', transaction_response)
|
||||||
|
request.db_set('status', 'Completed')
|
||||||
|
except Exception:
|
||||||
|
request.db_set('error', transaction_response)
|
||||||
|
request.db_set('status', 'Failed')
|
||||||
|
frappe.log_error(frappe.get_traceback())
|
||||||
|
|
||||||
|
else:
|
||||||
|
request.db_set('error', transaction_response)
|
||||||
|
request.db_set('status', 'Failed')
|
||||||
|
|
||||||
|
frappe.publish_realtime('process_phone_payment', after_commit=True, user=request.owner, message=transaction_response)
|
Loading…
x
Reference in New Issue
Block a user