From 370b14da960641eb695019b45a6577480e746513 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Wed, 4 Sep 2024 19:38:29 +0300 Subject: [PATCH 01/78] feat - Creation of mpesa settings doctype, stk push on billing file" --- .../press/doctype/mpesa_settings/__init__.py | 0 .../mpesa_settings/account_balance.html | 27 +++ .../doctype/mpesa_settings/mpesa_connector.py | 149 +++++++++++++ .../doctype/mpesa_settings/mpesa_settings.js | 36 +++ .../mpesa_settings/mpesa_settings.json | 154 +++++++++++++ .../doctype/mpesa_settings/mpesa_settings.py | 14 ++ .../mpesa_settings/test_mpesa_settings.py | 190 ++++++++++++++++ .../press_settings/press_settings.json | 2 +- .../doctype/press_settings/press_settings.py | 4 +- press/utils/billing.py | 206 ++++++++++++++++++ 10 files changed, 778 insertions(+), 4 deletions(-) create mode 100644 press/press/doctype/mpesa_settings/__init__.py create mode 100644 press/press/doctype/mpesa_settings/account_balance.html create mode 100644 press/press/doctype/mpesa_settings/mpesa_connector.py create mode 100644 press/press/doctype/mpesa_settings/mpesa_settings.js create mode 100644 press/press/doctype/mpesa_settings/mpesa_settings.json create mode 100644 press/press/doctype/mpesa_settings/mpesa_settings.py create mode 100644 press/press/doctype/mpesa_settings/test_mpesa_settings.py diff --git a/press/press/doctype/mpesa_settings/__init__.py b/press/press/doctype/mpesa_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/mpesa_settings/account_balance.html b/press/press/doctype/mpesa_settings/account_balance.html new file mode 100644 index 0000000000..6614cab89a --- /dev/null +++ b/press/press/doctype/mpesa_settings/account_balance.html @@ -0,0 +1,27 @@ +{% if not jQuery.isEmptyObject(data) %} +
{{ __("Balance Details") }}
+ + + + + + + + + + + + {% for(const [key, value] of Object.entries(data)) { %} + + + + + + + + {% } %} + +
{{ __("Account Type") }}{{ __("Current Balance") }}{{ __("Available Balance") }}{{ __("Reserved Balance") }}{{ __("Uncleared Balance") }}
{%= key %} {%= value["current_balance"] %} {%= value["available_balance"] %} {%= value["reserved_balance"] %} {%= value["uncleared_balance"] %}
+{% else %} +

Account Balance Information Not Available.

