Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 ############ //
Expand All @@ -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 ############ //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 += "<br>";
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]();
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
},
};

// #### 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);
}
});
Comment thread
vorasmit marked this conversation as resolved.

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 += "<br>";
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 (
Expand Down
26 changes: 26 additions & 0 deletions payment_integration_utils/public/js/pay_drivers.js
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import "./utils";
import "./auth";
import "./pay_drivers";
Loading