feat(pos): mpesa related fixes & additions (#24306)

* fix: switching of mode of payments

* feat: transaction limit for mpesa integration

* feat: resend payment request if one fails

* feat: make new request only for failed ones

* fix: invalid amount for mpesa request

* fix: payment successful message not shown

* fix: url and business shortcode for live env

* fix: duplicate items validation for pos invoices

* fix: pos closing entry queued status

* fix: peroid end date for amended pos closing
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 1b97050..53ac996 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -3,6 +3,7 @@
 # For license information, please see license.txt
 
 from __future__ import unicode_literals
+import json
 import frappe
 from frappe import _
 from frappe.model.document import Document
@@ -82,18 +83,37 @@
 			self.make_communication_entry()
 
 		elif self.payment_channel == "Phone":
-			controller = get_payment_gateway_controller(self.payment_gateway)
-			payment_record = dict(
-				reference_doctype="Payment Request",
-				reference_docname=self.name,
-				payment_reference=self.reference_name,
-				grand_total=self.grand_total,
-				sender=self.email_to,
-				currency=self.currency,
-				payment_gateway=self.payment_gateway
-			)
-			controller.validate_transaction_currency(self.currency)
-			controller.request_for_payment(**payment_record)
+			self.request_phone_payment()
+
+	def request_phone_payment(self):
+		controller = get_payment_gateway_controller(self.payment_gateway)
+		request_amount = self.get_request_amount()
+
+		payment_record = dict(
+			reference_doctype="Payment Request",
+			reference_docname=self.name,
+			payment_reference=self.reference_name,
+			request_amount=request_amount,
+			sender=self.email_to,
+			currency=self.currency,
+			payment_gateway=self.payment_gateway
+		)
+
+		controller.validate_transaction_currency(self.currency)
+		controller.request_for_payment(**payment_record)
+	
+	def get_request_amount(self):
+		data_of_completed_requests = frappe.get_all("Integration Request", filters={
+			'reference_doctype': self.doctype,
+			'reference_docname': self.name,
+			'status': 'Completed'
+		}, pluck="data")
+
+		if not data_of_completed_requests:
+			return self.grand_total
+
+		request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
+		return request_amounts
 
 	def on_cancel(self):
 		self.check_if_payment_entry_exists()
@@ -351,8 +371,8 @@
 		if args.order_type == "Shopping Cart" or args.mute_email:
 			pr.flags.mute_email = True
 
+		pr.insert(ignore_permissions=True)
 		if args.submit_doc:
-			pr.insert(ignore_permissions=True)
 			pr.submit()
 
 	if args.order_type == "Shopping Cart":
@@ -412,8 +432,8 @@
 
 def get_gateway_details(args):
 	"""return gateway and payment account of default payment gateway"""
-	if args.get("payment_gateway"):
-		return get_payment_gateway_account(args.get("payment_gateway"))
+	if args.get("payment_gateway_account"):
+		return get_payment_gateway_account(args.get("payment_gateway_account"))
 
 	if args.order_type == "Shopping Cart":
 		payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 8a10e2c..5eba62c 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -45,7 +45,8 @@
 
 	def test_payment_request_linkings(self):
 		so_inr = make_sales_order(currency="INR")
-		pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com")
+		pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
+			payment_gateway_account="_Test Gateway - INR")
 
 		self.assertEqual(pr.reference_doctype, "Sales Order")
 		self.assertEqual(pr.reference_name, so_inr.name)
@@ -54,7 +55,8 @@
 		conversion_rate = get_exchange_rate("USD", "INR")
 
 		si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate)
-		pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com")
+		pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
+			payment_gateway_account="_Test Gateway - USD")
 
 		self.assertEqual(pr.reference_doctype, "Sales Invoice")
 		self.assertEqual(pr.reference_name, si_usd.name)
@@ -68,7 +70,7 @@
 
 		so_inr = make_sales_order(currency="INR")
 		pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
