Skip to content

Commit

Permalink
feat: support a new payment controller
Browse files Browse the repository at this point in the history
  • Loading branch information
blaggacao committed Sep 5, 2024
1 parent aa54516 commit b8b3b3d
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 126 deletions.
125 changes: 125 additions & 0 deletions erpnext/accounts/doctype/payment_request/payment_gateway_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Compatipility methods for v1 implementations of payment gateways
"""
import json

import frappe
from frappe.utils import deprecated, flt

from erpnext.utilities import payment_app_import_guard


def _get_payment_controller(*args, **kwargs):
with payment_app_import_guard():
try:
from payments.utils import get_payment_controller
except Exception:
from payments.utils import get_payment_gateway_controller as get_payment_controller

return get_payment_controller(*args, **kwargs)


def is_v2_gateway(payment_gateway):
try:
from payments.controllers import PaymentController
except Exception:
return False
try:
controller = _get_payment_controller(payment_gateway)
except Exception:
frappe.warnings.warn(f"{payment_gateway} is not a valid gateway; this is normal during tests.")
return False
return isinstance(controller, PaymentController)


def get_request_amount(self):
data_of_completed_requests = frappe.get_all(
"Integration Request",
filters={
"reference_doctype": self.doctype,
"reference_docname": self.name,
"status": "Completed",
},
pluck="data",
)
if not data_of_completed_requests:
return self.grand_total
request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
return request_amounts


def request_phone_payment(self, controller):
request_amount = get_request_amount(self)

payment_record = dict(
reference_doctype="Payment Request",
reference_docname=self.name,
payment_reference=self.reference_name,
request_amount=request_amount,
sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway,
)

controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)


def payment_gateway_validation(self, controller):
try:
if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
return True
except Exception:
return False


def get_payment_url(self, controller):
if self.reference_doctype != "Fees":
data = frappe.db.get_value(
self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
)
else:
data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["student_name"], as_dict=1)
data.update({"company": frappe.defaults.get_defaults().company})

controller.validate_transaction_currency(self.currency)

if hasattr(controller, "validate_minimum_transaction_amount"):
controller.validate_minimum_transaction_amount(self.currency, self.grand_total)

return controller.get_payment_url(
**{
"amount": flt(self.grand_total, self.precision("grand_total")),
"title": data.company.encode("utf-8"),
"description": self.subject.encode("utf-8"),
"reference_doctype": "Payment Request",
"reference_docname": self.name,
"payer_email": self.email_to or frappe.session.user,
"payer_name": frappe.safe_encode(data.customer_name),
"order_id": self.name,
"currency": self.currency,
}
)


def set_payment_request_url(self, controller):
if self.payment_account and self.payment_gateway and payment_gateway_validation(self, controller):
self.payment_url = get_payment_url(self, controller)


@deprecated
def v1_gateway_before_submit(self, payment_gateway):
try:
controller = _get_payment_controller(payment_gateway)
except Exception:
frappe.warnings.warn(f"{payment_gateway} is not a valid gateway; this is normal during tests.")
return False
if self.payment_channel == "Phone":
request_phone_payment(self, controller)
else:
set_payment_request_url(self, controller)
if not (self.mute_email or self.flags.mute_email):
self.send_email()
self.make_communication_entry()
11 changes: 8 additions & 3 deletions erpnext/accounts/doctype/payment_request/payment_request.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@
"message",
"message_examples",
"mute_email",
"payment_url",
"section_break_7",
"payment_gateway",
"payment_account",
"payment_channel",
"payment_order",
"amended_from"
"amended_from",
"column_break_pnyv",
"payment_url"
],
"fields": [
{
Expand Down Expand Up @@ -343,7 +344,7 @@
{
"fieldname": "payment_url",
"fieldtype": "Data",
"hidden": 1,
"label": "Payment URL",
"length": 500,
"options": "URL",
"read_only": 1
Expand Down Expand Up @@ -408,6 +409,10 @@
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "column_break_pnyv",
"fieldtype": "Column Break"
}
],
"in_create": 1,
Expand Down
138 changes: 43 additions & 95 deletions erpnext/accounts/doctype/payment_request/payment_request.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import json

import frappe
from frappe import _
from frappe.model.document import Document
Expand All @@ -19,12 +17,7 @@
from erpnext.accounts.utils import get_account_currency, get_currency_precision
from erpnext.utilities import payment_app_import_guard


def _get_payment_gateway_controller(*args, **kwargs):
with payment_app_import_guard():
from payments.utils import get_payment_gateway_controller

return get_payment_gateway_controller(*args, **kwargs)
from .payment_gateway_v1 import is_v2_gateway, v1_gateway_before_submit


class PaymentRequest(Document):
Expand Down Expand Up @@ -165,53 +158,53 @@ def before_submit(self):
elif self.payment_request_type == "Inward":
self.status = "Requested"

if self.payment_request_type == "Inward":
if self.payment_channel == "Phone":
self.request_phone_payment()
else:
self.set_payment_request_url()
if not (self.mute_email or self.flags.mute_email):
self.send_email()
self.make_communication_entry()

def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()

payment_record = dict(
reference_doctype="Payment Request",
reference_docname=self.name,
payment_reference=self.reference_name,
request_amount=request_amount,
sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway,
)

controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)

def get_request_amount(self):
data_of_completed_requests = frappe.get_all(
"Integration Request",
filters={
"reference_doctype": self.doctype,
"reference_docname": self.name,
"status": "Completed",
},
pluck="data",
)

if not data_of_completed_requests:
return self.grand_total
if self.payment_request_type == "Inward" and self.payment_gateway:
if not is_v2_gateway(self.payment_gateway):
return v1_gateway_before_submit(self, self.payment_gateway)
tx_data = self.get_tx_data()
with payment_app_import_guard():
from payments.controllers import PaymentController

request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
return request_amounts
controller, self.payment_session_log = PaymentController.initiate(
tx_data, self.payment_gateway
)
# checkout page can also be the target of a gatway initialized flow
# gateways also may return None if appropriate
self.payment_url = controller.get_payment_url(self.payment_session_log)
if not controller.is_user_flow_initiation_delegated(self.payment_session_log):
if not (self.mute_email or self.flags.mute_email):
self.send_email()
self.make_communication_entry()

def on_cancel(self):
self.check_if_payment_entry_exists()
self.set_as_cancelled()

def get_tx_data(self):
party = frappe.get_doc(self.party_type, self.party)
if self.party_type == "Supplier" and party.supplier_primary_contact:
contact = frappe.get_doc("Contact", party.supplier_primary_contact).as_dict()
elif self.party_type == "Customer" and party.customer_primary_contact:
contact = frappe.get_doc("Contact", party.customer_primary_contact).as_dict()
else:
contact = {}
if self.party_type == "Supplier" and party.supplier_primary_address:
address = frappe.get_doc("Address", party.supplier_primary_address).as_dict()
elif self.party_type == "Customer" and party.customer_primary_address:
address = frappe.get_doc("Address", party.customer_primary_address).as_dict()
else:
address = {}
return frappe._dict(
{
"amount": self.grand_total,
"currency": self.currency,
"reference_doctype": self.doctype,
"reference_docname": self.name,
"payer_contact": contact,
"payer_address": address,
}
)

def make_invoice(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice

Expand All @@ -220,51 +213,6 @@ def make_invoice(self):
si = si.insert(ignore_permissions=True)
si.submit()

def payment_gateway_validation(self):
try:
controller = _get_payment_gateway_controller(self.payment_gateway)
if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
return True
except Exception:
return False

def set_payment_request_url(self):
if self.payment_account and self.payment_gateway and self.payment_gateway_validation():
self.payment_url = self.get_payment_url()

def get_payment_url(self):
if self.reference_doctype != "Fees":
data = frappe.db.get_value(
self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
)
else:
data = frappe.db.get_value(
self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
)
data.update({"company": frappe.defaults.get_defaults().company})

controller = _get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency)

if hasattr(controller, "validate_minimum_transaction_amount"):
controller.validate_minimum_transaction_amount(self.currency, self.grand_total)

return controller.get_payment_url(
**{
"amount": flt(self.grand_total, self.precision("grand_total")),
"title": data.company.encode("utf-8"),
"description": self.subject.encode("utf-8"),
"reference_doctype": "Payment Request",
"reference_docname": self.name,
"payer_email": self.email_to or frappe.session.user,
"payer_name": frappe.safe_encode(data.customer_name),
"order_id": self.name,
"currency": self.currency,
}
)

def set_as_paid(self):
if self.payment_channel == "Phone":
self.db_set("status", "Paid")
Expand Down Expand Up @@ -538,7 +486,7 @@ def make_payment_request(**args):
if args.order_type == "Shopping Cart":
frappe.db.commit()
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = pr.get_payment_url()
frappe.local.response["location"] = pr.payment_url

if args.return_doc:
return pr
Expand Down
Loading

0 comments on commit b8b3b3d

Please sign in to comment.