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/AddExchangeRate.vue b/dashboard/src2/components/AddExchangeRate.vue
new file mode 100644
index 0000000000..9d5ba2fa66
--- /dev/null
+++ b/dashboard/src2/components/AddExchangeRate.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
diff --git a/dashboard/src2/components/AddMpesaCredentials.vue b/dashboard/src2/components/AddMpesaCredentials.vue
new file mode 100644
index 0000000000..ab078b6eab
--- /dev/null
+++ b/dashboard/src2/components/AddMpesaCredentials.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/dashboard/src2/components/AddPaymentGateway.vue b/dashboard/src2/components/AddPaymentGateway.vue
new file mode 100644
index 0000000000..6da54c40d1
--- /dev/null
+++ b/dashboard/src2/components/AddPaymentGateway.vue
@@ -0,0 +1,257 @@
+
+
+
+
+
diff --git a/dashboard/src2/components/AddPaymobCredentials.vue b/dashboard/src2/components/AddPaymobCredentials.vue
new file mode 100644
index 0000000000..cd7bad131f
--- /dev/null
+++ b/dashboard/src2/components/AddPaymobCredentials.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dashboard/src2/components/BuyPrepaidCreditMpesa.vue b/dashboard/src2/components/BuyPrepaidCreditMpesa.vue
new file mode 100644
index 0000000000..9b4107fb05
--- /dev/null
+++ b/dashboard/src2/components/BuyPrepaidCreditMpesa.vue
@@ -0,0 +1,270 @@
+
+ Tax(%): {{ taxPercentage }}% Total Amount With Tax: Ksh. {{ amountWithTax }}
Invoice Name | +Date | +Amount | +Download Invoice | +
---|---|---|---|
{{ invoice.name }} | +{{ invoice.posting_date }} | +{{ formatCurrency(invoice.trans_amount) }} | ++ View Invoice + | +
{{ __("Account Type") }} | +{{ __("Current Balance") }} | +{{ __("Available Balance") }} | +{{ __("Reserved Balance") }} | +{{ __("Uncleared Balance") }} | +
---|---|---|---|---|
{%= key %} | +{%= value["current_balance"] %} | +{%= value["available_balance"] %} | +{%= value["reserved_balance"] %} | +{%= value["uncleared_balance"] %} | +
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..251e4029c3 --- /dev/null +++ b/press/press/doctype/mpesa_settings/mpesa_settings.json @@ -0,0 +1,156 @@ +{ + "actions": [], + "autoname": "format:{payment_gateway_name}-{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", + "sandbox", + "column_break_4", + "business_shortcode", + "online_passkey", + "transaction_limit", + "security_credential" + ], + "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": "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-11-08 17:28:31.432927", + "modified_by": "Administrator", + "module": "Press", + "name": "Mpesa Settings", + "naming_rule": "Expression", + "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..88703f3095 --- /dev/null +++ b/press/press/doctype/mpesa_settings/mpesa_settings.py @@ -0,0 +1,35 @@ +# 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): + # 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_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/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/partner_payment_payout/__init__.py b/press/press/doctype/partner_payment_payout/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/partner_payment_payout/partner_payment_payout.js b/press/press/doctype/partner_payment_payout/partner_payment_payout.js new file mode 100644 index 0000000000..86b5a2867f --- /dev/null +++ b/press/press/doctype/partner_payment_payout/partner_payment_payout.js @@ -0,0 +1,38 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Partner Payment Payout", { + refresh(frm) { + if(frm.doc.docstatus == 0) { + frm.add_custom_button("Fetch Payments", () => { + frappe.call({ + method: "press.api.local_payments.mpesa.utils.fetch_payments", + args: { + // transaction_doctype: frm.doc.transaction_doctype, + from_date: frm.doc.from_date, + to_date: frm.doc.to_date, + partner: frm.doc.partner, + payment_gateway: frm.doc.payment_gateway, + }, + callback: function(response) { + if (response.message) { + // Clear existing entries in transfer_items + frm.clear_table("transfer_items"); + + response.message.forEach(payment => { + let row = frm.add_child("transfer_items"); + row.transaction_id = payment.name; + row.posting_date=payment.posting_date + row.amount = payment.amount; + + }); + + frm.refresh_field("transfer_items"); + // frappe.msgprint("Payments fetched and added to the transfer items table."); + } + } + }); + }); + }}, +}); + diff --git a/press/press/doctype/partner_payment_payout/partner_payment_payout.json b/press/press/doctype/partner_payment_payout/partner_payment_payout.json new file mode 100644 index 0000000000..3fdb489322 --- /dev/null +++ b/press/press/doctype/partner_payment_payout/partner_payment_payout.json @@ -0,0 +1,148 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:PPT-{MM}-{#####}", + "creation": "2024-11-08 13:07:10.321274", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_l0lu", + "amended_from", + "from_date", + "partner", + "payment_gateway", + "column_break_plbi", + "to_date", + "partner_commission", + "section_break_tvae", + "transfer_items", + "section_break_qxag", + "total_amount", + "column_break_lgdh", + "commission", + "column_break_jfqp", + "net_amount" + ], + "fields": [ + { + "fieldname": "section_break_l0lu", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Partner Payment Payout", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "default": "Today", + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, + { + "fieldname": "column_break_plbi", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, + { + "fieldname": "section_break_tvae", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_qxag", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_amount", + "fieldtype": "Currency", + "label": "Total Amount", + "non_negative": 1 + }, + { + "fieldname": "transfer_items", + "fieldtype": "Table", + "label": "Partner Payment Transfer Item", + "options": "Partner Payment Payout Item", + "reqd": 1 + }, + { + "fieldname": "column_break_lgdh", + "fieldtype": "Column Break" + }, + { + "fieldname": "commission", + "fieldtype": "Currency", + "label": "Commission" + }, + { + "fieldname": "column_break_jfqp", + "fieldtype": "Column Break" + }, + { + "fieldname": "net_amount", + "fieldtype": "Currency", + "label": "Net Amount" + }, + { + "fieldname": "partner", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Partner", + "options": "Team", + "reqd": 1 + }, + { + "fetch_from": "partner.partner_commission", + "fieldname": "partner_commission", + "fieldtype": "Percent", + "label": "Partner Commission", + "read_only": 1 + }, + { + "fieldname": "payment_gateway", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Gateway", + "options": "Payment Gateway", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2024-11-22 11:22:22.557825", + "modified_by": "Administrator", + "module": "Press", + "name": "Partner Payment Payout", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/press/press/doctype/partner_payment_payout/partner_payment_payout.py b/press/press/doctype/partner_payment_payout/partner_payment_payout.py new file mode 100644 index 0000000000..0543a2621c --- /dev/null +++ b/press/press/doctype/partner_payment_payout/partner_payment_payout.py @@ -0,0 +1,66 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class PartnerPaymentPayout(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 + from press.press.doctype.partner_payment_payout_item.partner_payment_payout_item import PartnerPaymentPayoutItem + + amended_from: DF.Link | None + commission: DF.Currency + from_date: DF.Date | None + net_amount: DF.Currency + partner: DF.Link + partner_commission: DF.Percent + payment_gateway: DF.Link + to_date: DF.Date | None + total_amount: DF.Currency + transfer_items: DF.Table[PartnerPaymentPayoutItem] + # end: auto-generated types + pass + + def before_save(self): + self.total_amount = sum([item.amount for item in self.transfer_items]) + self.commission = self.total_amount * (self.partner_commission / 100) + self.net_amount = self.total_amount - self.commission + for item in self.transfer_items: + item.commission_amount = item.amount * (self.partner_commission / 100) + item.net_amount = item.amount - item.commission_amount + + def on_submit(self): + transaction_names = [item.transaction_id for item in self.transfer_items] + + if transaction_names: + frappe.db.set_value( + "Payment Partner Transaction", + {"name": ["in", transaction_names], "submitted_to_frappe": 0}, + "submitted_to_frappe", + 1 + ) + frappe.db.commit() + + def on_cancel(self): + transaction_names = [item.transaction_id for item in self.transfer_items] + + # Update Payment Partner Records + if transaction_names: + frappe.db.set_value( + "Payment Partner Transaction", + {"name": ["in", transaction_names], "submitted_to_frappe": 1}, + "submitted_to_frappe", + 0 + ) + frappe.db.commit() + + + + diff --git a/press/press/doctype/partner_payment_payout/test_partner_payment_payout.py b/press/press/doctype/partner_payment_payout/test_partner_payment_payout.py new file mode 100644 index 0000000000..2cf5103421 --- /dev/null +++ b/press/press/doctype/partner_payment_payout/test_partner_payment_payout.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPartnerPaymentPayout(FrappeTestCase): + pass diff --git a/press/press/doctype/partner_payment_payout_item/__init__.py b/press/press/doctype/partner_payment_payout_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/partner_payment_payout_item/partner_payment_payout_item.json b/press/press/doctype/partner_payment_payout_item/partner_payment_payout_item.json new file mode 100644 index 0000000000..f1fe4eb53e --- /dev/null +++ b/press/press/doctype/partner_payment_payout_item/partner_payment_payout_item.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-11-22 11:16:54.513751", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "transaction_id", + "posting_date", + "amount", + "column_break_ayfd", + "commission_amount", + "amount_in_local_currency", + "net_amount" + ], + "fields": [ + { + "fieldname": "transaction_id", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Transaction Id", + "options": "Payment Partner Transaction", + "reqd": 1 + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Posting Date" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Amount(USD)" + }, + { + "fieldname": "column_break_ayfd", + "fieldtype": "Column Break" + }, + { + "fieldname": "commission_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Commission Amount" + }, + { + "fieldname": "amount_in_local_currency", + "fieldtype": "Currency", + "label": "Amount(LC)" + }, + { + "fieldname": "net_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Net Amount" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-11-22 11:17:55.345884", + "modified_by": "Administrator", + "module": "Press", + "name": "Partner Payment Payout Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/press/press/doctype/partner_payment_payout_item/partner_payment_payout_item.py b/press/press/doctype/partner_payment_payout_item/partner_payment_payout_item.py new file mode 100644 index 0000000000..02eb0766af --- /dev/null +++ b/press/press/doctype/partner_payment_payout_item/partner_payment_payout_item.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PartnerPaymentPayoutItem(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 + + amount: DF.Currency + amount_in_local_currency: DF.Currency + commission_amount: DF.Currency + net_amount: DF.Currency + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + posting_date: DF.Date | None + transaction_id: DF.Link + # end: auto-generated types + 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..f86d67adef --- /dev/null +++ b/press/press/doctype/payment_gateway/payment_gateway.json @@ -0,0 +1,159 @@ +{ + "actions": [], + "autoname": "field:gateway", + "creation": "2024-09-13 13:25:09.836216", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "team", + "column_break_kvnc", + "team_name", + "column_break_pwgv", + "currency", + "gateway", + "ui_configuration_section", + "integration_logo", + "column_break_jcag", + "gateway_settings", + "column_break_noki", + "gateway_controller", + "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", + "in_list_view": 1, + "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" + }, + { + "fieldname": "column_break_jcag", + "fieldtype": "Column Break" + }, + { + "fieldname": "gateway_settings", + "fieldtype": "Link", + "label": "Gateway Settings", + "options": "DocType" + }, + { + "fieldname": "column_break_noki", + "fieldtype": "Column Break" + }, + { + "fieldname": "gateway_controller", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Gateway Controller", + "options": "gateway_settings" + }, + { + "fetch_from": "team.team_title", + "fieldname": "team_name", + "fieldtype": "Data", + "label": "Team Name", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-10-13 12:57:10.317435", + "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..5310211f88 --- /dev/null +++ b/press/press/doctype/payment_gateway/payment_gateway.py @@ -0,0 +1,29 @@ +# 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 + gateway_controller: DF.DynamicLink | None + gateway_settings: DF.Link | None + integration_logo: DF.AttachImage | None + taxes_and_charges: DF.Percent + team: DF.Link | None + team_name: DF.Data | 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_transaction/__init__.py b/press/press/doctype/payment_partner_transaction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/payment_partner_transaction/payment_partner_transaction.js b/press/press/doctype/payment_partner_transaction/payment_partner_transaction.js new file mode 100644 index 0000000000..a7642633a6 --- /dev/null +++ b/press/press/doctype/payment_partner_transaction/payment_partner_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 Transaction", { +// refresh(frm) { + +// }, +// }); diff --git a/press/press/doctype/payment_partner_transaction/payment_partner_transaction.json b/press/press/doctype/payment_partner_transaction/payment_partner_transaction.json new file mode 100644 index 0000000000..14878a635a --- /dev/null +++ b/press/press/doctype/payment_partner_transaction/payment_partner_transaction.json @@ -0,0 +1,176 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:PPT-{MM}-{#####}", + "creation": "2024-09-13 13:15:49.154039", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "partner_details_section", + "payment_partner", + "posting_date", + "column_break_manc", + "payment_gateway", + "column_break_ejza", + "team", + "transaction_details_section", + "amount", + "actual_amount", + "column_break_xqsh", + "currency", + "actual_currency", + "column_break_jyxx", + "exchange_rate", + "submitted_to_frappe", + "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 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", + "options": "currency" + }, + { + "fieldname": "column_break_xqsh", + "fieldtype": "Column Break" + }, + { + "fieldname": "partner_details_section", + "fieldtype": "Section Break", + "label": "Team Details" + }, + { + "fieldname": "payment_partner", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Payment Partner", + "options": "Team" + }, + { + "fieldname": "column_break_ejza", + "fieldtype": "Column Break" + }, + { + "fieldname": "team", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "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", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Payment Gateway", + "options": "Payment Gateway" + }, + { + "fieldname": "section_break_yhqq", + "fieldtype": "Section Break" + }, + { + "fieldname": "payment_transaction_details", + "fieldtype": "Code", + "label": "Payment Transaction Details", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "actual_amount", + "fieldtype": "Currency", + "label": "Actual Amount", + "options": "actual_currency" + }, + { + "fetch_from": "payment_gateway.currency", + "fieldname": "actual_currency", + "fieldtype": "Link", + "label": "Actual Currency", + "options": "Currency" + }, + { + "default": "0", + "fieldname": "submitted_to_frappe", + "fieldtype": "Check", + "label": "Submitted To Frappe" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2024-11-22 09:19:11.492603", + "modified_by": "Administrator", + "module": "Press", + "name": "Payment Partner Transaction", + "naming_rule": "Expression", + "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_transaction/payment_partner_transaction.py b/press/press/doctype/payment_partner_transaction/payment_partner_transaction.py new file mode 100644 index 0000000000..cfa98b3a98 --- /dev/null +++ b/press/press/doctype/payment_partner_transaction/payment_partner_transaction.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class PaymentPartnerTransaction(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 + + actual_amount: DF.Currency + actual_currency: DF.Link | None + 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.Code | None + posting_date: DF.Date | None + submitted_to_frappe: DF.Check + team: DF.Link | None + # end: auto-generated types + def on_submit(self): + team = frappe.get_doc("Team", self.team) + payment_partner_user = frappe.db.get_value("Team", self.payment_partner, "user") + remark = f"Credit allocated via Payment Partner: {payment_partner_user} using {self.payment_gateway} gateway ({self.doctype} ID: {self.name})" + team.allocate_credit_amount(self.amount, "Prepaid Credits", remark=remark) diff --git a/press/press/doctype/payment_partner_transaction/test_payment_partner_transaction.py b/press/press/doctype/payment_partner_transaction/test_payment_partner_transaction.py new file mode 100644 index 0000000000..605ab6e73b --- /dev/null +++ b/press/press/doctype/payment_partner_transaction/test_payment_partner_transaction.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPaymentPartnerTransaction(FrappeTestCase): + pass diff --git a/press/press/doctype/paymob_callback_log/__init__.py b/press/press/doctype/paymob_callback_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/paymob_callback_log/paymob_callback_log.js b/press/press/doctype/paymob_callback_log/paymob_callback_log.js new file mode 100644 index 0000000000..db4a5ac447 --- /dev/null +++ b/press/press/doctype/paymob_callback_log/paymob_callback_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Paymob Callback Log", { +// refresh(frm) { + +// }, +// }); diff --git a/press/press/doctype/paymob_callback_log/paymob_callback_log.json b/press/press/doctype/paymob_callback_log/paymob_callback_log.json new file mode 100644 index 0000000000..0b9e1bc9cc --- /dev/null +++ b/press/press/doctype/paymob_callback_log/paymob_callback_log.json @@ -0,0 +1,104 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-16 20:35:29.472893", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "event_type", + "team", + "payment_partner", + "column_break_lbwj", + "special_reference", + "order_id", + "transaction_id", + "success", + "section_break_yzyu", + "payload" + ], + "fields": [ + { + "fieldname": "team", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Team", + "options": "Team", + "read_only": 1 + }, + { + "fieldname": "special_reference", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Special Reference" + }, + { + "fieldname": "column_break_lbwj", + "fieldtype": "Column Break" + }, + { + "fieldname": "payment_partner", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Partner", + "options": "Team" + }, + { + "fieldname": "section_break_yzyu", + "fieldtype": "Section Break" + }, + { + "fieldname": "payload", + "fieldtype": "Code", + "label": "payload", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "transaction_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Transaction ID" + }, + { + "fieldname": "order_id", + "fieldtype": "Data", + "label": "Order ID" + }, + { + "fieldname": "event_type", + "fieldtype": "Data", + "label": "Event Type" + }, + { + "default": "0", + "fieldname": "success", + "fieldtype": "Check", + "label": "Success", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-11-27 14:55:09.850919", + "modified_by": "Administrator", + "module": "Press", + "name": "Paymob Callback Log", + "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/paymob_callback_log/paymob_callback_log.py b/press/press/doctype/paymob_callback_log/paymob_callback_log.py new file mode 100644 index 0000000000..f4c8f2ba5f --- /dev/null +++ b/press/press/doctype/paymob_callback_log/paymob_callback_log.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import parse_json +from frappe import as_json + +class PaymobCallbackLog(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 + + event_type: DF.Data | None + order_id: DF.Data | None + payload: DF.Code | None + payment_partner: DF.Link | None + special_reference: DF.Data | None + success: DF.Check + team: DF.Link | None + transaction_id: DF.Data | None + # end: auto-generated types + def validate(self): + self.set_missing_data() + + def set_missing_data(self): + + if not self.team or not self.payment_partner: + paymob_log_data = self._get_paymob_log_data() + if paymob_log_data: + self.team, self.payment_partner = paymob_log_data + + def after_insert(self): + if self._is_payment_successful(): + self._create_payment_partner_transaction() + + def _get_paymob_log_data(self): + return frappe.db.get_value("Paymob Log", + filters={"special_reference": self.special_reference}, + fieldname=["team", "payment_partner"] + ) + + def _is_payment_successful(self) -> bool: + + if not self.payload: + return False + + try: + payload = parse_json(self.payload) + obj = payload.get("obj", {}) + data = obj.get("data", {}) + + success = obj.get("success", False) + is_live = obj.get("is_live", False) + + txn_response_code = data.get("txn_response_code", "") + + return success and is_live and txn_response_code == "APPROVED" + except ValueError: + frappe.log_error("PaymobCallbackLog Payload Error", "Invalid JSON format in payload",) + return False + + def _create_payment_partner_transaction(self): + try: + paymob_log_data = frappe.db.get_value( + "Paymob Log", + filters={"special_reference": self.special_reference}, + fieldname=["exchange_rate", "amount", "actual_amount"] + ) + if not paymob_log_data: + frappe.log_error(f"Paymob Log not found for reference {self.special_reference}", "PaymobCallbackLog Error") + return + + exchange_rate, amount, paid_amount = paymob_log_data + payload=as_json(parse_json(self.payload)) + create_payment_partner_transaction(self.team, self.payment_partner, exchange_rate, amount, paid_amount, "Paymob", payload) + + + except Exception as e: + frappe.log_error("Error creating Payment Partner Transaction", f"PaymobCallbackLog Error :\n{str(e)}") + +def create_payment_partner_transaction(team, payment_partner, exchange_rate, amount, paid_amount,payment_gateway, payload=None): + """Create a Payment Partner Transaction record.""" + transaction_doc = frappe.get_doc({ + "doctype": "Payment Partner Transaction", + "team": team, + "payment_partner": payment_partner, + "exchange_rate": exchange_rate, + "payment_gateway": payment_gateway, + "amount": amount, + "actual_amount": paid_amount, + "payment_transaction_details": payload + }) + transaction_doc.insert() + transaction_doc.submit() diff --git a/press/press/doctype/paymob_callback_log/test_paymob_callback_log.py b/press/press/doctype/paymob_callback_log/test_paymob_callback_log.py new file mode 100644 index 0000000000..d2067c2128 --- /dev/null +++ b/press/press/doctype/paymob_callback_log/test_paymob_callback_log.py @@ -0,0 +1,137 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +class TestPaymobCallbackLog(FrappeTestCase): + def setUp(self): + """Set up a mock PaymobCallbackLog document for testing.""" + self.special_reference = "12345" + + # create team user + self.team_user = frappe.get_doc({ + "doctype": "User", + "email": "team_user@frappecloud.com", + "first_name": "Test Team" + }).insert(ignore_permissions=True, ignore_mandatory=True) + + self.team = frappe.get_doc({ + "doctype": "Team", + "team_title": "Test Team", + "user": self.team_user.name + }).insert(ignore_permissions=True, ignore_mandatory=True) + + # Payment Partner User + self.payment_partner_user = frappe.get_doc({ + "doctype": "User", + "email": "payment_partner@frappecloud.com", + "first_name": "Payment Partner Team" + }).insert(ignore_permissions=True, ignore_mandatory=True) + + self.payment_partner = frappe.get_doc({ + "doctype": "Team", + "team_title": "Test Payment Partner", + "user": self.payment_partner_user.name + }).insert(ignore_permissions=True, ignore_mandatory=True) + + # data needed to create Paymob Log + tax_percentage = (14 / 100) + exchange_rate = 48 + amount = 10 + amount_coverted_egp = (amount * exchange_rate) + actual_amount = (amount_coverted_egp) + (amount_coverted_egp * tax_percentage) + + self.paymob_log = frappe.get_doc({ + "doctype": "Paymob Log", + "special_reference": self.special_reference, + "team": self.team.name, + "payment_partner": self.payment_partner.name, + "exchange_rate": exchange_rate, + "amount": amount, + "actual_amount": actual_amount + }).insert(ignore_permissions=True) + + self.doc = frappe.get_doc({ + "doctype": "Paymob Callback Log", + "event_type": "Transaction", + "special_reference": self.special_reference, + "payload": '{"obj": {"success": true, "is_live": true, "data": {"txn_response_code": "APPROVED"}}}', + "team": None, + "payment_partner": None, + "submit_to_payment_partner": False + }).insert(ignore_permissions=True) + + def tearDown(self): + """Clean up test records.""" + frappe.delete_doc("Team", self.team.name, ignore_permissions=True, force=True) + frappe.delete_doc("Team", self.payment_partner.name, ignore_permissions=True, force=True) + frappe.delete_doc("Paymob Log", self.paymob_log.name, ignore_permissions=True, force=True) + frappe.delete_doc("Paymob Callback Log", self.doc.name, ignore_permissions=True, force=True) + frappe.delete_doc("User", self.team_user.name, ignore_permissions=True, force=True) + frappe.delete_doc("User", self.payment_partner_user.name, ignore_permissions=True, force=True) + + def test_set_missing_data(self): + """Test set_missing_data fetches and sets team and payment partner.""" + self.doc.set_missing_data() + self.assertEqual(self.doc.team, self.team.name) + self.assertEqual(self.doc.payment_partner, self.payment_partner.name) + self.assertEqual(self.doc.special_reference, self.special_reference) + + + def test_is_payment_successful(self): + """Test _is_payment_successful correctly parses payload and checks conditions.""" + # Valid success payload + self.assertTrue(self.doc._is_payment_successful()) + + # Payload with success = False + self.doc.payload = '{"obj": {"success": false}}' + self.assertFalse(self.doc._is_payment_successful()) + + # Invalid payload format + self.doc.payload = "invalid_json" + self.assertFalse(self.doc._is_payment_successful()) + + # Missing payload + self.doc.payload = None + self.assertFalse(self.doc._is_payment_successful()) + + def test_create_payment_partner_transaction(self): + """Test _create_payment_partner_transaction creates transactions successfully.""" + self.doc.set_missing_data() + self.doc._create_payment_partner_transaction() + + # Verify the transaction + transaction = frappe.get_all("Payment Partner Transaction", filters={ + "team": self.doc.team, + "payment_partner": self.doc.payment_partner, + "amount": 10 + }) + + self.assertTrue(len(transaction) > 0) + transaction_doc = frappe.get_doc("Payment Partner Transaction", transaction[0].name) + + self.assertEqual(transaction_doc.amount, 10) + self.assertEqual(transaction_doc.exchange_rate, 48) + expected_actual_amount = 547.2 + self.assertEqual(transaction_doc.actual_amount, expected_actual_amount) + self.assertEqual(transaction_doc.payment_partner, self.doc.payment_partner) + + def test_validate(self): + """Test validate triggers set_missing_data.""" + self.doc.team = None # Reset team to trigger `set_missing_data` + self.doc.validate() + self.assertIsNotNone(self.doc.team) + + def test_after_insert(self): + """Test after_insert calls appropriate methods based on success.""" + # self.doc.team = None # Reset team to trigger methods + self.doc.after_insert() + + # Verify the transaction + transaction = frappe.get_all("Payment Partner Transaction", filters={ + "team": self.doc.team, + "payment_partner": self.doc.payment_partner + }) + + self.assertTrue(len(transaction) > 0) \ No newline at end of file diff --git a/press/press/doctype/paymob_log/__init__.py b/press/press/doctype/paymob_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/paymob_log/paymob_log.js b/press/press/doctype/paymob_log/paymob_log.js new file mode 100644 index 0000000000..634b9dfe32 --- /dev/null +++ b/press/press/doctype/paymob_log/paymob_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Paymob Log", { +// refresh(frm) { + +// }, +// }); diff --git a/press/press/doctype/paymob_log/paymob_log.json b/press/press/doctype/paymob_log/paymob_log.json new file mode 100644 index 0000000000..dba66acf89 --- /dev/null +++ b/press/press/doctype/paymob_log/paymob_log.json @@ -0,0 +1,129 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2024-10-10 13:18:23.905341", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "event_type", + "payment_partner", + "amount", + "exchange_rate", + "amount_currency", + "column_break_qvrr", + "team", + "special_reference", + "actual_amount", + "actual_currency", + "section_break_zuri", + "payload" + ], + "fields": [ + { + "fieldname": "event_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Event Type" + }, + { + "fieldname": "payment_partner", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Partner", + "options": "Team", + "read_only": 1 + }, + { + "fieldname": "column_break_qvrr", + "fieldtype": "Column Break" + }, + { + "fieldname": "team", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Team", + "options": "Team", + "read_only": 1 + }, + { + "fieldname": "section_break_zuri", + "fieldtype": "Section Break" + }, + { + "fieldname": "payload", + "fieldtype": "Code", + "label": "Payload", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "special_reference", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Special Reference", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "amount_currency", + "read_only": 1 + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "read_only": 1 + }, + { + "fieldname": "actual_amount", + "fieldtype": "Currency", + "label": "Actual Amount", + "options": "actual_currency", + "read_only": 1 + }, + { + "default": "USD", + "fieldname": "amount_currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Amount Currency", + "options": "Currency" + }, + { + "default": "EGP", + "fieldname": "actual_currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Actual Currency", + "options": "Currency" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-10-17 18:17:45.767607", + "modified_by": "Administrator", + "module": "Press", + "name": "Paymob Log", + "naming_rule": "Random", + "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/paymob_log/paymob_log.py b/press/press/doctype/paymob_log/paymob_log.py new file mode 100644 index 0000000000..53b39d4603 --- /dev/null +++ b/press/press/doctype/paymob_log/paymob_log.py @@ -0,0 +1,28 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PaymobLog(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 + + actual_amount: DF.Currency + actual_currency: DF.Link | None + amount: DF.Currency + amount_currency: DF.Link | None + event_type: DF.Data | None + exchange_rate: DF.Float + payload: DF.Code | None + payment_partner: DF.Link | None + special_reference: DF.Data | None + team: DF.Link | None + # end: auto-generated types + pass diff --git a/press/press/doctype/paymob_log/test_paymob_log.py b/press/press/doctype/paymob_log/test_paymob_log.py new file mode 100644 index 0000000000..e571c13acd --- /dev/null +++ b/press/press/doctype/paymob_log/test_paymob_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPaymobLog(FrappeTestCase): + pass diff --git a/press/press/doctype/paymob_settings/__init__.py b/press/press/doctype/paymob_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/doctype/paymob_settings/paymob_settings.js b/press/press/doctype/paymob_settings/paymob_settings.js new file mode 100644 index 0000000000..66dcc2eb1f --- /dev/null +++ b/press/press/doctype/paymob_settings/paymob_settings.js @@ -0,0 +1,27 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Paymob Settings", { + refresh(frm) { + frm.add_custom_button(__("Get Access Token"), () => { + frm.trigger("get_access_token") + }).addClass("btn btn-primary") + }, + get_access_token: function (frm) { + try { + frm.call({ + method: "get_access_token", + doc: frm.doc, + freeze: true, + freeze_message: __("Getting Access Token ...") + }).then((r) => { + if (!r.exc && r.message) { + frm.set_value("token", r.message) + frappe.show_alert({ message: __("Access Token Updated"), indicator: "green"}); + } + }); + } catch(e) { + console.log(e); + } + } +}); diff --git a/press/press/doctype/paymob_settings/paymob_settings.json b/press/press/doctype/paymob_settings/paymob_settings.json new file mode 100644 index 0000000000..5aec3ac5b6 --- /dev/null +++ b/press/press/doctype/paymob_settings/paymob_settings.json @@ -0,0 +1,128 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-10 11:16:57.274423", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "credentials_section", + "api_key", + "public_key", + "hmac", + "column_break_abbe", + "token", + "secret_key", + "section_break_ddej", + "iframe", + "column_break_euul", + "payment_integration" + ], + "fields": [ + { + "fieldname": "credentials_section", + "fieldtype": "Section Break", + "label": "Credentials" + }, + { + "fieldname": "api_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "API Key", + "reqd": 1 + }, + { + "fieldname": "public_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Public Key", + "reqd": 1 + }, + { + "fieldname": "hmac", + "fieldtype": "Password", + "in_list_view": 1, + "label": "HMAC", + "reqd": 1 + }, + { + "fieldname": "column_break_abbe", + "fieldtype": "Column Break" + }, + { + "fieldname": "token", + "fieldtype": "Password", + "label": "Token" + }, + { + "fieldname": "secret_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Secret Key", + "reqd": 1 + }, + { + "fieldname": "section_break_ddej", + "fieldtype": "Section Break", + "label": "Payment Config" + }, + { + "fieldname": "iframe", + "fieldtype": "Data", + "label": "Iframe", + "reqd": 1 + }, + { + "fieldname": "column_break_euul", + "fieldtype": "Column Break" + }, + { + "fieldname": "payment_integration", + "fieldtype": "Int", + "label": "Payment Integration", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2024-10-10 12:48:56.812698", + "modified_by": "Administrator", + "module": "Press", + "name": "Paymob Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/press/press/doctype/paymob_settings/paymob_settings.py b/press/press/doctype/paymob_settings/paymob_settings.py new file mode 100644 index 0000000000..24bca277fd --- /dev/null +++ b/press/press/doctype/paymob_settings/paymob_settings.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from press.api.local_payments.paymob.accept_api import AcceptAPI + + +class PaymobSettings(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.Password + hmac: DF.Password + iframe: DF.Data + payment_integration: DF.Int + public_key: DF.Password + secret_key: DF.Password + token: DF.Password | None + # end: auto-generated types + + @frappe.whitelist() + def get_access_token(self): + accept = AcceptAPI() + token = accept.retrieve_auth_token() + return token + +@frappe.whitelist() +def update_paymob_settings(**kwargs): + args = frappe._dict(kwargs) + fields = frappe._dict( + { + "api_key": args.get("api_key"), + "secret_key": args.get("secret_key"), + "public_key": args.get("public_key"), + "hmac": args.get("hmac"), + "iframe": args.get("iframe"), + "payment_integration": args.get("payment_integration"), + } + ) + try: + paymob_settings = frappe.get_doc("Paymob Settings").update(fields) + paymob_settings.save() + return "Paymob Credentials Successfully" + except Exception as e: + return "Failed to Update Paymob Credentials" diff --git a/press/press/doctype/paymob_settings/test_paymob_settings.py b/press/press/doctype/paymob_settings/test_paymob_settings.py new file mode 100644 index 0000000000..854dc83a56 --- /dev/null +++ b/press/press/doctype/paymob_settings/test_paymob_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPaymobSettings(FrappeTestCase): + pass diff --git a/press/press/doctype/press_settings/press_settings.py b/press/press/doctype/press_settings/press_settings.py index 400b28755a..c498085f09 100644 --- a/press/press/doctype/press_settings/press_settings.py +++ b/press/press/doctype/press_settings/press_settings.py @@ -145,9 +145,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/press/doctype/team/team.json b/press/press/doctype/team/team.json index 3ae7e17b26..f51b38d40e 100644 --- a/press/press/doctype/team/team.json +++ b/press/press/doctype/team/team.json @@ -42,6 +42,9 @@ "free_credits_allocated", "column_break_12", "address_html", + "tax_id", + "phone_number", + "partner_commission", "custom_apps_section", "github_access_token", "partner_section", @@ -441,6 +444,21 @@ "fieldname": "enable_inplace_updates", "fieldtype": "Check", "label": "Enable In Place Updates" + }, + { + "fieldname": "tax_id", + "fieldtype": "Data", + "label": "Tax Id" + }, + { + "fieldname": "phone_number", + "fieldtype": "Data", + "label": "Phone Number" + }, + { + "fieldname": "partner_commission", + "fieldtype": "Percent", + "label": "Partner Commission" } ], "links": [ @@ -500,7 +518,7 @@ "link_fieldname": "team" } ], - "modified": "2024-09-13 11:55:07.048543", + "modified": "2024-11-15 11:46:09.082313", "modified_by": "Administrator", "module": "Press", "name": "Team", diff --git a/press/press/doctype/team/team.py b/press/press/doctype/team/team.py index b9a7f453ac..c1142f11ae 100644 --- a/press/press/doctype/team/team.py +++ b/press/press/doctype/team/team.py @@ -34,9 +34,7 @@ class Team(Document): if TYPE_CHECKING: from frappe.types import DF from press.press.doctype.child_team_member.child_team_member import ChildTeamMember - from press.press.doctype.communication_email.communication_email import ( - CommunicationEmail, - ) + from press.press.doctype.communication_email.communication_email import CommunicationEmail from press.press.doctype.invoice_discount.invoice_discount import InvoiceDiscount from press.press.doctype.team_member.team_member import TeamMember @@ -70,10 +68,12 @@ class Team(Document): last_used_team: DF.Link | None notify_email: DF.Data | None parent_team: DF.Link | None + partner_commission: DF.Percent partner_email: DF.Data | None partner_referral_code: DF.Data | None partnership_date: DF.Date | None payment_mode: DF.Literal["", "Card", "Prepaid Credits", "Paid By Partner"] + phone_number: DF.Data | None razorpay_enabled: DF.Check referrer_id: DF.Data | None security_portal_enabled: DF.Check @@ -83,6 +83,7 @@ class Team(Document): skip_backups: DF.Check ssh_access_enabled: DF.Check stripe_customer_id: DF.Data | None + tax_id: DF.Data | None team_members: DF.Table[TeamMember] team_title: DF.Data | None user: DF.Link | None diff --git a/press/press/report/mpesa_team_payment/__init__.py b/press/press/report/mpesa_team_payment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/report/mpesa_team_payment/mpesa_team_payment.js b/press/press/report/mpesa_team_payment/mpesa_team_payment.js new file mode 100644 index 0000000000..aa0188f3a6 --- /dev/null +++ b/press/press/report/mpesa_team_payment/mpesa_team_payment.js @@ -0,0 +1,45 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +frappe.query_reports["Mpesa Team Payment"] = { + "filters": [ + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_days(frappe.datetime.get_today(), -30), + } + , + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + } + , + { + "fieldname":"team", + "label": __("Team"), + "fieldtype": "Link", + "options": "Team", + } + , + + { + "fieldname":"payment_partner", + "label": __("Payment Partner"), + "fieldtype": "Link", + "options": "Team", + }, + { + "fieldname":"transaction_type", + "label": __("Transaction Type"), + "fieldtype": "Select", + "options": "\nMpesa Express\nMpesa C2B", + "default": "Mpesa Express", + }, + + + + ] +}; diff --git a/press/press/report/mpesa_team_payment/mpesa_team_payment.json b/press/press/report/mpesa_team_payment/mpesa_team_payment.json new file mode 100644 index 0000000000..a385ed6472 --- /dev/null +++ b/press/press/report/mpesa_team_payment/mpesa_team_payment.json @@ -0,0 +1,35 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2024-11-04 10:40:23.092691", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-11-04 10:40:23.092691", + "modified_by": "Administrator", + "module": "Press", + "name": "Mpesa Team Payment", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Mpesa Payment Record", + "report_name": "Mpesa Team Payment", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Sales User" + } + ] +} \ No newline at end of file diff --git a/press/press/report/mpesa_team_payment/mpesa_team_payment.py b/press/press/report/mpesa_team_payment/mpesa_team_payment.py new file mode 100644 index 0000000000..6fe5fddf79 --- /dev/null +++ b/press/press/report/mpesa_team_payment/mpesa_team_payment.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_columns(): + return [ + {"label": _("name"), "fieldname": "name", "fieldtype": "Link","options":"Mpesa Payment Record","width": 100}, + {"label": _("Team"), "fieldname": "team", "fieldtype": "Link", "options": "Team", "width": 150}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, + {"label": _("Transaction Type"), "fieldname": "transaction_type", "fieldtype": "Select", "width": 150}, + {"label": _("Grand Total"), "fieldname": "grand_total", "fieldtype": "Currency","options":"default_currency", "width": 120}, + {"label": _("FC Amount"), "fieldname": "trans_amount", "fieldtype": "Currency","options":"default_currency", "width": 120}, + {"label": _("FC Amount"), "fieldname": "amount_usd", "fieldtype": "Currency","options":"currency", "width": 120}, + {"label": _("Exchange Rate"), "fieldname": "exchange_rate", "fieldtype": "Float", "width": 100}, + {"label": _("MSISDN"), "fieldname": "msisdn", "fieldtype": "Data", "width": 120}, + {"label": _("Payment Partner"), "fieldname": "payment_partner", "fieldtype": "Link", "options": "Team", "width": 150}, + {"label": _("Default Currency"), "fieldname": "default_currency", "fieldtype": "Link","options":"Currency", "width": 100, "hidden":1}, + {"label": _("Balance Transaction"), "fieldname": "balance_transaction", "fieldtype": "Link","options":"Balance Transaction", "width": 150}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Link","options":"Currency", "width": 100, "hidden":1}, + ] + + +def get_data(filters): + if filters.from_date > filters.to_date: + frappe.throw(_("From Date cannot be after To Date")) + + mpesa_record=frappe.qb.DocType("Mpesa Payment Record") + + query=frappe.qb.from_(mpesa_record)\ + .select("name","team","transaction_type","merchant_request_id","trans_id","grand_total","exchange_rate","trans_amount","amount_usd","msisdn","payment_partner","invoice_number","posting_date","default_currency","balance_transaction", + ).where(mpesa_record.docstatus == 1) + + query = apply_filters(query, filters, mpesa_record) + data = query.run(as_dict=True) + # Append currency to each record + for record in data: + record["currency"] = "USD" + + return data + + +def apply_filters(query, filters, mpesa_record): + doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} + + for filter_key, filter_value in filters.items(): + if filter_key == "from_date": + query = query.where(mpesa_record.posting_date >= filter_value) + elif filter_key == "to_date": + query = query.where(mpesa_record.posting_date <= filter_value) + elif filter_key == "team": + query = query.where(mpesa_record.team == filter_value) + elif filter_key == "payment_partner": + query = query.where(mpesa_record.payment_partner == filter_value) + elif filter_key == "transaction_type": + query = query.where(mpesa_record.transaction_type == filter_value) + elif filter_key == "docstatus": + query = query.where(mpesa_record.docstatus == doc_status.get(filter_value, 0)) + + return query + diff --git a/press/press/report/payment_partner/__init__.py b/press/press/report/payment_partner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/press/press/report/payment_partner/payment_partner.js b/press/press/report/payment_partner/payment_partner.js new file mode 100644 index 0000000000..3199f25fdb --- /dev/null +++ b/press/press/report/payment_partner/payment_partner.js @@ -0,0 +1,47 @@ +// Copyright (c) 2024, Frappe and contributors +// For license information, please see license.txt + +frappe.query_reports["Payment Partner"] = { +"filters": [ + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_days(frappe.datetime.get_today(), -30), + } + , + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + } + , + { + "fieldname":"team", + "label": __("Team"), + "fieldtype": "Link", + "options": "Team", + } + , + + { + "fieldname":"payment_partner", + "label": __("Payment Partner"), + "fieldtype": "Link", + "options": "Team", + }, + { + "fieldname":"payment_gateway", + "label": __("Payment Gateway"), + "fieldtype": "Link", + "options": "Payment Gateway", + }, + { + "fieldname":"submitted_to_frappe", + "label": __("Submitted to Frappe"), + "fieldtype": "Check", + }, + + ] +}; diff --git a/press/press/report/payment_partner/payment_partner.json b/press/press/report/payment_partner/payment_partner.json new file mode 100644 index 0000000000..051dd7f378 --- /dev/null +++ b/press/press/report/payment_partner/payment_partner.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2024-11-25 15:50:14.609170", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-11-25 15:50:14.609170", + "modified_by": "Administrator", + "module": "Press", + "name": "Payment Partner", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Payment Partner Transaction", + "report_name": "Payment Partner", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Guest" + } + ] +} \ No newline at end of file diff --git a/press/press/report/payment_partner/payment_partner.py b/press/press/report/payment_partner/payment_partner.py new file mode 100644 index 0000000000..b5b15af1c7 --- /dev/null +++ b/press/press/report/payment_partner/payment_partner.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024, Frappe and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_columns(): + return [ + {"label": _("Transaction ID"), "fieldname": "name", "fieldtype": "Link","options":"Payment Partner Transaction","width": 100}, + {"label": _("Team"), "fieldname": "team", "fieldtype": "Link", "options": "Team", "width": 150}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, + {"label": _("Payment Gateway"), "fieldname": "payment_gateway", "fieldtype": "Link","options":"Payment Gateway","width": 150}, + {"label": _("FC Amount"), "fieldname": "amount", "fieldtype": "Currency","options":"currency", "width": 120}, + {"label": _("Actual Amount"), "fieldname": "actual_amount", "fieldtype": "Currency","options":"actual_currency", "width": 120}, + {"label": _("Exchange Rate"), "fieldname": "exchange_rate", "fieldtype": "Float", "width": 100}, + {"label": _("Payment Partner"), "fieldname": "payment_partner", "fieldtype": "Link", "options": "Team", "width": 150}, + {"label": _("Submitted To Frappe"), "fieldname": "submitted_to_frappe", "fieldtype": "Check", "width": 150}, + {"label": _("Actual Currency"), "fieldname": "actual_currency", "fieldtype": "Link","options":"Currency", "width": 100, "hidden":1}, + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Link","options":"Currency", "width": 100, "hidden":1}, + ] + + +def get_data(filters): + if filters.from_date > filters.to_date: + frappe.throw(_("From Date cannot be after To Date")) + + payment_record=frappe.qb.DocType("Payment Partner Transaction") + + query=frappe.qb.from_(payment_record)\ + .select("name","team","payment_gateway","payment_partner","amount","actual_amount","submitted_to_frappe","posting_date","actual_currency","exchange_rate", + ).where(payment_record.docstatus == 1) + + query = apply_filters(query, filters, payment_record) + data = query.run(as_dict=True) + # Append currency to each record + for record in data: + record["currency"] = "USD" + + return data + + +def apply_filters(query, filters, payment_record): + doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} + + for filter_key, filter_value in filters.items(): + if filter_key == "from_date": + query = query.where(payment_record.posting_date >= filter_value) + elif filter_key == "to_date": + query = query.where(payment_record.posting_date <= filter_value) + elif filter_key == "team": + query = query.where(payment_record.team == filter_value) + elif filter_key == "payment_partner": + query = query.where(payment_record.payment_partner == filter_value) + elif filter_key == "payment_gateway": + query = query.where(payment_record.payment_gateway == filter_value) + elif filter_key == "submitted_to_frappe": + query = query.where(payment_record.submitted_to_frappe == filter_value) + elif filter_key == "docstatus": + query = query.where(payment_record.docstatus == doc_status.get(filter_value, 0)) + + return query + diff --git a/press/public/images/mpesa.svg b/press/public/images/mpesa.svg new file mode 100644 index 0000000000..d24056df00 --- /dev/null +++ b/press/public/images/mpesa.svg @@ -0,0 +1,27 @@ + + + diff --git a/press/public/images/paymobLogo.png b/press/public/images/paymobLogo.png new file mode 100644 index 0000000000..36c1d5ce90 Binary files /dev/null and b/press/public/images/paymobLogo.png differ diff --git a/press/public/marketplace/js/call.js b/press/public/marketplace/js/call.js index 82e906b43b..41385f18c4 100644 --- a/press/public/marketplace/js/call.js +++ b/press/public/marketplace/js/call.js @@ -70,3 +70,4 @@ export default async function call(method, args) { throw e; } } + diff --git a/press/utils/billing.py b/press/utils/billing.py index e0d451dfc8..8c3c941493 100644 --- a/press/utils/billing.py +++ b/press/utils/billing.py @@ -8,6 +8,7 @@ from press.exceptions import CentralServerNotSet, FrappeioServerNotSet from press.utils import get_current_team, log_error + states_with_tin = { "Andaman and Nicobar Islands": "35", "Andhra Pradesh": "37", @@ -241,3 +242,25 @@ 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) + + +#Get partners external connection +def get_partner_external_connection(mpesa_settings): + #check if connection is already established + if hasattr(frappe.local, "_external_conn"): + return frappe.local.press_external_conn + from frappe.frappeclient import FrappeClient + + #Fetch API from gateway + payment_gateway = frappe.get_all("Payment Gateway", filters={"gateway_controller":mpesa_settings, "gatway_setting":"Mpesa Settings"}, fields=["name","url","api_key","api_secret"]) + if not payment_gateway: + frappe.throw("Mpesa Settings not set up in Payment Gateway") + #Fetch API key and secret + api_key = payment_gateway[0].api_key + api_secret = payment_gateway[0].api_secret + url= payment_gateway[0].url + + #Establish connection + frappe.local._external_conn = FrappeClient(url, api_key=api_key, api_secret=api_secret) + return frappe.local._external_conn + diff --git a/press/utils/mpesa_utils.py b/press/utils/mpesa_utils.py new file mode 100644 index 0000000000..06b02146cd --- /dev/null +++ b/press/utils/mpesa_utils.py @@ -0,0 +1,166 @@ +# Copyright (c) 2019, Frappe Technologies and contributors +# License: MIT. See LICENSE + +import datetime +import json +from urllib.parse import parse_qs + +import frappe +from frappe.utils import get_request_session + + +def make_request(method: str, url: str, auth=None, headers=None, data=None, json=None, params=None): + auth = auth or "" + data = data or {} + headers = headers or {} + + try: + s = get_request_session() + response = frappe.flags.integration_request = s.request( + method, url, data=data, auth=auth, headers=headers, json=json, params=params + ) + response.raise_for_status() + + # Check whether the response has a content-type, before trying to check what it is + if content_type := response.headers.get("content-type"): + if content_type == "text/plain; charset=utf-8": + return parse_qs(response.text) + elif content_type.startswith("application/") and content_type.split(";")[0].endswith("json"): + return response.json() + elif response.text: + return response.text + return + except Exception as exc: + if frappe.flags.integration_request_doc: + frappe.flags.integration_request_doc.log_error() + else: + frappe.log_error() + raise exc + + +def make_get_request(url: str, **kwargs): + """Make a 'GET' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ + return make_request("GET", url, **kwargs) + + +def make_post_request(url: str, **kwargs): + """Make a 'POST' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ + return make_request("POST", url, **kwargs) + + +def make_put_request(url: str, **kwargs): + """Make a 'PUT' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ + return make_request("PUT", url, **kwargs) + + +def make_patch_request(url: str, **kwargs): + """Make a 'PATCH' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ + return make_request("PATCH", url, **kwargs) + + +def make_delete_request(url: str, **kwargs): + """Make a 'DELETE' HTTP request to the given `url` and return processed response. + + You can optionally pass the below parameters: + + * `headers`: Headers to be set in the request. + * `data`: Data to be passed in body of the request. + * `json`: JSON to be passed in the request. + * `params`: Query parameters to be passed in the request. + * `auth`: Auth credentials. + """ + return make_request("DELETE", url, **kwargs) + + +def create_request_log( + data, + integration_type=None, + service_name=None, + name=None, + error=None, + request_headers=None, + output=None, + **kwargs, +): + """ + DEPRECATED: The parameter integration_type will be removed in the next major release. + Use is_remote_request instead. + """ + if integration_type == "Remote": + kwargs["is_remote_request"] = 1 + + elif integration_type == "Subscription Notification": + kwargs["request_description"] = integration_type + + reference_doctype = reference_docname = None + if "reference_doctype" not in kwargs: + if isinstance(data, str): + data = json.loads(data) + + reference_doctype = data.get("reference_doctype") + reference_docname = data.get("reference_docname") + + integration_request = frappe.get_doc( + { + "doctype": "Mpesa Request Log", + "integration_request_service": service_name, + "request_headers": get_json(request_headers), + "data": get_json(data), + "output": get_json(output), + "error": get_json(error), + "reference_doctype": reference_doctype, + "reference_docname": reference_docname, + **kwargs, + } + ) + + if name: + integration_request.flags._name = name + + integration_request.insert(ignore_permissions=True) + frappe.db.commit() + + return integration_request + + +def get_json(obj): + return obj if isinstance(obj, str) else frappe.as_json(obj, indent=1) + + +def json_handler(obj): + if isinstance(obj, datetime.date | datetime.timedelta | datetime.datetime): + return str(obj) \ No newline at end of file