-			mute_email=1, submit_doc=1, return_doc=1)
+			mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1)
 		pe = pr.set_as_paid()
 
 		so_inr = frappe.get_doc("Sales Order", so_inr.name)
@@ -79,7 +81,7 @@
 			currency="USD", conversion_rate=50)
 
 		pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
-			mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1)
+			mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
 
 		pe = pr.set_as_paid()
 
@@ -106,7 +108,7 @@
 			currency="USD", conversion_rate=50)
 
 		pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
-			mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1)
+			mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
 
 		pe = pr.create_payment_entry()
 		pr.load_from_db()
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index 57baac7..e79eb42 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -20,7 +20,7 @@
 			return { filters: { 'status': 'Open', 'docstatus': 1 } };
 		});
 		
-		if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime());
+		if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime());
 		if (frm.doc.docstatus === 1) set_html_data(frm);
 	},
 
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 86062d1..6a7c4be 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -187,18 +187,43 @@
 	},
 
 	request_for_payment: function (frm) {
+		if (!frm.doc.contact_mobile) {
+			frappe.throw(__('Please enter mobile number first.'));
+		}
+		frm.dirty();
 		frm.save().then(() => {
-			frappe.dom.freeze();
-			frappe.call({
-				method: 'create_payment_request',
-				doc: frm.doc,
-			})
+			frappe.dom.freeze(__('Waiting for payment...'));
+			frappe
+				.call({
+					method: 'create_payment_request',
+					doc: frm.doc
+				})
 				.fail(() => {
 					frappe.dom.unfreeze();
-					frappe.msgprint('Payment request failed');
+					frappe.msgprint(__('Payment request failed'));
 				})
-				.then(() => {
-					frappe.msgprint('Payment request sent successfully');
+				.then(({ message }) => {
+					const payment_request_name = message.name;
+					setTimeout(() => {
+						frappe.db.get_value('Payment Request', payment_request_name, ['status', 'grand_total']).then(({ message }) => {
+							if (message.status != 'Paid') {
+								frappe.dom.unfreeze();
+								frappe.msgprint({
+									message: __('Payment Request took too long to respond. Please try requesting for payment again.'),
+									title: __('Request Timeout')
+								});
+							} else if (frappe.dom.freeze_count != 0) {
+								frappe.dom.unfreeze();
+								cur_frm.reload_doc();
+								cur_pos.payment.events.submit_invoice();
+
+								frappe.show_alert({
+									message: __("Payment of {0} received successfully.", [format_currency(message.grand_total, frm.doc.currency, 0)]),
+									indicator: 'green'
+								});
+							}
+						});
+					}, 60000);
 				});
 		});
 	}
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index bd664c5..f67cae3 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -363,22 +363,48 @@
 				if not self.contact_mobile:
 					frappe.throw(_("Please enter the phone number first"))
 
-				payment_gateway = frappe.db.get_value("Payment Gateway Account", {
-					"payment_account": pay.account,
-				})
-				record = {
-					"payment_gateway": payment_gateway,
-					"dt": "POS Invoice",
-					"dn": self.name,
-					"payment_request_type": "Inward",
-					"party_type": "Customer",
-					"party": self.customer,
-					"mode_of_payment": pay.mode_of_payment,
-					"recipient_id": self.contact_mobile,
-					"submit_doc": True
-				}
+				pay_req = self.get_existing_payment_request(pay)
+				if not pay_req:
+					pay_req = self.get_new_payment_request(pay)
+					pay_req.submit()
+				else:
+					pay_req.request_phone_payment()
 
-				return make_payment_request(**record)
+				return pay_req
+	
+	def get_new_payment_request(self, mop):
+		payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
+			"payment_account": mop.account,
+		}, ["name"])
+
+		args = {
+			"dt": "POS Invoice",
+			"dn": self.name,
+			"recipient_id": self.contact_mobile,
+			"mode_of_payment": mop.mode_of_payment,
+			"payment_gateway_account": payment_gateway_account,
+			"payment_request_type": "Inward",
+			"party_type": "Customer",
+			"party": self.customer,
+			"return_doc": True
+		}
+		return make_payment_request(**args)
+
+	def get_existing_payment_request(self, pay):
+		payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
+			"payment_account": pay.account,
+		}, ["name"])
+
+		args = {
+			'doctype': 'Payment Request',
+			'reference_doctype': 'POS Invoice',
+			'reference_name': self.name,
+			'payment_gateway_account': payment_gateway_account,
+			'email_to': self.contact_mobile
+		}
+		pr = frappe.db.exists(args)
+		if pr:
+			return frappe.get_doc('Payment Request', pr[0][0])
 
 def add_return_modes(doc, pos_profile):
 	def append_payment(payment_mode):
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index e085048..aa7b27a 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -456,9 +456,13 @@
 		check_list, chk_dupl_itm = [], []
 		if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
 			return