+{% endif %} diff --git a/press/press/doctype/mpesa_settings/mpesa_connector.py b/press/press/doctype/mpesa_settings/mpesa_connector.py new file mode 100644 index 0000000000..7eb8b9c0d8 --- /dev/null +++ b/press/press/doctype/mpesa_settings/mpesa_connector.py @@ -0,0 +1,149 @@ +import base64 +import datetime + +import requests +from requests.auth import HTTPBasicAuth + + +class MpesaConnector: + def __init__( + self, + env="sandbox", + app_key=None, + app_secret=None, + sandbox_url="https://sandbox.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 + self.app_secret = app_secret + if env == "sandbox": + self.base_url = sandbox_url + else: + self.base_url = live_url + self.authenticate() + + def authenticate(self): + """ + This method is used to fetch the access token required by Mpesa. + + 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 = f"{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": f"Bearer {self.authentication_token}", + "Content-Type": "application/json", + } + saf_url = "{}{}".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 = f"{str(business_shortcode)}{str(passcode)}{time}" + encoded = base64.b64encode(bytes(password, encoding="utf8")) + payload = { + "BusinessShortCode": business_shortcode, + "Password": encoded.decode("utf-8"), + "Timestamp": time, + "Amount": amount, + "PartyA": int(phone_number), + "PartyB": reference_code, + "PhoneNumber": int(phone_number), + "CallBackURL": callback_url, + "AccountReference": reference_code, + "TransactionDesc": description, + "TransactionType": "CustomerPayBillOnline" + if self.env == "sandbox" + else "CustomerBuyGoodsOnline", + } + headers = { + "Authorization": f"Bearer {self.authentication_token}", + "Content-Type": "application/json", + } + + saf_url = "{}{}".format(self.base_url, "/mpesa/stkpush/v1/processrequest") + r = requests.post(saf_url, headers=headers, json=payload) + return r.json() diff --git a/press/press/doctype/mpesa_settings/mpesa_settings.js b/press/press/doctype/mpesa_settings/mpesa_settings.js new file mode 100644 index 0000000000..9d625736b1 --- /dev/null +++ b/press/press/doctype/mpesa_settings/mpesa_settings.js @@ -0,0 +1,36 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Mpesa Settings', { + onload_post_render: function(frm) { + frm.events.setup_account_balance_html(frm); + }, + + refresh: function(frm) { + frappe.realtime.on("refresh_mpesa_dashboard", function(){ + frm.reload_doc(); + frm.events.setup_account_balance_html(frm); + }); + }, + + get_account_balance: function(frm) { + if (!frm.doc.initiator_name && !frm.doc.security_credential) { + frappe.throw(__("Please set the initiator name and the security credential")); + } + frappe.call({ + method: "get_account_balance_info", + doc: frm.doc + }); + }, + + setup_account_balance_html: function(frm) { + if (!frm.doc.account_balance) return; + $("div").remove(".form-dashboard-section.custom"); + frm.dashboard.add_section( + frappe.render_template('account_balance', { + data: JSON.parse(frm.doc.account_balance) + }) + ); + frm.dashboard.show(); + } +}); diff --git a/press/press/doctype/mpesa_settings/mpesa_settings.json b/press/press/doctype/mpesa_settings/mpesa_settings.json new file mode 100644 index 0000000000..dd742d0091 --- /dev/null +++ b/press/press/doctype/mpesa_settings/mpesa_settings.json @@ -0,0 +1,154 @@ +{ + "actions": [], + "autoname": "field:payment_gateway_name", + "creation": "2024-07-04 09:03:02.080734", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_gateway_name", + "consumer_key", + "consumer_secret", + "initiator_name", + "till_number", + "transaction_limit", + "sandbox", + "column_break_4", + "business_shortcode", + "online_passkey", + "security_credential", + "get_account_balance", + "account_balance" + ], + "fields": [ + { + "fieldname": "payment_gateway_name", + "fieldtype": "Data", + "label": "Payment Gateway Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret", + "reqd": 1 + }, + { + "fieldname": "initiator_name", + "fieldtype": "Data", + "label": "Initiator Name" + }, + { + "fieldname": "till_number", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Till Number", + "reqd": 1 + }, + { + "default": "150000", + "fieldname": "transaction_limit", + "fieldtype": "Float", + "label": "Transaction Limit", + "non_negative": 1 + }, + { + "default": "0", + "fieldname": "sandbox", + "fieldtype": "Check", + "label": "Sandbox" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:(doc.sandbox==0)", + "fieldname": "business_shortcode", + "fieldtype": "Data", + "label": "Business Shortcode", + "mandatory_depends_on": "eval:(doc.sandbox==0)" + }, + { + "fieldname": "online_passkey", + "fieldtype": "Password", + "label": " Online PassKey", + "reqd": 1 + }, + { + "fieldname": "security_credential", + "fieldtype": "Small Text", + "label": "Security Credential" + }, + { + "fieldname": "get_account_balance", + "fieldtype": "Button", + "label": "Get Account Balance" + }, + { + "fieldname": "account_balance", + "fieldtype": "Long Text", + "hidden": 1, + "label": "Account Balance", + "read_only": 1 + } + ], + "links": [], + "modified": "2024-07-04 09:03:02.080734", + "modified_by": "Administrator", + "module": "Frappe Mpesa Payments", + "name": "Mpesa Settings", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/press/press/doctype/mpesa_settings/mpesa_settings.py b/press/press/doctype/mpesa_settings/mpesa_settings.py new file mode 100644 index 0000000000..044931896c --- /dev/null +++ b/press/press/doctype/mpesa_settings/mpesa_settings.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + + +from json import dumps, loads + +import frappe +from frappe import _ +from frappe.model.document import Document + +class MpesaSettings(Document): + supported_currencies = ["KES"] + + \ No newline at end of file diff --git a/press/press/doctype/mpesa_settings/test_mpesa_settings.py b/press/press/doctype/mpesa_settings/test_mpesa_settings.py new file mode 100644 index 0000000000..51f072e658 --- /dev/null +++ b/press/press/doctype/mpesa_settings/test_mpesa_settings.py @@ -0,0 +1,190 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest +from json import dumps + +import frappe + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_customer +from erpnext.stock.doctype.item.test_item import make_item + +from press.press.doctype.mpesa_settings.mpesa_settings import ( + process_balance_info, + verify_transaction, +) + + +class TestMpesaSettings(unittest.TestCase): + def setUp(self): + # create payment gateway in setup + create_mpesa_settings(payment_gateway_name="_Test") + create_mpesa_settings(payment_gateway_name="_Account Balance") + create_mpesa_settings(payment_gateway_name="Payment") + + self.customer = create_customer("_Test Customer", "USD") + self.item = make_item(properties={"is_stock_item": 1}).name + + + def tearDown(self): + frappe.db.sql("delete from `tabMpesa Settings`") + frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'") + + 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.assertEqual(integration_request.status, "Completed") + + # test formatting of account balance received as string to json with appropriate currency symbol + mpesa_doc.reload() + self.assertEqual( + 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", + } + } + ), + ) + + integration_request.delete() + + def test_processing_of_callback_payload(self): + 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("Customer", "_Test Customer", "default_currency", "KES") + + +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( # nosec + doctype="Mpesa Settings", + sandbox=1, + 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(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": CheckoutRequestID, + "ResultCode": 0, + "ResultDesc": "The service request is processed successfully.", + "CallbackMetadata": { + "Item": [ + {"Name": "Amount", "Value": Amount}, + {"Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R"}, + {"Name": "TransactionDate", "Value": 20201006113336}, + {"Name": "PhoneNumber", "Value": 254723575670}, + ] + }, + } + + +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": 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(): + """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", + } + }, + } + } diff --git a/press/press/doctype/press_settings/press_settings.json b/press/press/doctype/press_settings/press_settings.json index 7fd1812bc6..26429f66f9 100644 --- a/press/press/doctype/press_settings/press_settings.json +++ b/press/press/doctype/press_settings/press_settings.json @@ -1208,7 +1208,7 @@ ], "issingle": 1, "links": [], - "modified": "2024-07-18 16:26:48.824438", + "modified": "2024-09-02 12:32:53.863352", "modified_by": "Administrator", "module": "Press", "name": "Press Settings", diff --git a/press/press/doctype/press_settings/press_settings.py b/press/press/doctype/press_settings/press_settings.py index 1a2674ed14..ba278a7215 100644 --- a/press/press/doctype/press_settings/press_settings.py +++ b/press/press/doctype/press_settings/press_settings.py @@ -144,9 +144,7 @@ class PressSettings(Document): use_app_cache: DF.Check use_delta_builds: DF.Check use_staging_ca: DF.Check - verify_cards_with_micro_charge: DF.Literal[ - "No", "Only INR", "Only USD", "Both INR and USD" - ] + verify_cards_with_micro_charge: DF.Literal["No", "Only INR", "Only USD", "Both INR and USD"] webroot_directory: DF.Data | None # end: auto-generated types diff --git a/press/utils/billing.py b/press/utils/billing.py index e0d451dfc8..15d46204e3 100644 --- a/press/utils/billing.py +++ b/press/utils/billing.py @@ -7,6 +7,10 @@ from press.exceptions import CentralServerNotSet, FrappeioServerNotSet from press.utils import get_current_team, log_error +from frappe.utils import get_request_site_address +from press.press.doctype.mpesa_settings.mpesa_connector import MpesaConnector +from json import dumps, loads +from frappe.integrations.utils import create_request_log states_with_tin = { "Andaman and Nicobar Islands": "35", @@ -51,6 +55,7 @@ GSTIN_FORMAT = re.compile( "^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$" ) +supported_mpesa_currencies = ["KES"] def format_stripe_money(amount, currency): @@ -241,3 +246,204 @@ def process_micro_debit_test_charge(stripe_event): ).insert(ignore_permissions=True) except Exception: log_error("Error Processing Stripe Micro Debit Charge", body=stripe_event) + + +#Mpesa integrations, mpesa express +def validate_mpesa_transaction_currency(currency): + if currency not in supported_mpesa_currencies: + frappe.throw( + _( + "Please select another payment method. Mpesa does not support transactions in currency '{0}'" + ).format(currency) + ) +'''ensures number take the right format''' +def sanitize_mobile_number(number): + """Add country code and strip leading zeroes from the phone number.""" + return "254" + str(number).lstrip("0") + + +'''splitas amount if it exceeds 150,000''' +def split_request_amount_according_to_transaction_limit(amount, transaction_limit): + request_amount = amount + if request_amount > transaction_limit: + # make multiple requests + request_amounts = [] + requests_to_be_made = frappe.utils.ceil( + request_amount / transaction_limit + ) # 480/150 = ceil(3.2) = 4 + for i in range(requests_to_be_made): + amount = transaction_limit + if i == requests_to_be_made - 1: + amount = request_amount - ( + transaction_limit * i + ) # for 4th request, 480 - (150 * 3) = 30 + request_amounts.append(amount) + else: + request_amounts = [request_amount] + + return request_amounts + +'''Send stk push to the user''' +def generate_stk_push(**kwargs): + """Generate stk push by making a API call to the stk push API.""" + args = frappe._dict(kwargs) + try: + callback_url = ( + get_request_site_address(True) + + "/api/method/press.press.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" + # 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, + app_secret=mpesa_settings.get_password("consumer_secret"), + ) + + mobile_number = sanitize_mobile_number(args.sender) + + 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="Frappe Cloud Payment", + ) + + return response + + except Exception: + frappe.log_error("Mpesa Express Transaction Error") + frappe.throw( + _("Issue detected with Mpesa configuration, check the error logs for more details"), + title=_("Mpesa Express Error"), + ) + + +'''Verify transaction after push notification''' +@frappe.whitelist(allow_guest=True) +def verify_pesa_transaction(**kwargs): + """Verify the transaction result received via callback from STK.""" + transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) + + checkout_id = getattr(transaction_response, "CheckoutRequestID", "") + if not isinstance(checkout_id, str): + frappe.throw(_("Invalid Checkout Request ID")) + + # Retrieve the corresponding Integration Request document + integration_request = frappe.get_doc("Integration Request", checkout_id) + transaction_data = frappe._dict(loads(integration_request.data)) + total_paid = 0 + success = False + + if transaction_response["ResultCode"] == 0: # Transaction was successful + if integration_request.reference_doctype and integration_request.reference_docname: + try: + item_response = transaction_response["CallbackMetadata"]["Item"] + amount = fetch_param_value(item_response, "Amount", "Name") + mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") + + # Fetch the document associated with the payment + pr = frappe.get_doc( + integration_request.reference_doctype, integration_request.reference_docname + ) + + # Get completed payments and receipts for the integration request + 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 + + # Mark the Integration Request as successful + integration_request.handle_success(transaction_response) + except Exception: + # Handle failure scenario and log an error + integration_request.handle_failure(transaction_response) + frappe.log_error("Mpesa: Failed to verify transaction") + + else: + # If the transaction was not successful, handle the failure + integration_request.handle_failure(transaction_response) + + +'''fetch parameters from the args''' +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"] + +'''get completed integration requests''' +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 + +'''request for payments''' +def request_for_payment(**kwargs): + args = frappe._dict(kwargs) + request_amounts = split_request_amount_according_to_transaction_limit(args.request_amount) + + for i, amount in enumerate(request_amounts): + args.request_amount = amount + if frappe.flags.in_test: + from press.press.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)) + + handle_api_response("CheckoutRequestID", args, response) + + +def handle_api_response(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 + if getattr(response, "requestId"): + req_name = getattr(response, "requestId") + error = response + else: + # global checkout id used as request name + req_name = getattr(response, global_id) + error = None + + 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")) \ No newline at end of file From 0ada507b7b4bc726de15206df6e1b583416b56cd Mon Sep 17 00:00:00 2001 From: maniamartial Date: Thu, 5 Sep 2024 17:49:21 +0300 Subject: [PATCH 02/78] feat - Mpesa Payment Register to record transactions --- press/api/billing.py | 244 ++++++++++++++++++ .../mpesa_payment_register/__init__.py | 0 .../mpesa_payment_register.js | 8 + .../mpesa_payment_register.json | 236 +++++++++++++++++ .../mpesa_payment_register.py | 35 +++ .../test_mpesa_payment_register.py | 9 + .../mpesa_settings/mpesa_settings.json | 26 +- .../doctype/mpesa_settings/mpesa_settings.py | 22 ++ press/utils/billing.py | 206 +-------------- 9 files changed, 576 insertions(+), 210 deletions(-) create mode 100644 press/press/doctype/mpesa_payment_register/__init__.py create mode 100644 press/press/doctype/mpesa_payment_register/mpesa_payment_register.js create mode 100644 press/press/doctype/mpesa_payment_register/mpesa_payment_register.json create mode 100644 press/press/doctype/mpesa_payment_register/mpesa_payment_register.py create mode 100644 press/press/doctype/mpesa_payment_register/test_mpesa_payment_register.py diff --git a/press/api/billing.py b/press/api/billing.py index f0dd8e566d..9f2b35f5a3 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -23,7 +23,12 @@ states_with_tin, validate_gstin_check_digit, ) +from frappe.utils import get_request_site_address +from press.press.doctype.mpesa_settings.mpesa_connector import MpesaConnector +from json import dumps, loads +from frappe.integrations.utils import create_request_log +supported_mpesa_currencies = ["KES"] @frappe.whitelist() def get_publishable_key_and_setup_intent(): @@ -644,3 +649,242 @@ def total_unpaid_amount(): )[0] or 0 ) + negative_balance + + + +#Mpesa integrations, mpesa express +def validate_mpesa_transaction_currency(currency): + if currency not in supported_mpesa_currencies: + frappe.throw( + _( + "Please select another payment method. Mpesa does not support transactions in currency '{0}'" + ).format(currency) + ) + + +'''ensures number take the right format''' +def sanitize_mobile_number(number): + """Add country code and strip leading zeroes from the phone number.""" + return "254" + str(number).lstrip("0") + + +'''split amount if it exceeds 150,000''' +def split_request_amount_according_to_transaction_limit(amount, transaction_limit): + request_amount = amount + if request_amount > transaction_limit: + # make multiple requests + request_amounts = [] + requests_to_be_made = frappe.utils.ceil( + request_amount / transaction_limit + ) # 480/150 = ceil(3.2) = 4 + for i in range(requests_to_be_made): + amount = transaction_limit + if i == requests_to_be_made - 1: + amount = request_amount - ( + transaction_limit * i + ) # for 4th request, 480 - (150 * 3) = 30 + request_amounts.append(amount) + else: + request_amounts = [request_amount] + + return request_amounts + +'''Send stk push to the user''' +def generate_stk_push(**kwargs): + """Generate stk push by making a API call to the stk push API.""" + args = frappe._dict(kwargs) + try: + callback_url = ( + get_request_site_address(True) + + "/api/method/press.press.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" + # 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, + app_secret=mpesa_settings.get_password("consumer_secret"), + ) + + mobile_number = sanitize_mobile_number(args.sender) + + 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="Frappe Cloud Payment", + ) + + return response + + except Exception: + frappe.log_error("Mpesa Express Transaction Error") + frappe.throw( + _("Issue detected with Mpesa configuration, check the error logs for more details"), + title=_("Mpesa Express Error"), + ) + + +'''Verify transaction after push notification''' +@frappe.whitelist(allow_guest=True) +def verify_pesa_transaction(**kwargs): + """Verify the transaction result received via callback from STK.""" + if "Body" not in kwargs or "stkCallback" not in kwargs["Body"]: + frappe.throw(_("Invalid transaction response format")) + + transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) + + checkout_id = getattr(transaction_response, "CheckoutRequestID", "") + if not isinstance(checkout_id, str): + frappe.throw(_("Invalid Checkout Request ID")) + + # Retrieve the corresponding Integration Request document + integration_request = frappe.get_doc("Integration Request", checkout_id) + transaction_data = frappe._dict(loads(integration_request.data)) + total_paid = 0 + success = False + + if transaction_response["ResultCode"] == 0: # Transaction was successful + if integration_request.reference_doctype and integration_request.reference_docname: + try: + item_response = transaction_response["CallbackMetadata"]["Item"] + amount = fetch_param_value(item_response, "Amount", "Name") + mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") + + # Fetch the document associated with the payment + pr = frappe.get_doc( + integration_request.reference_doctype, integration_request.reference_docname + ) + + # Get completed payments and receipts for the integration request + 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") + # Mark the Integration Request as successful + integration_request.handle_success(transaction_response) + # Call function to create a new Mpesa Payment Register entry + create_mpesa_payment_register_entry(transaction_response) + except Exception: + # Handle failure scenario and log an error + integration_request.handle_failure(transaction_response) + frappe.log_error("Mpesa: Failed to verify transaction") + + else: + # If the transaction was not successful, handle the failure + integration_request.handle_failure(transaction_response) + + +'''fetch parameters from the args''' +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"] + +'''get completed integration requests''' +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 + +'''request for payments''' +def request_for_payment(**kwargs): + args = frappe._dict(kwargs) + request_amounts = split_request_amount_according_to_transaction_limit(args.request_amount) + + for i, amount in enumerate(request_amounts): + args.request_amount = amount + if frappe.flags.in_test: + from press.press.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)) + + handle_api_response("CheckoutRequestID", args, response) + + +def handle_api_response(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 + if getattr(response, "requestId"): + req_name = getattr(response, "requestId") + error = response + else: + # global checkout id used as request name + req_name = getattr(response, global_id) + error = None + + 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")) + +def create_mpesa_payment_register_entry(transaction_response): + team = get_current_team() + """Create a new entry in the Mpesa Payment Register for a successful transaction.""" + # Extract necessary details from the transaction response + item_response = transaction_response.get("CallbackMetadata", {}).get("Item", []) + + # Fetch relevant details from the response + transaction_id = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") + trans_time = fetch_param_value(item_response, "TransactionDate", "Name") + msisdn = fetch_param_value(item_response, "PhoneNumber", "Name") + transaction_id=transaction_response.get("CheckoutRequestID") + amount = fetch_param_value(item_response, "Amount", "Name") + request_id=transaction_response.get("MerchantRequestID") + + # Create a new entry in Mpesa Payment Register + new_entry = frappe.get_doc({ + "doctype": "Mpesa Payment Register", + "transaction_id": transaction_id, + "trans_time": trans_time, + "transaction_type":"Mpesa Express", + # "business_shortcode": business_shortcode, + "team": team, + "msisdn": msisdn, + "trans_amount": amount, + "merchant_request_id": request_id, + }) + + # Save the new document to the database + new_entry.insert(ignore_permissions=True) + frappe.db.commit() # Commit the changes to the database + frappe.msgprint(_("Mpesa Payment Register entry created successfully")) diff --git a/press/press/doctype/mpesa_payment_register/__init__.py b/press/press/doctype/mpesa_payment_register/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.js b/press/press/doctype/mpesa_payment_register/mpesa_payment_register.js new file mode 100644 index 0000000000..02d6c869d6 --- /dev/null +++ b/press/press/doctype/mpesa_payment_register/mpesa_payment_register.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Mpesa Payment Register", { +// refresh(frm) { + +// }, +// }); diff --git a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.json b/press/press/doctype/mpesa_payment_register/mpesa_payment_register.json new file mode 100644 index 0000000000..810eee61bf --- /dev/null +++ b/press/press/doctype/mpesa_payment_register/mpesa_payment_register.json @@ -0,0 +1,236 @@ +{ + "actions": [], + "allow_copy": 1, + "allow_import": 1, + "autoname": "MPC2B.-.YY.-.MM.-.######", + "creation": "2024-04-11 13:39:25.887687", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "merchant_request_id", + "trans_id", + "transaction_type", + "trans_time", + "trans_amount", + "bill_ref_number", + "org_account_balance", + "third_party_transid", + "msisdn", + "column_break_14", + "payment_partner", + "invoice_number", + "posting_date", + "posting_time", + "default_currency", + "submit_payment", + "amended_from" + ], + "fields": [ + { + "fieldname": "msisdn", + "fieldtype": "Data", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "MSISDN", + "no_copy": 1, + "options": "Phone", + "read_only": 1 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Posting Date", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "Now", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Team", + "options": "Team" + }, + { + "default": "KES", + "fetch_from": "company.default_currency", + "fieldname": "default_currency", + "fieldtype": "Data", + "label": "Default Currency", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "submit_payment", + "fieldtype": "Check", + "label": "Submit Payment " + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Mpesa Payment Register", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "trans_id", + "fieldtype": "Data", + "label": "Trans ID", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "trans_time", + "fieldtype": "Data", + "label": "Trans Time", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "trans_amount", + "fieldtype": "Float", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Trans Amount", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "bill_ref_number", + "fieldtype": "Data", + "label": "Bill Ref Number", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "invoice_number", + "fieldtype": "Data", + "label": "Invoice Number", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "org_account_balance", + "fieldtype": "Data", + "label": "Org Account Balance", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "third_party_transid", + "fieldtype": "Data", + "label": "Third Party Trans ID", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "transaction_type", + "fieldtype": "Select", + "label": "Transaction Type", + "options": "\nMpesa Express\nMpesa C2B" + }, + { + "fieldname": "merchant_request_id", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Request ID", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "payment_partner", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Payment Partner", + "no_copy": 1, + "read_only": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2024-09-05 17:44:50.641932", + "modified_by": "Administrator", + "module": "Press", + "name": "Mpesa Payment Register", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "show_preview_popup": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "trans_id", + "track_changes": 1 +} \ No newline at end of file diff --git a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.py b/press/press/doctype/mpesa_payment_register/mpesa_payment_register.py new file mode 100644 index 0000000000..349d7ac080 --- /dev/null +++ b/press/press/doctype/mpesa_payment_register/mpesa_payment_register.py @@ -0,0 +1,35 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class MpesaPaymentRegister(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + amended_from: DF.Link | None + bill_ref_number: DF.Data | None + company: DF.Link | None + default_currency: DF.Data | None + invoice_number: DF.Data | None + merchant_request_id: DF.Data | None + msisdn: DF.Data | None + org_account_balance: DF.Data | None + payment_partner: DF.Data | None + posting_date: DF.Date | None + posting_time: DF.Time | None + submit_payment: DF.Check + third_party_transid: DF.Data | None + trans_amount: DF.Float + trans_id: DF.Data | None + trans_time: DF.Data | None + transaction_type: DF.Literal["", "Mpesa Express", "Mpesa C2B"] + # end: auto-generated types + pass diff --git a/press/press/doctype/mpesa_payment_register/test_mpesa_payment_register.py b/press/press/doctype/mpesa_payment_register/test_mpesa_payment_register.py new file mode 100644 index 0000000000..92e029c64b --- /dev/null +++ b/press/press/doctype/mpesa_payment_register/test_mpesa_payment_register.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestMpesaPaymentRegister(FrappeTestCase): + pass diff --git a/press/press/doctype/mpesa_settings/mpesa_settings.json b/press/press/doctype/mpesa_settings/mpesa_settings.json index dd742d0091..a0a7399fa1 100644 --- a/press/press/doctype/mpesa_settings/mpesa_settings.json +++ b/press/press/doctype/mpesa_settings/mpesa_settings.json @@ -1,21 +1,23 @@ { "actions": [], - "autoname": "field:payment_gateway_name", + "autoname": "format:{team}-{api_type}", "creation": "2024-07-04 09:03:02.080734", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "payment_gateway_name", + "team", + "api_type", "consumer_key", "consumer_secret", "initiator_name", "till_number", - "transaction_limit", "sandbox", "column_break_4", "business_shortcode", "online_passkey", + "transaction_limit", "security_credential", "get_account_balance", "account_balance" @@ -100,14 +102,28 @@ "hidden": 1, "label": "Account Balance", "read_only": 1 + }, + { + "fieldname": "team", + "fieldtype": "Link", + "label": "Team", + "options": "Team", + "reqd": 1 + }, + { + "fieldname": "api_type", + "fieldtype": "Select", + "label": "API Type", + "options": "\nMpesa Express\nMpesa C2B", + "reqd": 1 } ], "links": [], - "modified": "2024-07-04 09:03:02.080734", + "modified": "2024-09-05 17:33:46.139266", "modified_by": "Administrator", - "module": "Frappe Mpesa Payments", + "module": "Press", "name": "Mpesa Settings", - "naming_rule": "By fieldname", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/press/press/doctype/mpesa_settings/mpesa_settings.py b/press/press/doctype/mpesa_settings/mpesa_settings.py index 044931896c..aa96a3f151 100644 --- a/press/press/doctype/mpesa_settings/mpesa_settings.py +++ b/press/press/doctype/mpesa_settings/mpesa_settings.py @@ -9,6 +9,28 @@ from frappe.model.document import Document class MpesaSettings(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + account_balance: DF.LongText | None + api_type: DF.Literal["", "Mpesa Express", "Mpesa C2B"] + business_shortcode: DF.Data | None + consumer_key: DF.Data + consumer_secret: DF.Password + initiator_name: DF.Data | None + online_passkey: DF.Password + payment_gateway_name: DF.Data + sandbox: DF.Check + security_credential: DF.SmallText | None + team: DF.Link + till_number: DF.Data + transaction_limit: DF.Float + # end: auto-generated types supported_currencies = ["KES"] \ No newline at end of file diff --git a/press/utils/billing.py b/press/utils/billing.py index 15d46204e3..8a0e4fe4e1 100644 --- a/press/utils/billing.py +++ b/press/utils/billing.py @@ -7,10 +7,7 @@ from press.exceptions import CentralServerNotSet, FrappeioServerNotSet from press.utils import get_current_team, log_error -from frappe.utils import get_request_site_address -from press.press.doctype.mpesa_settings.mpesa_connector import MpesaConnector -from json import dumps, loads -from frappe.integrations.utils import create_request_log + states_with_tin = { "Andaman and Nicobar Islands": "35", @@ -55,7 +52,6 @@ GSTIN_FORMAT = re.compile( "^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$" ) -supported_mpesa_currencies = ["KES"] def format_stripe_money(amount, currency): @@ -247,203 +243,3 @@ def process_micro_debit_test_charge(stripe_event): except Exception: log_error("Error Processing Stripe Micro Debit Charge", body=stripe_event) - -#Mpesa integrations, mpesa express -def validate_mpesa_transaction_currency(currency): - if currency not in supported_mpesa_currencies: - frappe.throw( - _( - "Please select another payment method. Mpesa does not support transactions in currency '{0}'" - ).format(currency) - ) -'''ensures number take the right format''' -def sanitize_mobile_number(number): - """Add country code and strip leading zeroes from the phone number.""" - return "254" + str(number).lstrip("0") - - -'''splitas amount if it exceeds 150,000''' -def split_request_amount_according_to_transaction_limit(amount, transaction_limit): - request_amount = amount - if request_amount > transaction_limit: - # make multiple requests - request_amounts = [] - requests_to_be_made = frappe.utils.ceil( - request_amount / transaction_limit - ) # 480/150 = ceil(3.2) = 4 - for i in range(requests_to_be_made): - amount = transaction_limit - if i == requests_to_be_made - 1: - amount = request_amount - ( - transaction_limit * i - ) # for 4th request, 480 - (150 * 3) = 30 - request_amounts.append(amount) - else: - request_amounts = [request_amount] - - return request_amounts - -'''Send stk push to the user''' -def generate_stk_push(**kwargs): - """Generate stk push by making a API call to the stk push API.""" - args = frappe._dict(kwargs) - try: - callback_url = ( - get_request_site_address(True) - + "/api/method/press.press.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" - # 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, - app_secret=mpesa_settings.get_password("consumer_secret"), - ) - - mobile_number = sanitize_mobile_number(args.sender) - - 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="Frappe Cloud Payment", - ) - - return response - - except Exception: - frappe.log_error("Mpesa Express Transaction Error") - frappe.throw( - _("Issue detected with Mpesa configuration, check the error logs for more details"), - title=_("Mpesa Express Error"), - ) - - -'''Verify transaction after push notification''' -@frappe.whitelist(allow_guest=True) -def verify_pesa_transaction(**kwargs): - """Verify the transaction result received via callback from STK.""" - transaction_response = frappe._dict(kwargs["Body"]["stkCallback"]) - - checkout_id = getattr(transaction_response, "CheckoutRequestID", "") - if not isinstance(checkout_id, str): - frappe.throw(_("Invalid Checkout Request ID")) - - # Retrieve the corresponding Integration Request document - integration_request = frappe.get_doc("Integration Request", checkout_id) - transaction_data = frappe._dict(loads(integration_request.data)) - total_paid = 0 - success = False - - if transaction_response["ResultCode"] == 0: # Transaction was successful - if integration_request.reference_doctype and integration_request.reference_docname: - try: - item_response = transaction_response["CallbackMetadata"]["Item"] - amount = fetch_param_value(item_response, "Amount", "Name") - mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") - - # Fetch the document associated with the payment - pr = frappe.get_doc( - integration_request.reference_doctype, integration_request.reference_docname - ) - - # Get completed payments and receipts for the integration request - 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 - - # Mark the Integration Request as successful - integration_request.handle_success(transaction_response) - except Exception: - # Handle failure scenario and log an error - integration_request.handle_failure(transaction_response) - frappe.log_error("Mpesa: Failed to verify transaction") - - else: - # If the transaction was not successful, handle the failure - integration_request.handle_failure(transaction_response) - - -'''fetch parameters from the args''' -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"] - -'''get completed integration requests''' -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 - -'''request for payments''' -def request_for_payment(**kwargs): - args = frappe._dict(kwargs) - request_amounts = split_request_amount_according_to_transaction_limit(args.request_amount) - - for i, amount in enumerate(request_amounts): - args.request_amount = amount - if frappe.flags.in_test: - from press.press.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)) - - handle_api_response("CheckoutRequestID", args, response) - - -def handle_api_response(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 - if getattr(response, "requestId"): - req_name = getattr(response, "requestId") - error = response - else: - # global checkout id used as request name - req_name = getattr(response, global_id) - error = None - - 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")) \ No newline at end of file From 335d05fe57561774a2d4fe21e81d069df3b14caf Mon Sep 17 00:00:00 2001 From: maniamartial Date: Mon, 9 Sep 2024 15:27:48 +0300 Subject: [PATCH 03/78] feat - Creation of balance transaction after every successfull mpesa transaction --- press/api/billing.py | 45 +++++++++++++++--- .../__init__.py | 0 .../mpesa_payment_record.js} | 2 +- .../mpesa_payment_record.json} | 46 ++++++++----------- .../mpesa_payment_record.py} | 7 ++- .../test_mpesa_payment_record.py} | 2 +- .../mpesa_settings/mpesa_settings.json | 4 +- 7 files changed, 63 insertions(+), 43 deletions(-) rename press/press/doctype/{mpesa_payment_register => mpesa_payment_record}/__init__.py (100%) rename press/press/doctype/{mpesa_payment_register/mpesa_payment_register.js => mpesa_payment_record/mpesa_payment_record.js} (73%) rename press/press/doctype/{mpesa_payment_register/mpesa_payment_register.json => mpesa_payment_record/mpesa_payment_record.json} (86%) rename press/press/doctype/{mpesa_payment_register/mpesa_payment_register.py => mpesa_payment_record/mpesa_payment_record.py} (85%) rename press/press/doctype/{mpesa_payment_register/test_mpesa_payment_register.py => mpesa_payment_record/test_mpesa_payment_record.py} (73%) diff --git a/press/api/billing.py b/press/api/billing.py index 9f2b35f5a3..ba018521d6 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -773,11 +773,10 @@ def verify_pesa_transaction(**kwargs): 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") - # Mark the Integration Request as successful integration_request.handle_success(transaction_response) - # Call function to create a new Mpesa Payment Register entry + + # Call function to create a new Mpesa Payment Record entry create_mpesa_payment_register_entry(transaction_response) except Exception: # Handle failure scenario and log an error @@ -859,7 +858,7 @@ def handle_api_response(global_id, request_dict, response): def create_mpesa_payment_register_entry(transaction_response): team = get_current_team() - """Create a new entry in the Mpesa Payment Register for a successful transaction.""" + """Create a new entry in the Mpesa Payment Record for a successful transaction.""" # Extract necessary details from the transaction response item_response = transaction_response.get("CallbackMetadata", {}).get("Item", []) @@ -871,9 +870,9 @@ def create_mpesa_payment_register_entry(transaction_response): amount = fetch_param_value(item_response, "Amount", "Name") request_id=transaction_response.get("MerchantRequestID") - # Create a new entry in Mpesa Payment Register + # Create a new entry in Mpesa Payment Record new_entry = frappe.get_doc({ - "doctype": "Mpesa Payment Register", + "doctype": "Mpesa Payment Record", "transaction_id": transaction_id, "trans_time": trans_time, "transaction_type":"Mpesa Express", @@ -887,4 +886,36 @@ def create_mpesa_payment_register_entry(transaction_response): # Save the new document to the database new_entry.insert(ignore_permissions=True) frappe.db.commit() # Commit the changes to the database - frappe.msgprint(_("Mpesa Payment Register entry created successfully")) + frappe.msgprint(_("Mpesa Payment Record entry created successfully")) + + +def create_balance_transaction(team, amount, invoice=None): + """Create a new entry in the Balance Transaction table.""" + + #Get the ending balance of this team + team_balance_transaction=frappe.get_all("Balance Transaction", filters={"team": team}, fields=["ending_balance"], order_by="creation desc", limit=1) + ending_balance=team_balance_transaction[0].ending_balance if team_balance_transaction else 0 + # Create a new entry in the Balance Transaction table + new_entry = frappe.get_doc({ + "doctype": "Balance Transaction", + "team": team, + "type":"Adjustment", + "amount": amount, + "source": "Prepaid Credits", + "ending_balance":ending_balance+amount, + "docstatus": 1, + "invoice": invoice if invoice else None, + }) + + # Save the new document to the database + new_entry.insert(ignore_permissions=True) + frappe.db.commit() # Commit the changes to the database + frappe.msgprint(_("Balance Transaction entry created successfully")) + +def after_save(doc, method=None): + team = doc.team + amount = doc.amount + create_balance_transaction(team, amount) + + + \ No newline at end of file diff --git a/press/press/doctype/mpesa_payment_register/__init__.py b/press/press/doctype/mpesa_payment_record/__init__.py similarity index 100% rename from press/press/doctype/mpesa_payment_register/__init__.py rename to press/press/doctype/mpesa_payment_record/__init__.py diff --git a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.js b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.js similarity index 73% rename from press/press/doctype/mpesa_payment_register/mpesa_payment_register.js rename to press/press/doctype/mpesa_payment_record/mpesa_payment_record.js index 02d6c869d6..d556a34a3f 100644 --- a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.js +++ b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.js @@ -1,7 +1,7 @@ // Copyright (c) 2024, Frappe and contributors // For license information, please see license.txt -// frappe.ui.form.on("Mpesa Payment Register", { +// frappe.ui.form.on("Mpesa Payment Record", { // refresh(frm) { // }, diff --git a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.json b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.json similarity index 86% rename from press/press/doctype/mpesa_payment_register/mpesa_payment_register.json rename to press/press/doctype/mpesa_payment_record/mpesa_payment_record.json index 810eee61bf..d5450ab521 100644 --- a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.json +++ b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.json @@ -15,17 +15,16 @@ "transaction_type", "trans_time", "trans_amount", + "amountusd", "bill_ref_number", - "org_account_balance", - "third_party_transid", - "msisdn", + "exchange_rate", "column_break_14", + "msisdn", "payment_partner", "invoice_number", "posting_date", "posting_time", "default_currency", - "submit_payment", "amended_from" ], "fields": [ @@ -77,18 +76,12 @@ "label": "Default Currency", "read_only": 1 }, - { - "default": "0", - "fieldname": "submit_payment", - "fieldtype": "Check", - "label": "Submit Payment " - }, { "fieldname": "amended_from", "fieldtype": "Link", "label": "Amended From", "no_copy": 1, - "options": "Mpesa Payment Register", + "options": "Mpesa Payment Record", "print_hide": 1, "read_only": 1 }, @@ -112,7 +105,7 @@ "in_list_view": 1, "in_preview": 1, "in_standard_filter": 1, - "label": "Trans Amount", + "label": "Trans Amount(Ksh)", "no_copy": 1, "read_only": 1 }, @@ -130,20 +123,6 @@ "no_copy": 1, "read_only": 1 }, - { - "fieldname": "org_account_balance", - "fieldtype": "Data", - "label": "Org Account Balance", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "third_party_transid", - "fieldtype": "Data", - "label": "Third Party Trans ID", - "no_copy": 1, - "read_only": 1 - }, { "fieldname": "transaction_type", "fieldtype": "Select", @@ -167,16 +146,27 @@ "label": "Payment Partner", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "amountusd", + "fieldtype": "Currency", + "label": "Amount(USD)" + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "precision": "9" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-09-05 17:44:50.641932", + "modified": "2024-09-09 11:56:11.306741", "modified_by": "Administrator", "module": "Press", - "name": "Mpesa Payment Register", + "name": "Mpesa Payment Record", "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ diff --git a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.py b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.py similarity index 85% rename from press/press/doctype/mpesa_payment_register/mpesa_payment_register.py rename to press/press/doctype/mpesa_payment_record/mpesa_payment_record.py index 349d7ac080..33b99881fc 100644 --- a/press/press/doctype/mpesa_payment_register/mpesa_payment_register.py +++ b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.py @@ -5,7 +5,7 @@ from frappe.model.document import Document -class MpesaPaymentRegister(Document): +class MpesaPaymentRecord(Document): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -15,18 +15,17 @@ class MpesaPaymentRegister(Document): from frappe.types import DF amended_from: DF.Link | None + amountusd: DF.Currency bill_ref_number: DF.Data | None company: DF.Link | None default_currency: DF.Data | None + exchange_rate: DF.Float invoice_number: DF.Data | None merchant_request_id: DF.Data | None msisdn: DF.Data | None - org_account_balance: DF.Data | None payment_partner: DF.Data | None posting_date: DF.Date | None posting_time: DF.Time | None - submit_payment: DF.Check - third_party_transid: DF.Data | None trans_amount: DF.Float trans_id: DF.Data | None trans_time: DF.Data | None diff --git a/press/press/doctype/mpesa_payment_register/test_mpesa_payment_register.py b/press/press/doctype/mpesa_payment_record/test_mpesa_payment_record.py similarity index 73% rename from press/press/doctype/mpesa_payment_register/test_mpesa_payment_register.py rename to press/press/doctype/mpesa_payment_record/test_mpesa_payment_record.py index 92e029c64b..3b81a74b30 100644 --- a/press/press/doctype/mpesa_payment_register/test_mpesa_payment_register.py +++ b/press/press/doctype/mpesa_payment_record/test_mpesa_payment_record.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestMpesaPaymentRegister(FrappeTestCase): +class TestMpesaPaymentRecord(FrappeTestCase): pass diff --git a/press/press/doctype/mpesa_settings/mpesa_settings.json b/press/press/doctype/mpesa_settings/mpesa_settings.json index a0a7399fa1..bbe0549014 100644 --- a/press/press/doctype/mpesa_settings/mpesa_settings.json +++ b/press/press/doctype/mpesa_settings/mpesa_settings.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:{team}-{api_type}", + "autoname": "format:{payment_gateway_name}-{api_type}", "creation": "2024-07-04 09:03:02.080734", "doctype": "DocType", "editable_grid": 1, @@ -119,7 +119,7 @@ } ], "links": [], - "modified": "2024-09-05 17:33:46.139266", + "modified": "2024-09-06 08:49:55.451792", "modified_by": "Administrator", "module": "Press", "name": "Mpesa Settings", From 7981591f001134d8116ce7e71824e09d391a3138 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Tue, 10 Sep 2024 17:50:10 +0300 Subject: [PATCH 04/78] feat - link bank the transaction balance to mpesa payment record --- press/api/billing.py | 73 +++++++++++-------- press/hooks.py | 3 + .../mpesa_payment_record.json | 11 ++- .../mpesa_payment_record.py | 1 + 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/press/api/billing.py b/press/api/billing.py index ba018521d6..e30b964f74 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -779,12 +779,10 @@ def verify_pesa_transaction(**kwargs): # Call function to create a new Mpesa Payment Record entry create_mpesa_payment_register_entry(transaction_response) except Exception: - # Handle failure scenario and log an error integration_request.handle_failure(transaction_response) frappe.log_error("Mpesa: Failed to verify transaction") else: - # If the transaction was not successful, handle the failure integration_request.handle_failure(transaction_response) @@ -876,46 +874,63 @@ def create_mpesa_payment_register_entry(transaction_response): "transaction_id": transaction_id, "trans_time": trans_time, "transaction_type":"Mpesa Express", - # "business_shortcode": business_shortcode, "team": team, "msisdn": msisdn, "trans_amount": amount, "merchant_request_id": request_id, }) - # Save the new document to the database new_entry.insert(ignore_permissions=True) - frappe.db.commit() # Commit the changes to the database + frappe.db.commit() frappe.msgprint(_("Mpesa Payment Record entry created successfully")) def create_balance_transaction(team, amount, invoice=None): - """Create a new entry in the Balance Transaction table.""" + """Create a new entry in the Balance Transaction table.""" - #Get the ending balance of this team - team_balance_transaction=frappe.get_all("Balance Transaction", filters={"team": team}, fields=["ending_balance"], order_by="creation desc", limit=1) - ending_balance=team_balance_transaction[0].ending_balance if team_balance_transaction else 0 - # Create a new entry in the Balance Transaction table - new_entry = frappe.get_doc({ - "doctype": "Balance Transaction", - "team": team, - "type":"Adjustment", - "amount": amount, - "source": "Prepaid Credits", - "ending_balance":ending_balance+amount, - "docstatus": 1, - "invoice": invoice if invoice else None, - }) + # Get the ending balance of this team + team_balance_transaction = frappe.get_all( + "Balance Transaction", + filters={"team": team}, + fields=["ending_balance"], + order_by="creation desc", + limit=1 + ) + ending_balance = team_balance_transaction[0].ending_balance if team_balance_transaction else 0 + + # Create a new entry in the Balance Transaction table + new_entry = frappe.get_doc({ + "doctype": "Balance Transaction", + "team": team, + "type": "Adjustment", + "amount": amount, + "source": "Prepaid Credits", + "ending_balance": ending_balance + amount, + "docstatus": 1, + "invoice": invoice if invoice else None, + "description": "Added Credits through mpesa payments", + }) + + new_entry.insert(ignore_permissions=True) + frappe.db.commit() + frappe.msgprint(_("Balance Transaction entry created successfully")) + + return new_entry.name - # Save the new document to the database - new_entry.insert(ignore_permissions=True) - frappe.db.commit() # Commit the changes to the database - frappe.msgprint(_("Balance Transaction entry created successfully")) - -def after_save(doc, method=None): - team = doc.team - amount = doc.amount - create_balance_transaction(team, amount) +def after_save_mpesa_payment_record(doc, method=None): + team = doc.team + amount = doc.amount + + # Create the Balance Transaction and get the transaction name + balance_transaction_name = create_balance_transaction(team, amount) + + # Update Mpesa Payment Record with the balance transaction reference + doc.balance_transaction = balance_transaction_name + doc.docstatus=1 + doc.save() + doc.submit() + frappe.msgprint(_("Mpesa Payment Record has been linked with Balance Transaction.")) + \ No newline at end of file diff --git a/press/hooks.py b/press/hooks.py index a7d54a2791..e7fbadfc94 100644 --- a/press/hooks.py +++ b/press/hooks.py @@ -163,6 +163,9 @@ "Marketplace App Subscription": { "on_update": "press.press.doctype.storage_integration_subscription.storage_integration_subscription.create_after_insert", }, + "Mpesa Payment Record": { + "after_save":"press.press.api.billing.after_save_mpesa_payment_record" +}, } # Scheduled Tasks diff --git a/press/press/doctype/mpesa_payment_record/mpesa_payment_record.json b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.json index d5450ab521..522b48896d 100644 --- a/press/press/doctype/mpesa_payment_record/mpesa_payment_record.json +++ b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.json @@ -25,7 +25,8 @@ "posting_date", "posting_time", "default_currency", - "amended_from" + "amended_from", + "balance_transaction" ], "fields": [ { @@ -157,13 +158,19 @@ "fieldtype": "Float", "label": "Exchange Rate", "precision": "9" + }, + { + "fieldname": "balance_transaction", + "fieldtype": "Link", + "label": "Balance Transaction", + "options": "Balance Transaction" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-09-09 11:56:11.306741", + "modified": "2024-09-10 17:37:45.538455", "modified_by": "Administrator", "module": "Press", "name": "Mpesa Payment Record", diff --git a/press/press/doctype/mpesa_payment_record/mpesa_payment_record.py b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.py index 33b99881fc..00b794906c 100644 --- a/press/press/doctype/mpesa_payment_record/mpesa_payment_record.py +++ b/press/press/doctype/mpesa_payment_record/mpesa_payment_record.py @@ -16,6 +16,7 @@ class MpesaPaymentRecord(Document): amended_from: DF.Link | None amountusd: DF.Currency + balance_transaction: DF.Link | None bill_ref_number: DF.Data | None company: DF.Link | None default_currency: DF.Data | None From c052f65a80f0c0972615f35893a5883444ab8005 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Wed, 11 Sep 2024 19:14:54 +0300 Subject: [PATCH 05/78] feat - getting partner and team from integration request --- press/api/billing.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/press/api/billing.py b/press/api/billing.py index e30b964f74..11e9f57b19 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -7,6 +7,7 @@ import frappe from frappe.core.utils import find from frappe.utils import fmt_money +import json from press.press.doctype.team.team import ( has_unsettled_invoices, @@ -834,10 +835,9 @@ def request_for_payment(**kwargs): else: response = frappe._dict(generate_stk_push(**args)) - handle_api_response("CheckoutRequestID", args, response) - - -def handle_api_response(global_id, request_dict, response): + handle_api_mpesa_response("CheckoutRequestID", args, response) + +def handle_api_mpesa_response(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 if getattr(response, "requestId"): @@ -849,13 +849,13 @@ def handle_api_response(global_id, request_dict, response): error = None if not frappe.db.exists("Integration Request", req_name): - create_request_log(request_dict, "Host", "Mpesa", req_name, error) + create_request_log(request_dict, "Host", "Mpesa Express", req_name, error) if error: frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) def create_mpesa_payment_register_entry(transaction_response): - team = get_current_team() + #team = get_current_team() """Create a new entry in the Mpesa Payment Record for a successful transaction.""" # Extract necessary details from the transaction response item_response = transaction_response.get("CallbackMetadata", {}).get("Item", []) @@ -867,7 +867,8 @@ def create_mpesa_payment_register_entry(transaction_response): transaction_id=transaction_response.get("CheckoutRequestID") amount = fetch_param_value(item_response, "Amount", "Name") request_id=transaction_response.get("MerchantRequestID") - + team, partner = get_team_and_partner_from_integration_request(transaction_id) + # Create a new entry in Mpesa Payment Record new_entry = frappe.get_doc({ "doctype": "Mpesa Payment Record", @@ -878,6 +879,7 @@ def create_mpesa_payment_register_entry(transaction_response): "msisdn": msisdn, "trans_amount": amount, "merchant_request_id": request_id, + "payment_partner": partner, }) new_entry.insert(ignore_permissions=True) @@ -933,4 +935,24 @@ def after_save_mpesa_payment_record(doc, method=None): frappe.msgprint(_("Mpesa Payment Record has been linked with Balance Transaction.")) - \ No newline at end of file +def get_team_and_partner_from_integration_request(transaction_id): + """Get the team and partner associated with the integration request.""" + integration_request = frappe.get_doc("Integration Request", transaction_id) + + request_data = integration_request.get("request_data") + + # Parse the request_data as a dictionary + if request_data: + try: + request_data_dict = json.loads(request_data) + team = request_data_dict.get("team") + partner = request_data_dict.get("partner") + except json.JSONDecodeError: + frappe.throw(_("Invalid JSON format in request_data")) + team = None + partner = None + else: + team = None + partner = None + + return team, partner From 162183d2d85f068233975a17a2438a6ce8d10df4 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Thu, 12 Sep 2024 17:49:56 +0300 Subject: [PATCH 06/78] feat - getting mpesa settings from partner --- press/api/billing.py | 174 ++++++++++++++++++++----------------------- 1 file changed, 81 insertions(+), 93 deletions(-) diff --git a/press/api/billing.py b/press/api/billing.py index 11e9f57b19..e4e0c27024 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -694,13 +694,19 @@ def split_request_amount_according_to_transaction_limit(amount, transaction_limi def generate_stk_push(**kwargs): """Generate stk push by making a API call to the stk push API.""" args = frappe._dict(kwargs) + + # Fetch the team document + partner = frappe.get_doc("Team", args.partner) + + # Get Mpesa settings for the partner's team + mpesa_settings = get_mpesa_settings_for_team(partner.name) + try: callback_url = ( get_request_site_address(True) + "/api/method/press.press.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" # for sandbox, business shortcode is same as till number business_shortcode = ( @@ -734,6 +740,13 @@ def generate_stk_push(**kwargs): title=_("Mpesa Express Error"), ) +def get_mpesa_settings_for_team(team_name): + """Fetch Mpesa settings for a given team.""" + mpesa_settings = frappe.get_all("Mpesa Settings", filters={"team": team_name}, pluck="name") + if not mpesa_settings: + frappe.throw(_("Mpesa Settings not configured for this team"), title=_("Mpesa Express Error")) + return frappe.get_doc("Mpesa Settings", mpesa_settings[0]) + '''Verify transaction after push notification''' @frappe.whitelist(allow_guest=True) @@ -750,38 +763,14 @@ def verify_pesa_transaction(**kwargs): # Retrieve the corresponding Integration Request document integration_request = frappe.get_doc("Integration Request", checkout_id) - transaction_data = frappe._dict(loads(integration_request.data)) - total_paid = 0 - success = False - - if transaction_response["ResultCode"] == 0: # Transaction was successful - if integration_request.reference_doctype and integration_request.reference_docname: - try: - item_response = transaction_response["CallbackMetadata"]["Item"] - amount = fetch_param_value(item_response, "Amount", "Name") - mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") - - # Fetch the document associated with the payment - pr = frappe.get_doc( - integration_request.reference_doctype, integration_request.reference_docname - ) - - # Get completed payments and receipts for the integration request - 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]) - pr.run_method("on_payment_authorized", "Completed") - integration_request.handle_success(transaction_response) - - # Call function to create a new Mpesa Payment Record entry - create_mpesa_payment_register_entry(transaction_response) - except Exception: - integration_request.handle_failure(transaction_response) - frappe.log_error("Mpesa: Failed to verify transaction") + if transaction_response["ResultCode"] == 0: + try: + integration_request.handle_success(transaction_response) + create_mpesa_payment_register_entry(transaction_response) + except Exception: + integration_request.handle_failure(transaction_response) + frappe.log_error("Mpesa: Failed to verify transaction") else: integration_request.handle_failure(transaction_response) @@ -855,7 +844,6 @@ def handle_api_mpesa_response(global_id, request_dict, response): frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error")) def create_mpesa_payment_register_entry(transaction_response): - #team = get_current_team() """Create a new entry in the Mpesa Payment Record for a successful transaction.""" # Extract necessary details from the transaction response item_response = transaction_response.get("CallbackMetadata", {}).get("Item", []) @@ -888,71 +876,71 @@ def create_mpesa_payment_register_entry(transaction_response): def create_balance_transaction(team, amount, invoice=None): - """Create a new entry in the Balance Transaction table.""" + """Create a new entry in the Balance Transaction table.""" - # Get the ending balance of this team - team_balance_transaction = frappe.get_all( - "Balance Transaction", - filters={"team": team}, - fields=["ending_balance"], - order_by="creation desc", - limit=1 - ) - ending_balance = team_balance_transaction[0].ending_balance if team_balance_transaction else 0 - - # Create a new entry in the Balance Transaction table - new_entry = frappe.get_doc({ - "doctype": "Balance Transaction", - "team": team, - "type": "Adjustment", - "amount": amount, - "source": "Prepaid Credits", - "ending_balance": ending_balance + amount, - "docstatus": 1, - "invoice": invoice if invoice else None, - "description": "Added Credits through mpesa payments", - }) - - new_entry.insert(ignore_permissions=True) - frappe.db.commit() - frappe.msgprint(_("Balance Transaction entry created successfully")) - - return new_entry.name + # Get the ending balance of this team + team_balance_transaction = frappe.get_all( + "Balance Transaction", + filters={"team": team}, + fields=["ending_balance"], + order_by="creation desc", + limit=1 + ) + ending_balance = team_balance_transaction[0].ending_balance if team_balance_transaction else 0 + + # Create a new entry in the Balance Transaction table + new_entry = frappe.get_doc({ + "doctype": "Balance Transaction", + "team": team, + "type": "Adjustment", + "amount": amount, + "source": "Prepaid Credits", + "ending_balance": ending_balance + amount, + "docstatus": 1, + "invoice": invoice if invoice else None, + "description": "Added Credits through mpesa payments", + }) + + new_entry.insert(ignore_permissions=True) + frappe.db.commit() + frappe.msgprint(_("Balance Transaction entry created successfully")) + + return new_entry.name def after_save_mpesa_payment_record(doc, method=None): - team = doc.team - amount = doc.amount + team = doc.team + amount = doc.amount - # Create the Balance Transaction and get the transaction name - balance_transaction_name = create_balance_transaction(team, amount) - - # Update Mpesa Payment Record with the balance transaction reference - doc.balance_transaction = balance_transaction_name - doc.docstatus=1 - doc.save() - doc.submit() - frappe.msgprint(_("Mpesa Payment Record has been linked with Balance Transaction.")) + # Create the Balance Transaction and get the transaction name + balance_transaction_name = create_balance_transaction(team, amount) + + # Update Mpesa Payment Record with the balance transaction reference + doc.balance_transaction = balance_transaction_name + doc.docstatus=1 + doc.save() + doc.submit() + frappe.msgprint(_("Mpesa Payment Record has been linked with Balance Transaction.")) def get_team_and_partner_from_integration_request(transaction_id): - """Get the team and partner associated with the integration request.""" - integration_request = frappe.get_doc("Integration Request", transaction_id) - - request_data = integration_request.get("request_data") - - # Parse the request_data as a dictionary - if request_data: - try: - request_data_dict = json.loads(request_data) - team = request_data_dict.get("team") - partner = request_data_dict.get("partner") - except json.JSONDecodeError: - frappe.throw(_("Invalid JSON format in request_data")) - team = None - partner = None - else: - team = None - partner = None - - return team, partner + """Get the team and partner associated with the integration request.""" + integration_request = frappe.get_doc("Integration Request", transaction_id) + + request_data = integration_request.get("request_data") + + # Parse the request_data as a dictionary + if request_data: + try: + request_data_dict = json.loads(request_data) + team = request_data_dict.get("team") + partner = request_data_dict.get("partner") + except json.JSONDecodeError: + frappe.throw(_("Invalid JSON format in request_data")) + team = None + partner = None + else: + team = None + partner = None + + return team, partner From 8fd808990884ebf95b85d6a18d48ce20f6f88891 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Sun, 15 Sep 2024 00:38:54 +0300 Subject: [PATCH 07/78] feta - adding mpesa button, frontend --- dashboard/src/assets/mpesa.svg | 27 ++++ .../src2/components/BuyPrepaidCreditMpesa.vue | 134 ++++++++++++++++++ .../src2/components/BuyPrepaidCreditsForm.vue | 31 +++- 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 dashboard/src/assets/mpesa.svg create mode 100644 dashboard/src2/components/BuyPrepaidCreditMpesa.vue diff --git a/dashboard/src/assets/mpesa.svg b/dashboard/src/assets/mpesa.svg new file mode 100644 index 0000000000..d24056df00 --- /dev/null +++ b/dashboard/src/assets/mpesa.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/src2/components/BuyPrepaidCreditMpesa.vue b/dashboard/src2/components/BuyPrepaidCreditMpesa.vue new file mode 100644 index 0000000000..ff4b7d47f4 --- /dev/null +++ b/dashboard/src2/components/BuyPrepaidCreditMpesa.vue @@ -0,0 +1,134 @@ + + + + diff --git a/dashboard/src2/components/BuyPrepaidCreditsForm.vue b/dashboard/src2/components/BuyPrepaidCreditsForm.vue index 5d2882887f..f0c7b6fabb 100644 --- a/dashboard/src2/components/BuyPrepaidCreditsForm.vue +++ b/dashboard/src2/components/BuyPrepaidCreditsForm.vue @@ -68,6 +68,24 @@ alt="Stripe Logo" /> + + + @@ -87,16 +105,27 @@ @success="onSuccess" @cancel="onCancel" /> + + + + --> + + + diff --git a/dashboard/src2/components/BuyPrepaidCreditsForm.vue b/dashboard/src2/components/BuyPrepaidCreditsForm.vue index f0c7b6fabb..f5ccd621d6 100644 --- a/dashboard/src2/components/BuyPrepaidCreditsForm.vue +++ b/dashboard/src2/components/BuyPrepaidCreditsForm.vue @@ -82,8 +82,8 @@ > Stripe Logo diff --git a/press/api/billing.py b/press/api/billing.py index e4e0c27024..d2c69922ff 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -4,6 +4,7 @@ from itertools import groupby from typing import Dict, List + import frappe from frappe.core.utils import find from frappe.utils import fmt_money @@ -28,6 +29,11 @@ from press.press.doctype.mpesa_settings.mpesa_connector import MpesaConnector from json import dumps, loads from frappe.integrations.utils import create_request_log +from frappe import _ # Import this for translation functionality +import json + +# other imports and the rest of your code... + supported_mpesa_currencies = ["KES"] @@ -703,8 +709,8 @@ def generate_stk_push(**kwargs): try: callback_url = ( - get_request_site_address(True) - + "/api/method/press.press.doctype.mpesa_settings.mpesa_settings.verify_transaction" + # get_request_site_address(True) + "https://635d-41-80-112-111.ngrok-free.app"+ "/api/method/press.api.billing.verify_mpesa_transaction" ) env = "production" if not mpesa_settings.sandbox else "sandbox" @@ -720,7 +726,7 @@ def generate_stk_push(**kwargs): ) mobile_number = sanitize_mobile_number(args.sender) - + response = connector.stk_push( business_shortcode=business_shortcode, amount=args.request_amount, @@ -730,7 +736,7 @@ def generate_stk_push(**kwargs): phone_number=mobile_number, description="Frappe Cloud Payment", ) - + print(str(response)) return response except Exception: @@ -739,18 +745,19 @@ def generate_stk_push(**kwargs): _("Issue detected with Mpesa configuration, check the error logs for more details"), title=_("Mpesa Express Error"), ) - + def get_mpesa_settings_for_team(team_name): - """Fetch Mpesa settings for a given team.""" - mpesa_settings = frappe.get_all("Mpesa Settings", filters={"team": team_name}, pluck="name") - if not mpesa_settings: - frappe.throw(_("Mpesa Settings not configured for this team"), title=_("Mpesa Express Error")) - return frappe.get_doc("Mpesa Settings", mpesa_settings[0]) + """Fetch Mpesa settings for a given team.""" + mpesa_settings = frappe.get_all("Mpesa Settings", filters={"team": team_name}, pluck="name") + if not mpesa_settings: + frappe.throw(_("Mpesa Settings not configured for this team"), title=_("Mpesa Express Error")) + return frappe.get_doc("Mpesa Settings", mpesa_settings[0]) '''Verify transaction after push notification''' @frappe.whitelist(allow_guest=True) -def verify_pesa_transaction(**kwargs): +def verify_mpesa_transaction(**kwargs): + print("Working here") """Verify the transaction result received via callback from STK.""" if "Body" not in kwargs or "stkCallback" not in kwargs["Body"]: frappe.throw(_("Invalid transaction response format")) @@ -776,6 +783,9 @@ def verify_pesa_transaction(**kwargs): integration_request.handle_failure(transaction_response) +# @frappe.whitelist(allow_guest=True) +# def hello_test(): +# print("Hello sir") '''fetch parameters from the args''' def fetch_param_value(response, key, key_field): """Fetch the specified key from list of dictionary. Key is identified via the key field.""" @@ -809,9 +819,20 @@ def get_completed_integration_requests_info(reference_doctype, reference_docname return mpesa_receipts, completed_payments '''request for payments''' -def request_for_payment(**kwargs): +@frappe.whitelist(allow_guest=True) +#def request_for_payment(**kwargs) +def request_for_payment(): + kwargs={ + "partner": "ijlpjgrgr7", + "sender": "0740743521", + "request_amount": 1, # Amount in Kenyan Shillings (Ksh) + "reference_doctype": "Invoice", + "reference_docname": "INV-2024-00006", + "transaction_limit":150000 +} + args = frappe._dict(kwargs) - request_amounts = split_request_amount_according_to_transaction_limit(args.request_amount) + request_amounts = split_request_amount_according_to_transaction_limit(args.request_amount, args.transaction_limit) for i, amount in enumerate(request_amounts): args.request_amount = amount From d330ffa01bdf0bd22966b284fa4add3bb70c0b31 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Wed, 18 Sep 2024 21:01:50 +0300 Subject: [PATCH 09/78] feat - extending BuyPrepaidCreditsForm to cater for KES and Egyption currency(need fix on egyptian currency)" ; --- .../src/components/PrepaidCreditsDialog.vue | 2 +- .../src2/components/BuyPrepaidCreditsForm.vue | 68 +++++++++++++++---- press/api/billing.py | 36 ++++++---- press/hooks.py | 2 +- press/public/images/mpesa.svg | 27 ++++++++ 5 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 press/public/images/mpesa.svg diff --git a/dashboard/src/components/PrepaidCreditsDialog.vue b/dashboard/src/components/PrepaidCreditsDialog.vue index e63553dbaf..934321c6cc 100644 --- a/dashboard/src/components/PrepaidCreditsDialog.vue +++ b/dashboard/src/components/PrepaidCreditsDialog.vue @@ -5,7 +5,7 @@ > diff --git a/press/press/doctype/currency_exchange/__init__.py b/press/press/doctype/currency_exchange/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/currency_exchange/currency_exchange.js b/press/press/doctype/currency_exchange/currency_exchange.js new file mode 100644 index 0000000000..1e749e9a6c --- /dev/null +++ b/press/press/doctype/currency_exchange/currency_exchange.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Currency Exchange", { +// refresh(frm) { + +// }, +// }); diff --git a/press/press/doctype/currency_exchange/currency_exchange.json b/press/press/doctype/currency_exchange/currency_exchange.json new file mode 100644 index 0000000000..4baacf5ac4 --- /dev/null +++ b/press/press/doctype/currency_exchange/currency_exchange.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-09-17 15:41:44.281942", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_c3nz" + ], + "fields": [ + { + "fieldname": "section_break_c3nz", + "fieldtype": "Section Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-09-17 15:41:44.281942", + "modified_by": "Administrator", + "module": "Press", + "name": "Currency Exchange", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/press/press/doctype/currency_exchange/currency_exchange.py b/press/press/doctype/currency_exchange/currency_exchange.py new file mode 100644 index 0000000000..ee63943f19 --- /dev/null +++ b/press/press/doctype/currency_exchange/currency_exchange.py @@ -0,0 +1,19 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class CurrencyExchange(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + + # end: auto-generated types + pass diff --git a/press/press/doctype/currency_exchange/test_currency_exchange.py b/press/press/doctype/currency_exchange/test_currency_exchange.py new file mode 100644 index 0000000000..461286a897 --- /dev/null +++ b/press/press/doctype/currency_exchange/test_currency_exchange.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestCurrencyExchange(FrappeTestCase): + pass diff --git a/press/press/doctype/payment_gateway/__init__.py b/press/press/doctype/payment_gateway/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/payment_gateway/payment_gateway.js b/press/press/doctype/payment_gateway/payment_gateway.js new file mode 100644 index 0000000000..a8edaa0051 --- /dev/null +++ b/press/press/doctype/payment_gateway/payment_gateway.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Payment Gateway", { +// refresh(frm) { + +// }, +// }); diff --git a/press/press/doctype/payment_gateway/payment_gateway.json b/press/press/doctype/payment_gateway/payment_gateway.json new file mode 100644 index 0000000000..9aca2ccaba --- /dev/null +++ b/press/press/doctype/payment_gateway/payment_gateway.json @@ -0,0 +1,125 @@ +{ + "actions": [], + "autoname": "field:gateway", + "creation": "2024-09-13 13:25:09.836216", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "team", + "column_break_kvnc", + "currency", + "column_break_pwgv", + "gateway", + "ui_configuration_section", + "integration_logo", + "partner_integration_section", + "url", + "column_break_oefu", + "api_key", + "column_break_slwm", + "api_secret", + "taxes_section", + "taxes_and_charges" + ], + "fields": [ + { + "fieldname": "gateway", + "fieldtype": "Data", + "label": "Gateway", + "unique": 1 + }, + { + "fieldname": "column_break_kvnc", + "fieldtype": "Column Break" + }, + { + "fieldname": "team", + "fieldtype": "Link", + "label": "Team", + "options": "Team" + }, + { + "fieldname": "column_break_pwgv", + "fieldtype": "Column Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "taxes_section", + "fieldtype": "Section Break", + "label": "Taxes" + }, + { + "fieldname": "taxes_and_charges", + "fieldtype": "Percent", + "label": "Taxes and Charges" + }, + { + "fieldname": "partner_integration_section", + "fieldtype": "Section Break", + "label": "Partner Integration" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL" + }, + { + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key" + }, + { + "fieldname": "api_secret", + "fieldtype": "Password", + "label": "API Secret" + }, + { + "fieldname": "column_break_oefu", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_slwm", + "fieldtype": "Column Break" + }, + { + "fieldname": "ui_configuration_section", + "fieldtype": "Section Break", + "label": "UI Configuration" + }, + { + "fieldname": "integration_logo", + "fieldtype": "Attach Image", + "label": "Integration Logo" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-09-17 15:39:34.507841", + "modified_by": "Administrator", + "module": "Press", + "name": "Payment Gateway", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/press/press/doctype/payment_gateway/payment_gateway.py b/press/press/doctype/payment_gateway/payment_gateway.py new file mode 100644 index 0000000000..e6b0fe7b2f --- /dev/null +++ b/press/press/doctype/payment_gateway/payment_gateway.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PaymentGateway(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + api_key: DF.Data | None + api_secret: DF.Password | None + currency: DF.Link | None + gateway: DF.Data | None + integration_logo: DF.AttachImage | None + taxes_and_charges: DF.Percent + team: DF.Link | None + url: DF.Data | None + # end: auto-generated types + pass diff --git a/press/press/doctype/payment_gateway/test_payment_gateway.py b/press/press/doctype/payment_gateway/test_payment_gateway.py new file mode 100644 index 0000000000..9e3f993f27 --- /dev/null +++ b/press/press/doctype/payment_gateway/test_payment_gateway.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPaymentGateway(FrappeTestCase): + pass diff --git a/press/press/doctype/payment_partner_balance_transaction/__init__.py b/press/press/doctype/payment_partner_balance_transaction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.js b/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.js new file mode 100644 index 0000000000..ad1bd9aa6f --- /dev/null +++ b/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Payment Partner Balance Transaction", { +// refresh(frm) { + +// }, +// }); diff --git a/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.json b/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.json new file mode 100644 index 0000000000..caa9420829 --- /dev/null +++ b/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.json @@ -0,0 +1,136 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-09-13 13:15:49.154039", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "partner_details_section", + "payment_partner", + "column_break_manc", + "payment_gateway", + "column_break_ejza", + "team", + "transaction_details_section", + "amount", + "column_break_xqsh", + "currency", + "column_break_jyxx", + "exchange_rate", + "section_break_yhqq", + "payment_transaction_details", + "section_break_7oh3", + "amended_from" + ], + "fields": [ + { + "fieldname": "section_break_7oh3", + "fieldtype": "Section Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Payment Partner Balance Transaction", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "transaction_details_section", + "fieldtype": "Section Break", + "label": "Transaction Details" + }, + { + "default": "USD", + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount" + }, + { + "fieldname": "column_break_xqsh", + "fieldtype": "Column Break" + }, + { + "fieldname": "partner_details_section", + "fieldtype": "Section Break", + "label": "Team Details" + }, + { + "fieldname": "payment_partner", + "fieldtype": "Link", + "label": "Payment Partner", + "options": "Team" + }, + { + "fieldname": "column_break_ejza", + "fieldtype": "Column Break" + }, + { + "fieldname": "team", + "fieldtype": "Link", + "label": "Team", + "options": "Team" + }, + { + "fieldname": "column_break_jyxx", + "fieldtype": "Column Break" + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate" + }, + { + "fieldname": "column_break_manc", + "fieldtype": "Column Break" + }, + { + "fieldname": "payment_gateway", + "fieldtype": "Link", + "label": "Payment Gateway", + "options": "Payment Gateway" + }, + { + "fieldname": "section_break_yhqq", + "fieldtype": "Section Break" + }, + { + "fieldname": "payment_transaction_details", + "fieldtype": "JSON", + "label": "Payment Transaction Details" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2024-09-13 13:38:50.952893", + "modified_by": "Administrator", + "module": "Press", + "name": "Payment Partner Balance Transaction", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.py b/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.py new file mode 100644 index 0000000000..e77f090f7e --- /dev/null +++ b/press/press/doctype/payment_partner_balance_transaction/payment_partner_balance_transaction.py @@ -0,0 +1,33 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class PaymentPartnerBalanceTransaction(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + amended_from: DF.Link | None + amount: DF.Currency + currency: DF.Link | None + exchange_rate: DF.Float + payment_gateway: DF.Link | None + payment_partner: DF.Link | None + payment_transaction_details: DF.JSON | None + team: DF.Link | None + # end: auto-generated types + def on_submit(self): + team = frappe.get_doc("Team", self.team) + # In case of Egypt Billing + credit_amount = self.amount - (self.amount * (13/100)) + self.currency = "EGP" + self.exchange_rate = 48 + credit_amount = credit_amount /self.exchange_rate + team.allocate_credit_amount(credit_amount, "Prepaid Credits") diff --git a/press/press/doctype/payment_partner_balance_transaction/test_payment_partner_balance_transaction.py b/press/press/doctype/payment_partner_balance_transaction/test_payment_partner_balance_transaction.py new file mode 100644 index 0000000000..0672d79744 --- /dev/null +++ b/press/press/doctype/payment_partner_balance_transaction/test_payment_partner_balance_transaction.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPaymentPartnerBalanceTransaction(FrappeTestCase): + pass diff --git a/press/public/images/paymobLogo.png b/press/public/images/paymobLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..36c1d5ce909bfbdec8e98c4b5b4fc925fb8e100f GIT binary patch literal 3165 zcmai%2{e>#8^_1`Y9_LUY(q1HQRKB0vNy<*Jt9kpv5XKVA;wtBk}YWlY3zG6jICm_ zuUXSrQ!0_IP-E?zc9R=x8G zGXQ{2gQoRa7--zhxus9LF!*WdS+KCMOuRLoq1|6GF|^R8Edzf10N{TBFux-1mpE97 z-cO_c#>jn5W7x0C2%4b&j0hSx0TG)&iT%wj0A>q--4B|;Yy+^{fUtcYXd1(R#b0{g zEu0Dn-#4N0chH<^5x(#Fcha1Hr+v`}K>SYsd%X65D=D-S&+|7kG6&ExF|z&+Ci%guug9++gcX-Yl z>OKUjrAQHQvD5ssx@hT2Ty)y&Rmk^KWE?hO=OKOI2K`<5C(Br{l6!dj3pWj-H(k5D zJOL0&1ULh2`k{3s4QF5jjxKQfXWcdCnN5&<0b%YKM)%|Z4y%?+U9!gf$y#m(c!T%Y znJ3NIZq1Bx%ijIIUa|ejsxCZh*;4_GAFJ3Epn6^9+~KRf?h>Q&kh0~dxVi~^mT#m9 zSg2L)nrU=#?D^md{Ld`Xe(^;L!3x`(2Qv<9R7REQX4R*-+2?N&h zc8t$8Fc5Riz&jh)gmb0_TTCL~4RpL#mPQNK>dKh0uP5CcC3=W@#$d`f+`Zk&-c-Hr zD1%$G?R+azrKf6PEDnXLr7a4)5^QwpU)jb(s%KpzDT)4*JbUPKklf}om%NpO+8@}D zoQUo&({A-mpAnC9d7Gb!4w+X}Yh)RkW6tWUZD-y&Z^a&`_5`n+Y-(Bq-9zEhWDu1~ z;VFk?GakHND(pL@WbbIh*zvhysQvp*uY8Eik0KavifJ4vben{@@mX+kFr6h;S39G@kxFqli`9@7K3iiwRGwtZ> zqMpyXNRh4lO@kA3&B8et14&=y1<>i2dBepQgJVhMiDGoC!lQwf=NGjpKy~`AB1FnP zKEaci9jH!5Cv-t|{3|G|K-p=J5?Af@qk*DQX`#TYRS(U@aD2oXoL2Gx3M+1K2zz=`xaL|ZWVXod{ufwz^=`;kicXE8Pj_|^Rna3;haE%YK~&8 zU-8|BSmIvKu%eM8&wFC=3AV%MC`{$6lKe0UpNlrVM@IrBWi0K~3cKi)YF^N-tTD1j zx>zht8*47JkkwVH(o61307Zh=S(Hb1R-OarVRd1R`9Bf;V)rea2v*ROhIy-L#6os< z=b46tuQcuwZ zI}Fbz(N8u_6!7rJba2g62sv6RR5g#tjd0t9ViB{3Ab9dD_){tu2icr58xbT340!XI zjWVM3^quuLx{Cl;D}GG8KDR+mswj`R2u@i_n+-KnVztm1t!kfZ+)2OiL1h%3Axt*J z^>!SS!AT_bK=%-}7i7Fd>NAd&C|w=?1_)LeYtP7i^sKFfM>5u0$jAC^)qTb6@N{vd zB+y+`Ff&0k;zeop+br}r4$Dl4x1_y=#!mA?=?_z$I?E@;K}$_KsO{Qg=GAs|(l5Re z8}p4HEzre>uWb($@YwL)lx*v-IfX9d!-OEX@bNN7^o5-J5%1Fm{``okLwTT_Bk`fr zp0tBQztW{!hn|-`DI_#M^@LlslPrJAUMJ~d2Y&Gj+jgMg;cRfNrkX;=D})mlb`inK z0z4^O>(WnNY&6KC++F-apW%ZAlJl8$9z@Aw=a-_}S3g7Ai9u2-s-eZt;hg|5#>L#B#y5-}cF&Z$ z1;nK*L#;QM!ixXkEj58ml0y& zcYRkq)wy7B`?-C=Da&jverKIadJ(X9B)mdrez3v);)daNZYn0|qcn61?B)gvkaCN) z>wWzF@?}rbI58-<$Gn$Vyz@RgOSU^JHu^znuyyGe3cW=xYaX=CZh4DiH`zIId2w-G zVajgnCE}QeONp`w>D@49PXF^_9)MCYxb=z9{ z0m06jMTtcXiXZ9a`rylQ6)UwQ2&aF~4W6@SPnRq>H1rnKuV+p?6T%UTnZXN29+`t`7;8Bkxx)a9}d zpJi^YJU}omerux#DHZB7H~Q2w&Opmqf&<~cf#%;%X%#b>_T562DEp)QETn?s2KfVF=kxa-R^&;YwpFakA+D3@ST34|D0n(-Si2wiq literal 0 HcmV?d00001 From 33bb5ebf8d49e43cb81024dd56a50ad3143866ee Mon Sep 17 00:00:00 2001 From: maniamartial Date: Thu, 19 Sep 2024 17:43:48 +0300 Subject: [PATCH 11/78] feat - get arguments from frontend for mpesa --- .../src2/components/BuyPrepaidCreditMpesa.vue | 292 ++++++++++-------- .../src2/components/BuyPrepaidCreditsForm.vue | 44 ++- press/api/billing.py | 50 ++- press/public/marketplace/js/call.js | 1 + 4 files changed, 233 insertions(+), 154 deletions(-) diff --git a/dashboard/src2/components/BuyPrepaidCreditMpesa.vue b/dashboard/src2/components/BuyPrepaidCreditMpesa.vue index 02a86f1940..76f3c80a36 100644 --- a/dashboard/src2/components/BuyPrepaidCreditMpesa.vue +++ b/dashboard/src2/components/BuyPrepaidCreditMpesa.vue @@ -1,125 +1,4 @@ - + + + + + + \ No newline at end of file diff --git a/dashboard/src2/components/BuyPrepaidCreditsForm.vue b/dashboard/src2/components/BuyPrepaidCreditsForm.vue index 1ff0a23533..b1ee10b345 100644 --- a/dashboard/src2/components/BuyPrepaidCreditsForm.vue +++ b/dashboard/src2/components/BuyPrepaidCreditsForm.vue @@ -20,6 +20,23 @@ + + + + - +--> \ No newline at end of file diff --git a/dashboard/src2/pages/BillingOverview.vue b/dashboard/src2/pages/BillingOverview.vue index 4112fdd00f..8e727773ce 100644 --- a/dashboard/src2/pages/BillingOverview.vue +++ b/dashboard/src2/pages/BillingOverview.vue @@ -90,6 +90,21 @@ Not set + +
+
+
Add M-Pesa Express Credentials
+ +
+
+ Not set +
+
+ + + + +
@@ -146,6 +161,7 @@ import { defineAsyncComponent } from 'vue'; import InvoiceTable from '../components/InvoiceTable.vue'; import UpdateBillingDetails from '../components/UpdateBillingDetails.vue'; +import AddMpesaCredentials from '../components/AddMpesaCredentials.vue'; export default { name: 'BillingOverview', @@ -160,6 +176,9 @@ export default { ), StripeCardDialog: defineAsyncComponent(() => import('../components/StripeCardDialog.vue') + ), + AddMpesaCredentials: defineAsyncComponent(() => + import('../components/AddMpesaCredentials.vue') ) }, resources: { @@ -177,7 +196,8 @@ export default { showChangeModeDialog: false, showBillingDetailsDialog: false, showAddCardDialog: false, - showUpcomingInvoiceDialog: false + showUpcomingInvoiceDialog: false, + showAddMpesaDialog: false }; }, mounted() { diff --git a/press/api/billing.py b/press/api/billing.py index a821a1fe92..4c114082e6 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -882,9 +882,8 @@ def create_mpesa_payment_register_entry(transaction_response): amount = fetch_param_value(item_response, "Amount", "Name") request_id=transaction_response.get("MerchantRequestID") team, partner = get_team_and_partner_from_integration_request(transaction_id) - print("Partner", partner) amount_usd, exchange_rate=convert("KES", "USD", amount) - # Create a new entry in M-pesa Payment Record + # Create a new entry in M-Pesa Payment Record new_entry = frappe.get_doc({ "doctype": "Mpesa Payment Record", "transaction_id": transaction_id, @@ -941,10 +940,10 @@ def create_balance_transaction(team, amount, invoice=None): def after_save_mpesa_payment_record(doc, method=None): - try: team = doc.team - amount = doc.trans_amount + # amount = doc.trans_amount + amount = doc.amount_usd balance_transaction_name = create_balance_transaction(team, amount) @@ -1007,7 +1006,6 @@ def get_tax_percentage(payment_partner): mpesa_settings=frappe.get_all("Mpesa Settings", filters={"api_type":"Mpesa Express", "team":team_doc.name}, fields=["name"]) for mpesa_setting in mpesa_settings: payment_gateways = frappe.get_all("Payment Gateway", filters={"gateway_settings":"Mpesa settings","gateway_controller":mpesa_setting }, fields=["taxes_and_charges"]) - print("hello",payment_gateways) if payment_gateways: taxes_and_charges = payment_gateways[0].taxes_and_charges return taxes_and_charges @@ -1017,4 +1015,83 @@ def convert(from_currency, to_currency, amount): exchange_rate = frappe.get_value("Currency Exchange", {"from_currency": from_currency, "to_currency": to_currency}, "exchange_rate") converted_amount = amount * exchange_rate - return converted_amount, exchange_rate \ No newline at end of file + return converted_amount, exchange_rate + +@frappe.whitelist(allow_guest=True) +def webhook_trial(): + print("Receiving webhook data...") + + rawdata = frappe.local.request.get_data(as_text=True) + + try: + data = json.loads(rawdata) + except Exception as e: + frappe.log_error(f"Webhook data could not be parsed: {e}") + frappe.throw(_("Error parsing the webhook data")) + + transaction_id = data.get("transaction_id") + trans_amount = data.get("trans_amount") + msisdn = data.get("msisdn") + team = data.get("team") + default_currency = data.get("default_currency") + + if not transaction_id or not trans_amount: + frappe.throw(_("Invalid transaction data received")) + + # Step 3: Create a new Sales Invoice + try: + sales_invoice = frappe.get_doc({ + "doctype": "Invoice", + "team": team, + "due_date": frappe.utils.nowdate(), + "currency": default_currency, + "items": [{ + "item_name": "Mpesa Payment", + "description": f"Payment via Mpesa transaction {transaction_id}", + "quantity": 1, + "rate": float(trans_amount) + }], + "remarks": f"Mpesa Transaction ID: {transaction_id}", + "mpesa_transaction_id": transaction_id, + "status":"Paid" + }) + + # Step 4: Insert the Sales Invoice into the database + sales_invoice.insert(ignore_permissions=True) + sales_invoice.submit() + frappe.db.commit() + frappe.msgprint(_("Sales Invoice created successfully")) + + return {"status": "success", "invoice_id": sales_invoice.name} + + except Exception as e: + print(f"Error creating Sales Invoice: {e}") + frappe.throw(_("Failed to create Sales Invoice")) + +@frappe.whitelist(allow_guest=True) +def create_mpesa_settings(**kwargs): + """Create Mpesa Settings for the team.""" + team = get_current_team() + + try: + mpesa_settings = frappe.get_doc({ + "doctype": "Mpesa Settings", + "team": team, + "payment_gateway_name": kwargs.get("payment_gateway_name"), + "api_type": "Mpesa Express", + "consumer_key": kwargs.get("consumer_key"), + "consumer_secret": kwargs.get("consumer_secret"), + "business_shortcode": kwargs.get("short_code"), + "till_number": kwargs.get("till_number"), + "online_passkey": kwargs.get("pass_key"), + "security_credential": kwargs.get("security_credential"), + "sandbox": 1 if kwargs.get("sandbox") else 0, + }) + + mpesa_settings.insert(ignore_permissions=True) + frappe.db.commit() + + return mpesa_settings.name + except Exception as e: + frappe.log_error(message=f"Error creating Mpesa Settings: {str(e)}", title="M-Pesa Settings Creation Error") + return None From 91b67aca09366ddd97c4d3d743122d090dbde4f5 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Mon, 30 Sep 2024 15:37:21 +0300 Subject: [PATCH 18/78] feat(UI): added Buy Prepaid Credits Paymob component --- .../src2/components/BuyPrepaidCreditsForm.vue | 30 +++++- .../components/BuyPrepaidCreditsPaymob.vue | 101 +++++++++++------- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/dashboard/src2/components/BuyPrepaidCreditsForm.vue b/dashboard/src2/components/BuyPrepaidCreditsForm.vue index f64164bb03..a3ceaf1eba 100644 --- a/dashboard/src2/components/BuyPrepaidCreditsForm.vue +++ b/dashboard/src2/components/BuyPrepaidCreditsForm.vue @@ -109,6 +109,22 @@ alt="Stripe Logo" /> + +
@@ -129,6 +130,7 @@ + +
+ Not set +
+ + + +
@@ -162,6 +175,7 @@ import { defineAsyncComponent } from 'vue'; import InvoiceTable from '../components/InvoiceTable.vue'; import UpdateBillingDetails from '../components/UpdateBillingDetails.vue'; import AddMpesaCredentials from '../components/AddMpesaCredentials.vue'; +import AddPaymentGateway from '../components/AddPaymentGateway.vue'; export default { name: 'BillingOverview', @@ -179,6 +193,9 @@ export default { ), AddMpesaCredentials: defineAsyncComponent(() => import('../components/AddMpesaCredentials.vue') + ), + AddPaymentGateway: defineAsyncComponent(() => + import('../components/AddPaymentGateway.vue') ) }, resources: { @@ -197,7 +214,8 @@ export default { showBillingDetailsDialog: false, showAddCardDialog: false, showUpcomingInvoiceDialog: false, - showAddMpesaDialog: false + showAddMpesaDialog: false, + showAddPaymentGatewayDialog: false }; }, mounted() { diff --git a/press/api/billing.py b/press/api/billing.py index 15ca414a31..ac425582c6 100644 --- a/press/api/billing.py +++ b/press/api/billing.py @@ -1147,6 +1147,90 @@ def get_exchange_rate(from_currency, to_currency): "Currency Exchange", {"from_currency": from_currency, "to_currency": to_currency}, "exchange_rate", - order_by="creation DESC" # Fetch the latest record by creation date + order_by="creation DESC" ) return exchange_rate + +# @frappe.whitelist(allow_guest=True) +# def create_payment_gateway_settings(**kwargs): +# """Create Payment Gateway Settings for the team.""" +# team = get_current_team() +# args=frappe._dict(kwargs) +# print(str(args)) +# try: +# payment_gateway_settings = frappe.get_doc({ +# "doctype": "Payment Gateway", +# "team": team, +# "gateway": args.get("gateway_name"), +# "currency": args.get("currency"), +# "integration_logo": kwargs.get("integration_logo"), +# "gateway_settings": args.get("gateway_setting"), +# "gateway_controller": args.get("gateway_controller"), +# "url": args.get("url"), +# "api_key": args.get("api_key"), +# "api_secret": args.get("api_secret"), +# "taxes_and_charges": args.get("taxes_and_charges"), +# }) + +# payment_gateway_settings.insert(ignore_permissions=True) + +# frappe.db.commit() + +# return payment_gateway_settings.name +# except Exception as e: +# print(str(e)) +# frappe.log_error(message=f"Error creating Payment Gateway Settings: {str(e)}", title="Payment Gateway Settings Creation Error") +# return None +@frappe.whitelist(allow_guest=True) +def create_payment_gateway_settings(**kwargs): + """Create Payment Gateway Settings for the team.""" + team = get_current_team() + args = frappe._dict(kwargs) + + try: + + payment_gateway_settings = frappe.get_doc({ + "doctype": "Payment Gateway", + "team": team, + "gateway": args.get("gateway_name"), + "currency": args.get("currency"), + "gateway_settings": args.get("gateway_setting"), + "gateway_controller": args.get("gateway_controller"), + "url": args.get("url"), + "api_key": args.get("api_key"), + "api_secret": args.get("api_secret"), + "taxes_and_charges": args.get("taxes_and_charges"), + }) + + payment_gateway_settings.insert(ignore_permissions=True) + frappe.db.commit() + + return payment_gateway_settings.name + except Exception as e: + print(str(e)) + frappe.log_error(message=f"Error creating Payment Gateway Settings: {str(e)}", title="Payment Gateway Settings Creation Error") + return None + +@frappe.whitelist(allow_guest=True) +def get_currency_options(): + """Get the list of currencies supported by the system.""" + currencies=frappe.get_all("Currency", fields=["name"]) + names=[currency['name'] for currency in currencies] + return names + +@frappe.whitelist(allow_guest=True) +def get_gateway_settings(): + """Get the list of doctypes supported by the system.""" + doctypes=frappe.get_all("DocType", fields=["name"]) + names=[doc['name'] for doc in doctypes] + return names + +@frappe.whitelist(allow_guest=True) +def get_gateway_controllers(gateway_setting): + """Get the list of controllers for the given doctype.""" + controllers = frappe.get_all(gateway_setting, fields=["name"]) + + # Extract just the name from the list of dictionaries + names = [doc['name'] for doc in controllers] + + return names # Return only the list of names From 3b4541eece62a756cffa716663d28f6e9642578f Mon Sep 17 00:00:00 2001 From: maniamartial Date: Mon, 21 Oct 2024 17:21:03 +0300 Subject: [PATCH 37/78] feat - code refactor(removing commented code) --- .../src2/components/AddMpesaCredentials.vue | 4 +- .../src2/components/AddPaymentGateway.vue | 19 +- .../src2/components/BuyPrepaidCreditMpesa.vue | 399 ++++++++---------- .../src2/components/BuyPrepaidCreditsForm.vue | 28 +- dashboard/src2/pages/BillingMpesaInvoices.vue | 20 +- press/api/billing.py | 212 +++++----- 6 files changed, 278 insertions(+), 404 deletions(-) diff --git a/dashboard/src2/components/AddMpesaCredentials.vue b/dashboard/src2/components/AddMpesaCredentials.vue index 5f3069938e..fe07c415c5 100644 --- a/dashboard/src2/components/AddMpesaCredentials.vue +++ b/dashboard/src2/components/AddMpesaCredentials.vue @@ -146,13 +146,14 @@ import { toast } from 'vue-sonner'; security_credential: this.securityCredential, till_number: this.tillNumber, sandbox: this.sandBox - }, + validate(){ if(!this.paymentGatewayName || !this.consumerKey || !this.consumerSecret || !this.passKey || !this.shortCode || !this.initiatorName || !this.securityCredential){ return 'All fields are required'; } }, + async onSuccess(data){ if(data){ toast.success('M-Pesa credentials savedd', data); @@ -165,7 +166,6 @@ import { toast } from 'vue-sonner'; } }, - methods: { async saveMpesaCredentials() { try { diff --git a/dashboard/src2/components/AddPaymentGateway.vue b/dashboard/src2/components/AddPaymentGateway.vue index e2b155fff2..37b7c33eb4 100644 --- a/dashboard/src2/components/AddPaymentGateway.vue +++ b/dashboard/src2/components/AddPaymentGateway.vue @@ -1,5 +1,3 @@ - -