diff --git a/buzz/ticketing/doctype/event_payment/event_payment.js b/buzz/ticketing/doctype/event_payment/event_payment.js index 0cf3899c..c87f7e1b 100644 --- a/buzz/ticketing/doctype/event_payment/event_payment.js +++ b/buzz/ticketing/doctype/event_payment/event_payment.js @@ -1,8 +1,58 @@ // Copyright (c) 2025, BWH Studios and contributors // For license information, please see license.txt -// frappe.ui.form.on("Event Payment", { -// refresh(frm) { +frappe.ui.form.on("Event Payment", { + refresh(frm) { + if ( + frm.doc.payment_gateway === "Razorpay" && + frm.doc.payment_received && + frm.doc.refund_status !== "Refunded" + ) { + frm.add_custom_button(__("Refund"), () => show_refund_dialog(frm)); + } -// }, -// }); + if (frm.doc.refund_status === "Refund Pending") { + frm.add_custom_button(__("Sync Refund Status"), async () => { + const status = await frm.call("sync_refund_status"); + frappe.show_alert({ + message: __("Refund Status: {0}", [status.message]), + indicator: "green", + }); + frm.reload_doc(); + }); + } + }, +}); + +function show_refund_dialog(frm) { + const refundable_amount = frm.doc.amount - frm.doc.refunded_amount; + + const dialog = new frappe.ui.Dialog({ + title: __("Refund Payment"), + fields: [ + { + fieldname: "amount", + fieldtype: "Currency", + label: __("Refund Amount"), + options: frm.doc.currency, + default: refundable_amount, + reqd: 1, + description: __("Maximum refundable: {0}", [ + format_currency(refundable_amount, frm.doc.currency), + ]), + }, + ], + primary_action_label: __("Refund"), + async primary_action({ amount }) { + dialog.hide(); + const status = await frm.call("refund", { amount }); + frappe.show_alert({ + message: __("Refund Status: {0}", [status.message]), + indicator: "green", + }); + frm.reload_doc(); + }, + }); + + dialog.show(); +} diff --git a/buzz/ticketing/doctype/event_payment/event_payment.json b/buzz/ticketing/doctype/event_payment/event_payment.json index 0cb46c00..73e81ba9 100644 --- a/buzz/ticketing/doctype/event_payment/event_payment.json +++ b/buzz/ticketing/doctype/event_payment/event_payment.json @@ -17,7 +17,12 @@ "payment_gateway", "column_break_oauu", "payment_id", - "order_id" + "order_id", + "refund_section", + "refund_status", + "refund_id", + "column_break_refund", + "refunded_amount" ], "fields": [ { @@ -88,6 +93,37 @@ "fieldtype": "Link", "label": "Payment Gateway", "options": "Payment Gateway" + }, + { + "depends_on": "refund_status", + "fieldname": "refund_section", + "fieldtype": "Section Break", + "label": "Refund" + }, + { + "fieldname": "refund_status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Refund Status", + "options": "\nRefund Pending\nPartially Refunded\nRefunded", + "read_only": 1 + }, + { + "fieldname": "refund_id", + "fieldtype": "Data", + "label": "Refund ID", + "read_only": 1 + }, + { + "fieldname": "column_break_refund", + "fieldtype": "Column Break" + }, + { + "fieldname": "refunded_amount", + "fieldtype": "Currency", + "label": "Refunded Amount", + "options": "currency", + "read_only": 1 } ], "grid_page_length": 50, diff --git a/buzz/ticketing/doctype/event_payment/event_payment.py b/buzz/ticketing/doctype/event_payment/event_payment.py index 200ed0ae..016997f2 100644 --- a/buzz/ticketing/doctype/event_payment/event_payment.py +++ b/buzz/ticketing/doctype/event_payment/event_payment.py @@ -1,8 +1,12 @@ # Copyright (c) 2025, BWH Studios and contributors # For license information, please see license.txt -# import frappe +import frappe +from frappe import _ from frappe.model.document import Document +from frappe.utils import flt + +from buzz.payments import get_controller class EventPayment(Document): @@ -23,7 +27,53 @@ class EventPayment(Document): payment_received: DF.Check reference_docname: DF.DynamicLink | None reference_doctype: DF.Link | None + refund_id: DF.Data | None + refund_status: DF.Literal["", "Refund Pending", "Partially Refunded", "Refunded"] + refunded_amount: DF.Currency user: DF.Link # end: auto-generated types - pass + @frappe.whitelist() + def refund(self, amount: float | None = None): + frappe.only_for("System Manager") + + if not self.payment_received or not self.payment_id: + frappe.throw(_("Only received payments can be refunded")) + + if self.refund_status == "Refunded": + frappe.throw(_("This payment has already been fully refunded")) + + controller = get_controller(self.payment_gateway) + if not hasattr(controller, "refund_payment"): + frappe.throw(_("Refunds are not supported for {0}").format(self.payment_gateway)) + + refund = controller.refund_payment(self.payment_id, amount) + + self.refund_id = refund.get("id") + self.refunded_amount = flt(self.refunded_amount) + flt(refund.get("amount")) / 100 + self.set_refund_status(refund.get("status")) + self.save() + + return self.refund_status + + @frappe.whitelist() + def sync_refund_status(self): + frappe.only_for("System Manager") + + if not self.refund_id: + frappe.throw(_("No refund found on this payment")) + + controller = get_controller(self.payment_gateway) + refund = controller.fetch_refund(self.refund_id) + self.set_refund_status(refund.get("status")) + self.save() + + return self.refund_status + + def set_refund_status(self, gateway_refund_status: str): + if gateway_refund_status != "processed": + self.refund_status = "Refund Pending" + elif flt(self.refunded_amount) >= flt(self.amount): + self.refund_status = "Refunded" + else: + self.refund_status = "Partially Refunded"