+		if self.doctype == "Sales Invoice" and self.is_consolidated:
+			return
+		if self.doctype == "POS Invoice":
+			return
 
 		for d in self.get('items'):
-			if self.doctype in ["POS Invoice","Sales Invoice"]:
+			if self.doctype == "Sales Invoice":
 				stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
 				non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
 			elif self.doctype == "Delivery Note":
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
index d33b0a7..554c6b0 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
@@ -5,7 +5,7 @@
 
 class MpesaConnector():
 	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://api.safaricom.co.ke"):
 		"""Setup configuration for Mpesa connector and generate new access token."""
 		self.env = env
 		self.app_key = app_key
@@ -102,14 +102,14 @@
 			"BusinessShortCode": business_shortcode,
 			"Password": encoded.decode("utf-8"),
 			"Timestamp": time,
-			"TransactionType": "CustomerPayBillOnline",
 			"Amount": amount,
 			"PartyA": int(phone_number),
-			"PartyB": business_shortcode,
+			"PartyB": reference_code,
 			"PhoneNumber": int(phone_number),
 			"CallBackURL": callback_url,
 			"AccountReference": reference_code,
-			"TransactionDesc": description
+			"TransactionDesc": description,
+			"TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline"
 		}
 		headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
 
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
index fc7b310..407f826 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
@@ -11,8 +11,10 @@
   "consumer_secret",
   "initiator_name",
   "till_number",
+  "transaction_limit",
   "sandbox",
   "column_break_4",
+  "business_shortcode",
   "online_passkey",
   "security_credential",
   "get_account_balance",
@@ -84,10 +86,24 @@
    "fieldname": "get_account_balance",
    "fieldtype": "Button",
    "label": "Get Account Balance"
+  },
+  {
+   "depends_on": "eval:(doc.sandbox==0)",
+   "fieldname": "business_shortcode",
+   "fieldtype": "Data",
+   "label": "Business Shortcode",
+   "mandatory_depends_on": "eval:(doc.sandbox==0)"
+  },
+  {
+   "default": "150000",
+   "fieldname": "transaction_limit",
+   "fieldtype": "Float",
+   "label": "Transaction Limit",
+   "non_negative": 1
   }
  ],
  "links": [],
- "modified": "2020-09-25 20:21:38.215494",
+ "modified": "2021-01-29 12:02:16.106942",
  "modified_by": "Administrator",
  "module": "ERPNext Integrations",
  "name": "Mpesa Settings",
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
index 1cad84d..b571802 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
@@ -33,13 +33,34 @@
 		create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
 
 	def request_for_payment(self, **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))
+		args = frappe._dict(kwargs)
+		request_amounts = self.split_request_amount_according_to_transaction_limit(args)
 
