diff --git a/payment_integration_utils/payment_integration_utils/client_overrides/form/payment_entry.js b/payment_integration_utils/payment_integration_utils/client_overrides/form/payment_entry.js
index 400e20a..c85b2e7 100644
--- a/payment_integration_utils/payment_integration_utils/client_overrides/form/payment_entry.js
+++ b/payment_integration_utils/payment_integration_utils/client_overrides/form/payment_entry.js
@@ -110,6 +110,14 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("contact_mobile", "");
}
},
+
+ make_bank_online_payment: function (frm) {
+ // Flip the primary action live as the checkbox toggles (not just on reload).
+ if (frm.doc.docstatus !== 0 || frm.doc.__islocal || frm.toolbar._has_workflow) return;
+
+ if (frm.doc.make_bank_online_payment) update_submit_button_label(frm);
+ else frm.toolbar.set_primary_action(); // back to the stock Submit
+ },
});
// ############ HELPERS ############ //
@@ -124,9 +132,12 @@ function update_submit_button_label(frm) {
)
return;
- frm.page.set_primary_action(__("Pay and Submit"), () => {
- frm.savesubmit();
- });
+ // The backend claiming this PE (via integration_doctype) decides what
+ // "Pay and Submit" does; default is submit-then-pay-on-submit.
+ const driver = payment_integration_utils.get_pay_driver(frm.doc.integration_doctype);
+ const run = driver?.form || ((frm) => frm.savesubmit());
+
+ frm.page.set_primary_action(__("Pay and Submit"), () => run(frm));
}
// ############ UTILITY ############ //
diff --git a/payment_integration_utils/payment_integration_utils/client_overrides/list/payment_entry_list.js b/payment_integration_utils/payment_integration_utils/client_overrides/list/payment_entry_list.js
index 882aabc..1ebc091 100644
--- a/payment_integration_utils/payment_integration_utils/client_overrides/list/payment_entry_list.js
+++ b/payment_integration_utils/payment_integration_utils/client_overrides/list/payment_entry_list.js
@@ -10,6 +10,8 @@ frappe.listview_settings["Payment Entry"] = {
"party_bank_account",
"contact_mobile",
"contact_email",
+ // extra columns a registered pay driver needs for its own eligibility
+ ...pay_driver_fields(),
],
onload: function (list_view) {
@@ -18,40 +20,109 @@ frappe.listview_settings["Payment Entry"] = {
list_view.page.add_actions_menu_item(__("Pay and Submit"), () => {
const selected_docs = list_view.get_checked_items();
- const marked_docs = [];
- const unmarked_docs = [];
- const ineligible_docs = [];
-
- selected_docs.forEach((doc) => {
- if (can_make_payment(doc)) {
- if (doc.make_bank_online_payment) marked_docs.push(doc);
- else unmarked_docs.push(doc);
- } else {
- doc["reason"] = get_ineligibility_reason(doc);
- ineligible_docs.push(doc);
- }
- });
- if (!marked_docs.length && !unmarked_docs.length) {
- let message = __("Please select valid payment entries to pay and submit.");
+ // A backend claiming a PE (via integration_doctype) can swap the bulk
+ // flow; everything else falls through to the default submit-then-pay path.
+ const { driven, defaults } = partition_by_driver(selected_docs);
- if (ineligible_docs.length) {
- message += "
";
- message += get_ineligible_docs_html(
- ineligible_docs,
- __("View Ineligible Docs ({0})", [ineligible_docs.length])
- );
- }
+ // Each driver group is a payment flow; the default path is one too, but
+ // only when it has a payable doc (ineligible rows aren't a "method").
+ const flows = driven.map((group) => () => group.driver.bulk(list_view, group.docs));
+ if (defaults.some(can_make_payment)) {
+ flows.push(() => default_bulk(list_view, defaults));
+ }
- frappe.msgprint(message, __("Invalid Selection"));
- return;
+ // No payable flow: hand the selection to the default path so its
+ // eligibility message ("select valid entries") still shows.
+ if (!flows.length) return default_bulk(list_view, defaults);
+
+ // Pay one method at a time. A mixed selection would stack the flows'
+ // confirm/OTP dialogs and run two money-moving batches at once, so
+ // alert and run just one; the user re-runs for the rest.
+ if (flows.length > 1) {
+ frappe.show_alert({
+ message: __(
+ "Selected entries use different payment methods. Paying one method now; re-run Pay and Submit for the rest."
+ ),
+ indicator: "orange",
+ });
}
- show_confirm_dialog(list_view, marked_docs, unmarked_docs, ineligible_docs);
+ flows[0]();
});
},
};
+// #### Driver routing #### //
+// Extra list columns every registered pay driver asked for.
+function pay_driver_fields() {
+ const drivers = payment_integration_utils.pay_drivers || {};
+ return Object.values(drivers).flatMap((driver) => driver.add_fields || []);
+}
+
+// Split selected docs into driver-owned bulk groups (keyed by integration_doctype)
+// and the default remainder.
+function partition_by_driver(docs) {
+ const groups = new Map();
+ const defaults = [];
+
+ docs.forEach((doc) => {
+ const driver = payment_integration_utils.get_pay_driver(doc.integration_doctype);
+ if (driver?.bulk) {
+ if (!groups.has(doc.integration_doctype)) {
+ groups.set(doc.integration_doctype, { driver, docs: [] });
+ }
+ groups.get(doc.integration_doctype).docs.push(doc);
+ } else {
+ // A driver with a form handler but no bulk handler would silently pay
+ // via the default path (wrong server API for a pay-first backend).
+ if (driver && !driver.bulk) {
+ console.warn(
+ `pay_driver for "${doc.integration_doctype}" has no bulk handler; ` +
+ `falling back to the default path for ${doc.name}.`
+ );
+ }
+ defaults.push(doc);
+ }
+ });
+
+ return { driven: [...groups.values()], defaults };
+}
+
+// Default submit-then-pay flow (RazorpayX): confirm, OTP, bulk_pay_and_submit.
+function default_bulk(list_view, selected_docs) {
+ const marked_docs = [];
+ const unmarked_docs = [];
+ const ineligible_docs = [];
+
+ selected_docs.forEach((doc) => {
+ if (can_make_payment(doc)) {
+ if (doc.make_bank_online_payment) marked_docs.push(doc);
+ else unmarked_docs.push(doc);
+ } else {
+ doc["reason"] = get_ineligibility_reason(doc);
+ ineligible_docs.push(doc);
+ }
+ });
+
+ if (!marked_docs.length && !unmarked_docs.length) {
+ let message = __("Please select valid payment entries to pay and submit.");
+
+ if (ineligible_docs.length) {
+ message += "
";
+ message += get_ineligible_docs_html(
+ ineligible_docs,
+ __("View Ineligible Docs ({0})", [ineligible_docs.length])
+ );
+ }
+
+ frappe.msgprint(message, __("Invalid Selection"));
+ return;
+ }
+
+ show_confirm_dialog(list_view, marked_docs, unmarked_docs, ineligible_docs);
+}
+
// #### Utils #### //
function can_make_payment(doc) {
if (
diff --git a/payment_integration_utils/public/js/pay_drivers.js b/payment_integration_utils/public/js/pay_drivers.js
new file mode 100644
index 0000000..32c3aab
--- /dev/null
+++ b/payment_integration_utils/public/js/pay_drivers.js
@@ -0,0 +1,26 @@
+// Copyright (c) 2025, Resilient Tech and contributors
+// For license information, please see license.txt
+
+// Pay-driver registry: a backend claims a PE via integration_doctype, then swaps
+// what "Pay and Submit" DOES without touching the button/label/toggle/dialogs.
+// No driver for a doctype -> the default RazorpayX behaviour (savesubmit on the
+// form, OTP + bulk_pay_and_submit on the list).
+//
+// driver = {
+// form(frm), // pay-and-submit one PE; default: frm.savesubmit()
+// bulk(list_view, docs), // pay-and-submit these docs; default: OTP + bulk_pay_and_submit
+// add_fields: [...], // extra list columns the driver's eligibility needs
+// }
+
+frappe.provide("payment_integration_utils");
+
+payment_integration_utils.pay_drivers = payment_integration_utils.pay_drivers || {};
+
+payment_integration_utils.register_pay_driver = function (integration_doctype, driver) {
+ payment_integration_utils.pay_drivers[integration_doctype] = driver;
+};
+
+// The driver for a PE's integration_doctype, or null for the default flow.
+payment_integration_utils.get_pay_driver = function (integration_doctype) {
+ return payment_integration_utils.pay_drivers[integration_doctype] || null;
+};
diff --git a/payment_integration_utils/public/js/payment_integration_utils.bundle.js b/payment_integration_utils/public/js/payment_integration_utils.bundle.js
index f23c5bb..2bff546 100644
--- a/payment_integration_utils/public/js/payment_integration_utils.bundle.js
+++ b/payment_integration_utils/public/js/payment_integration_utils.bundle.js
@@ -1,2 +1,3 @@
import "./utils";
import "./auth";
+import "./pay_drivers";