Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat - webshop mpesa express #11

Merged
merged 2 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions frappe_mpsa_payments/frappe_mpsa_payments/api/m_pesa_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
from frappe import _
from requests.auth import HTTPBasicAuth
import json
from ..doctype.mpesa_settings.mpesa_settings import (
get_completed_integration_requests_info,
fetch_param_value,

)

from json import dumps, loads


def get_token(app_key, app_secret, base_url):
Expand Down Expand Up @@ -189,3 +196,80 @@ def get_mode_of_payment(mpesa_doc):
mode_of_payment = frappe.get_value("Mpesa C2B Payment Register URL", {"till_number": business_short_code, "register_status": "Success"}, "mode_of_payment")
return mode_of_payment


@frappe.whitelist(allow_guest=True)
def verify_transaction(**kwargs) -> None:
"""Verify the transaction result received via callback from stk."""

transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])

checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
if not isinstance(checkout_id, str):
frappe.throw(_("Invalid Checkout Request ID"))
print("=====================================")
print(str(transaction_response))
integration_request = frappe.get_doc("Integration Request", checkout_id)
transaction_data = frappe._dict(loads(integration_request.data))
total_paid = 0
success = False # for reporting successfull callback to point of sale ui

if transaction_response["ResultCode"] == 0:
if (
integration_request.reference_doctype
and integration_request.reference_docname
):
try:
item_response = transaction_response["CallbackMetadata"]["Item"]
amount = fetch_param_value(item_response, "Amount", "Name")
mpesa_receipt = fetch_param_value(
item_response, "MpesaReceiptNumber", "Name"
)
pr = frappe.get_doc(
integration_request.reference_doctype,
integration_request.reference_docname,
)

mpesa_receipts, completed_payments = (
get_completed_integration_requests_info(
integration_request.reference_doctype,
integration_request.reference_docname,
checkout_id,
)
)

total_paid = amount + sum(completed_payments)
mpesa_receipts = ", ".join(mpesa_receipts + [mpesa_receipt])

if total_paid >= pr.grand_total:
pr.run_method("on_payment_authorized", "Completed")
success = True

frappe.db.set_value(
"POS Invoice",
pr.reference_name,
"mpesa_receipt_number",
mpesa_receipts,
)
integration_request.handle_success(transaction_response)
except Exception:
integration_request.handle_failure(transaction_response)
frappe.log_error("Mpesa: Failed to verify transaction")

else:
integration_request.handle_failure(transaction_response)

frappe.publish_realtime(
event="process_phone_payment",
doctype="POS Invoice",
docname=transaction_data.payment_reference,
user=integration_request.owner,
message={
"amount": total_paid,
"success": success,
"failure_message": (
transaction_response["ResultDesc"]
if transaction_response["ResultCode"] != 0
else ""
),
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ def before_insert(self) -> None:

self.security_credential = base64.b64encode(ciphertext).decode("utf-8")

@frappe.whitelist()
def get_payment_url(self, **kwargs) -> str:
"""Return the payment URL"""
return "/all-products"

def on_update(self) -> None:
"""On Update Hook"""
from ....utils.utils import create_payment_gateway
Expand Down Expand Up @@ -145,7 +150,7 @@ def handle_api_response(
) -> None:
"""Response received from API calls returns a global identifier for each transaction, this code is returned during the callback."""
# check error response
if response["requestId"]:
if "requestId" in response:
req_name = response["requestId"]
error = response
else:
Expand All @@ -167,7 +172,8 @@ def generate_stk_push(**kwargs) -> str | Any:
try:
callback_url = (
get_request_site_address(True)
+ "/api/method/payments.payment_gateways.doctype.mpesa_settings.mpesa_settings.verify_transaction"
# "https://9836-41-80-117-181.ngrok-free.app"
+ "/api/method/frappe_mpsa_payments.frappe_mpsa_payments.api.m_pesa_api.verify_transaction"
)

mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
Expand All @@ -184,9 +190,9 @@ def generate_stk_push(**kwargs) -> str | Any:
app_key=mpesa_settings.consumer_key,
app_secret=mpesa_settings.get_password("consumer_secret"),
)

mobile_number = sanitize_mobile_number(args.sender)

# phone_no='0740743521'
mobile_number=sanitize_mobile_number(args.phone_number)
# mobile_number = sanitize_mobile_number(args.sender)
response = connector.stk_push(
business_shortcode=business_shortcode,
amount=args.request_amount,
Expand Down Expand Up @@ -214,82 +220,6 @@ def sanitize_mobile_number(number: str) -> str:
return "254" + str(number).lstrip("0")


@frappe.whitelist(allow_guest=True)
def verify_transaction(**kwargs) -> None:
"""Verify the transaction result received via callback from stk."""
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])

checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
if not isinstance(checkout_id, str):
frappe.throw(_("Invalid Checkout Request ID"))

integration_request = frappe.get_doc("Integration Request", checkout_id)
transaction_data = frappe._dict(loads(integration_request.data))
total_paid = 0 # for multiple integration request made against a pos invoice
success = False # for reporting successfull callback to point of sale ui

if transaction_response["ResultCode"] == 0:
if (
integration_request.reference_doctype
and integration_request.reference_docname
):
try:
item_response = transaction_response["CallbackMetadata"]["Item"]
amount = fetch_param_value(item_response, "Amount", "Name")
mpesa_receipt = fetch_param_value(
item_response, "MpesaReceiptNumber", "Name"
)
pr = frappe.get_doc(
integration_request.reference_doctype,
integration_request.reference_docname,
)

mpesa_receipts, completed_payments = (
get_completed_integration_requests_info(
integration_request.reference_doctype,
integration_request.reference_docname,
checkout_id,
)
)

total_paid = amount + sum(completed_payments)
mpesa_receipts = ", ".join(mpesa_receipts + [mpesa_receipt])

if total_paid >= pr.grand_total:
pr.run_method("on_payment_authorized", "Completed")
success = True

frappe.db.set_value(
"POS Invoice",
pr.reference_name,
"mpesa_receipt_number",
mpesa_receipts,
)
integration_request.handle_success(transaction_response)
except Exception:
integration_request.handle_failure(transaction_response)
frappe.log_error("Mpesa: Failed to verify transaction")

else:
integration_request.handle_failure(transaction_response)

frappe.publish_realtime(
event="process_phone_payment",
doctype="POS Invoice",
docname=transaction_data.payment_reference,
user=integration_request.owner,
message={
"amount": total_paid,
"success": success,
"failure_message": (
transaction_response["ResultDesc"]
if transaction_response["ResultCode"] != 0
else ""
),
},
)


def get_completed_integration_requests_info(
reference_doctype: str, reference_docname: str, checkout_id: str
) -> tuple[list, list]:
Expand Down
14 changes: 14 additions & 0 deletions frappe_mpsa_payments/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,17 @@ def save_access_token(
# TODO: Not sure what exception is thrown here. Confirm
frappe.throw("Error Encountered")
return False

def get_payment_gateway_controller(payment_gateway):
"""Return payment gateway controller"""
gateway = frappe.get_doc("Payment Gateway", payment_gateway)
if gateway.gateway_controller is None:
try:
return frappe.get_doc(f"{payment_gateway} Settings")
except Exception:
frappe.throw(_("{0} Settings not found").format(payment_gateway))
else:
try:
return frappe.get_doc(gateway.gateway_settings, gateway.gateway_controller)
except Exception:
frappe.throw(_("{0} Settings not found").format(payment_gateway))
Loading