-		self.handle_api_response("CheckoutRequestID", kwargs, response)
+		for i, amount in enumerate(request_amounts):
+			args.request_amount = amount
+			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(amount))
+			else:
+				response = frappe._dict(generate_stk_push(**args))
+
+			self.handle_api_response("CheckoutRequestID", args, response)
+
+	def split_request_amount_according_to_transaction_limit(self, args):
+		request_amount = args.request_amount
+		if request_amount > self.transaction_limit:
+			# make multiple requests
+			request_amounts = []
+			requests_to_be_made = frappe.utils.ceil(request_amount / self.transaction_limit) # 480/150 = ceil(3.2) = 4
+			for i in range(requests_to_be_made):
+				amount = self.transaction_limit
+				if i == requests_to_be_made - 1:
+					amount = request_amount - (self.transaction_limit * i) # for 4th request, 480 - (150 * 3) = 30
+				request_amounts.append(amount)
+		else:
+			request_amounts = [request_amount]
+		
+		return request_amounts
 
 	def get_account_balance_info(self):
 		payload = dict(
@@ -67,7 +88,8 @@
 			req_name = getattr(response, global_id)
 			error = None
 
-		create_request_log(request_dict, "Host", "Mpesa", req_name, error)
+		if not frappe.db.exists('Integration Request', req_name):
+			create_request_log(request_dict, "Host", "Mpesa", req_name, error)
 
 		if error:
 			frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
@@ -80,6 +102,8 @@
 
 		mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
 		env = "production" if not mpesa_settings.sandbox else "sandbox"
+		# for sandbox, business shortcode is same as till number
+		business_shortcode = mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number
 
 		connector = MpesaConnector(env=env,
 			app_key=mpesa_settings.consumer_key,
@@ -87,10 +111,12 @@
 
 		mobile_number = sanitize_mobile_number(args.sender)
 
-		response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
-			passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
+		response = connector.stk_push(
+			business_shortcode=business_shortcode, amount=args.request_amount,
+			passcode=mpesa_settings.get_password("online_passkey"),
 			callback_url=callback_url, reference_code=mpesa_settings.till_number,
-			phone_number=mobile_number, description="POS Payment")
+			phone_number=mobile_number, description="POS Payment"
+		)
 
 		return response
 
@@ -108,29 +134,72 @@
 	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(loads(request.data))
+	integration_request = frappe.get_doc("Integration Request", checkout_id)
+	transaction_data = frappe._dict(loads(integration_request.data))
+	total_paid = 0 # for multiple integration request made against a pos invoice
+	success = False # for reporting successfull callback to point of sale ui
 
 	if transaction_response['ResultCode'] == 0:
-		if request.reference_doctype and request.reference_docname:
+		if integration_request.reference_doctype and integration_request.reference_docname:
 			try:
-				doc = frappe.get_doc(request.reference_doctype,
-					request.reference_docname)
-				doc.run_method("on_payment_authorized", 'Completed')
-
 				item_response = transaction_response["CallbackMetadata"]["Item"]
+				amount = fetch_param_value(item_response, "Amount", "Name")
 				mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
-				frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt)
-				request.handle_success(transaction_response)
+				pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname)
+
+				mpesa_receipts, completed_payments = get_completed_integration_requests_info(
+					integration_request.reference_doctype,
+					integration_request.reference_docname,
+					checkout_id
+				)
+
+				total_paid = amount + sum(completed_payments)
+				mpesa_receipts = ', '.join(mpesa_receipts + [mpesa_receipt])
+
+				if total_paid >= pr.grand_total:
+					pr.run_method("on_payment_authorized", 'Completed')
+					success = True
+
+				frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts)
+				integration_request.handle_success(transaction_response)
 			except Exception:
-				request.handle_failure(transaction_response)
+				integration_request.handle_failure(transaction_response)
 				frappe.log_error(frappe.get_traceback())
 
 	else:
-		request.handle_failure(transaction_response)
+		integration_request.handle_failure(transaction_response)
 
