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";