-	frappe.publish_realtime('process_phone_payment', doctype="POS Invoice",
-		docname=transaction_data.payment_reference, user=request.owner, message=transaction_response)
+	frappe.publish_realtime(
+		event='process_phone_payment',
+		doctype="POS Invoice",
+		docname=transaction_data.payment_reference,
+		user=integration_request.owner,
+		message={
+			'amount': total_paid,
+			'success': success,
+			'failure_message': transaction_response["ResultDesc"] if transaction_response['ResultCode'] != 0 else ''
+		},
+	)
+
+def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id):
+	output_of_other_completed_requests = frappe.get_all("Integration Request", filters={
+		'name': ['!=', checkout_id],
+		'reference_doctype': reference_doctype,
+		'reference_docname': reference_docname,
+		'status': 'Completed'
+	}, pluck="output")
+
+	mpesa_receipts, completed_payments = [], []
+
+	for out in output_of_other_completed_requests:
+		out = frappe._dict(loads(out))
+		item_response = out["CallbackMetadata"]["Item"]
+		completed_amount = fetch_param_value(item_response, "Amount", "Name")
+		completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
+		completed_payments.append(completed_amount)
+		mpesa_receipts.append(completed_mpesa_receipt)
+	
+	return mpesa_receipts, completed_payments
 
 def get_account_balance(request_payload):
 	"""Call account balance API to send the request to the Mpesa Servers."""
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 4e86d36..18d2732 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
@@ -9,6 +9,10 @@
 from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
 
 class TestMpesaSettings(unittest.TestCase):
+	def tearDown(self):
+		frappe.db.sql('delete from `tabMpesa Settings`')
+		frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
+
 	def test_creation_of_payment_gateway(self):
 		create_mpesa_settings(payment_gateway_name="_Test")
 
@@ -40,6 +44,8 @@
 			}
 		}))
 
+		integration_request.delete()
+
 	def test_processing_of_callback_payload(self):
 		create_mpesa_settings(payment_gateway_name="Payment")
 		mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
@@ -55,10 +61,16 @@
 		# test payment request creation
 		self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
 
-		callback_response = get_payment_callback_payload()
+		# submitting payment request creates integration requests with random id
+		integration_req_ids = frappe.get_all("Integration Request", filters={
+			'reference_doctype': pr.doctype,
+			'reference_docname': pr.name,
+		}, pluck="name")
+
+		callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0])
 		verify_transaction(**callback_response)
 		# test creation of integration request
-		integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972")
+		integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
 
 		# test integration request creation and successful update of the status on receiving callback response
 		self.assertTrue(integration_request)
@@ -68,6 +80,120 @@
 		integration_request.reload()
 		self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
 		self.assertEquals(integration_request.status, "Completed")
+		
+		frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
+		integration_request.delete()
+		pr.reload()
+		pr.cancel()
+		pr.delete()
+		pos_invoice.delete()
+
+	def test_processing_of_multiple_callback_payload(self):
+		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")
+		frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
+		frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
+
+		pos_invoice = create_pos_invoice(do_not_submit=1)
+		pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
+		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")
+
+		# submitting payment request creates integration requests with random id
+		integration_req_ids = frappe.get_all("Integration Request", filters={
+			'reference_doctype': pr.doctype,
+			'reference_docname': pr.name,
+		}, pluck="name")
+
+		# create random receipt nos and send it as response to callback handler
+		mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
+
+		integration_requests = []
+		for i in range(len(integration_req_ids)):
+			callback_response = get_payment_callback_payload(
+				Amount=500,
+				CheckoutRequestID=integration_req_ids[i],
+				MpesaReceiptNumber=mpesa_receipt_numbers[i]
+			)
+			# handle response manually
+			verify_transaction(**callback_response)
+			# test completion of integration request
+			integration_request = frappe.get_doc("Integration Request", integration_req_ids[i])
+			self.assertEquals(integration_request.status, "Completed")
+			integration_requests.append(integration_request)
+
+		# check receipt number once all the integration requests are completed
+		pos_invoice.reload()
+		self.assertEquals(pos_invoice.mpesa_receipt_number, ', '.join(mpesa_receipt_numbers))
+
+		frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
+		[d.delete() for d in integration_requests]
+		pr.reload()
+		pr.cancel()
+		pr.delete()
+		pos_invoice.delete()
+	
+	def test_processing_of_only_one_succes_callback_payload(self):
+		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")
+		frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
+		frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
+
+		pos_invoice = create_pos_invoice(do_not_submit=1)
+		pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
+		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")
+
+		# submitting payment request creates integration requests with random id
+		integration_req_ids = frappe.get_all("Integration Request", filters={
+			'reference_doctype': pr.doctype,
+			'reference_docname': pr.name,
+		}, pluck="name")
+
+		# create random receipt nos and send it as response to callback handler
+		mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
+
+		callback_response = get_payment_callback_payload(
+			Amount=500,
+			CheckoutRequestID=integration_req_ids[0],
+			MpesaReceiptNumber=mpesa_receipt_numbers[0]
+		)
+		# handle response manually
+		verify_transaction(**callback_response)
+		# test completion of integration request
+		integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
+		self.assertEquals(integration_request.status, "Completed")
+
+		# now one request is completed
+		# second integration request fails
+		# now retrying payment request should make only one integration request again
+		pr = pos_invoice.create_payment_request()
+		new_integration_req_ids = frappe.get_all("Integration Request", filters={
+			'reference_doctype': pr.doctype,
+			'reference_docname': pr.name,
+			'name': ['not in', integration_req_ids]
+		}, pluck="name")
+
+		self.assertEquals(len(new_integration_req_ids), 1)
+
+		frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
+		frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
+		pr.reload()
+		pr.cancel()
+		pr.delete()
+		pos_invoice.delete()
 
 def create_mpesa_settings(payment_gateway_name="Express"):
 	if frappe.db.exists("Mpesa Settings", payment_gateway_name):
@@ -157,16 +283,19 @@
 	}
 		}
 
-def get_payment_request_response_payload():
+def get_payment_request_response_payload(Amount=500):
 	"""Response received after successfully calling the stk push process request API."""
+
+	CheckoutRequestID = frappe.utils.random_string(10)
+
 	return {
 		"MerchantRequestID": "8071-27184008-1",
-		"CheckoutRequestID": "ws_CO_061020201133231972",
+		"CheckoutRequestID": CheckoutRequestID,
 		"ResultCode": 0,
 		"ResultDesc": "The service request is processed successfully.",
 		"CallbackMetadata": {
 			"Item": [
-				{ "Name": "Amount", "Value": 500.0 },
+				{ "Name": "Amount", "Value": Amount },
 				{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
 				{ "Name": "TransactionDate", "Value": 20201006113336 },
 				{ "Name": "PhoneNumber", "Value": 254723575670 }
@@ -174,41 +303,26 @@
 		}
 	}
 
-
-def get_payment_callback_payload():
+def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"):
 	"""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
+			"stkCallback":{
+				"MerchantRequestID":"19465-780693-1",
+				"CheckoutRequestID":CheckoutRequestID,
+				"ResultCode":0,
+				"ResultDesc":"The service request is processed successfully.",
+				"CallbackMetadata":{
+					"Item":[
+						{ "Name":"Amount", "Value":Amount },
+						{ "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber },
+						{ "Name":"Balance" },
+						{ "Name":"TransactionDate", "Value":20170727154800 },
+						{ "Name":"PhoneNumber", "Value":254721566839 }
+					]
 				}
-			]
 			}
 		}
-		}
 	}
 
 def get_account_balance_callback_payload():
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 3938300..03d99c6 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -482,8 +482,12 @@
 		this.render_net_total(frm.doc.base_net_total);
 		this.render_grand_total(frm.doc.base_grand_total);
 
-		const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }})
-		this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes);
+		const taxes = frm.doc.taxes.map(t => {
+			return {
+				description: t.description, rate: t.rate
+			};
+		});
+		this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
 	}
 	
 	render_net_total(value) {
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index e4d8965..e150271 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -168,30 +168,22 @@
 				me.toggle_numpad(true);
 
 				me.selected_mode = me[`${mode}_control`];
-				const doc = me.events.get_frm().doc;
-				me.selected_mode?.$input?.get(0).focus();
-				const current_value = me.selected_mode?.get_value()
-				!current_value && doc.grand_total > doc.paid_amount ? me.selected_mode?.set_value(doc.grand_total - doc.paid_amount) : '';
+				me.selected_mode && me.selected_mode.$input.get(0).focus();
+				me.auto_set_remaining_amount();
 			}
 		})
 
-		frappe.realtime.on("process_phone_payment", function(data) {
-			frappe.dom.unfreeze();
-			cur_frm.reload_doc();
-			let message = data["ResultDesc"];
-			let title = __("Payment Failed");
+		frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => {
+			const contact = frm.doc.contact_mobile;
+			const request_button = $(this.request_for_payment_field.$input[0]);
+			if (contact) {
+				request_button.removeClass('btn-default').addClass('btn-primary');
+			} else {
+				request_button.removeClass('btn-primary').addClass('btn-default');
+      		}
+    	});
 
-			if (data["ResultCode"] == 0) {
-				title = __("Payment Received");
-				$('.btn.btn-xs.btn-default[data-fieldname=request_for_payment]').html(`Payment Received`)
-				me.events.submit_invoice();
-			}
-
-			frappe.msgprint({
-				"message": message,
-				"title": title
-			});
-		});
+		this.setup_listener_for_payments();
 
 		this.$payment_modes.on('click', '.shortcut', function(e) {
 			const value = $(this).attr('data-value');
@@ -250,6 +242,41 @@
 		})
 	}
 
+	setup_listener_for_payments() {
+		frappe.realtime.on("process_phone_payment", (data) => {
+			const doc = this.events.get_frm().doc;
+			const { response, amount, success, failure_message } = data;
+			let message, title;
+
+			if (success) {
+				title = __("Payment Received");
+				if (amount >= doc.grand_total) {
+					frappe.dom.unfreeze();
+					message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
+					this.events.submit_invoice();
+					cur_frm.reload_doc();
+
+				} else {
+					message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]);
+				}
+			} else if (failure_message) {
+				message = failure_message;
+				title = __("Payment Failed");
+			}
+
+			frappe.msgprint({ "message": message, "title": title });
+		});
+	}
+
+	auto_set_remaining_amount() {
+		const doc = this.events.get_frm().doc;
+		const remaining_amount = doc.grand_total - doc.paid_amount;
+		const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
+		if (!current_value && remaining_amount > 0 && this.selected_mode) {
+			this.selected_mode.set_value(remaining_amount);
+		}
+	}
+
 	attach_shortcuts() {
 		const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
 		this.$component.find('.submit-order').attr("title", `${ctrl_label}+Enter`);
@@ -370,9 +397,11 @@
 					fieldtype: 'Currency',
 					placeholder: __('Enter {0} amount.', [p.mode_of_payment]),
 					onchange: function() {
-						if (this.value || this.value == 0) {
-							frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value))
-								.then(() => me.update_totals_section());
+						const current_value = frappe.model.get_value(p.doctype, p.name, 'amount');
+						if (current_value != this.value) {
+							frappe.model
+								.set_value(p.doctype, p.name, 'amount', flt(this.value))
+								.then(() => me.update_totals_section())
 
 							const formatted_currency = format_currency(this.value, currency);
 							me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency);