Skip to content

feat(payments): Add PaymentController v2 gateway support#51723

Merged
ruthra-kumar merged 2 commits into
frappe:developfrom
nlvegan:feat/payment-controller-v2-support
May 8, 2026
Merged

feat(payments): Add PaymentController v2 gateway support#51723
ruthra-kumar merged 2 commits into
frappe:developfrom
nlvegan:feat/payment-controller-v2-support

Conversation

@0spinboson
Copy link
Copy Markdown
Contributor

@0spinboson 0spinboson commented Jan 13, 2026

Summary

Adds support for the new PaymentController interface from frappe/payments, enabling Payment Request to work with v2 payment gateways while maintaining backward compatibility with existing v1 gateways.

Related: frappe/payments#192

Documentation: frappe/frappe_io#332

Background

This PR is the ERPNext-side integration for the PaymentController architecture introduced in frappe/payments. It builds on the foundation laid by @blaggacao in:

Rather than refactoring existing Payment Request logic, this PR adds a branch that detects v2 gateways and routes them through PaymentController.initiate() while leaving v1 behavior untouched.

Changes

Gateway detection:

  • _is_v2_gateway() - Thin wrapper around payments.utils.is_v2_gateway() with import guard

New v2 flow methods:

  • _process_v2_gateway() - Calls PaymentController.initiate() and sets payment URL
  • get_tx_data() - Prepares transaction data matching the TxData interface
  • _get_party_contact_and_address() - Extracts payer contact/address info

Modified before_submit:

  • Routes to v2 or v1 flow based on gateway type
  • Email/communication handling unchanged

Design Decisions

  1. Delegation to payments app: Gateway detection logic lives in payments.utils.is_v2_gateway() to avoid duplication and ensure consistency.

  2. Request amount: Uses get_request_amount() (not grand_total) to support partial payment scenarios, consistent with existing phone payment flow.

  3. Reference semantics: tx_data.reference_doctype/docname points to Payment Request (not the underlying invoice/order) because Payment Request handles callbacks and reconciliation.

  4. Contact/address fields: Returns only payment-relevant fields rather than full document data.

Flow

Payment Request submitted
         │
         ▼
   Is v2 gateway?
    /         \
  Yes          No
   │            │
   ▼            ▼
_process_v2_   Legacy flow
gateway()      (unchanged)
   │
   ▼
PaymentController.initiate()
   │
   ▼
Payment Session Log created
   │
   ▼
/pay URL set

Dependencies

Requires frappe/payments with PaymentController support (#192).

Falls back gracefully when payments app lacks v2 support.

Test Plan

  • _is_v2_gateway() returns True for v2 gateways, False for v1
  • v2 gateway creates Payment Session Log via PaymentController.initiate()
  • v1 gateways continue working unchanged
  • Partial payment amounts handled correctly
  • Email sending respects mute_email flag for both flows

@github-actions github-actions Bot added the needs-tests This PR needs automated unit-tests. label Jan 13, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds v2 gateway support to PaymentRequest. Introduces _is_v2_gateway(payment_gateway) with an import guard and error handling. Updates before_submit to route inward requests to _process_v2_gateway() when using a v2 gateway; preserves Phone-channel handling and legacy v1 URL/email flows otherwise. Implements _process_v2_gateway(), get_tx_data(), _get_party_contact_and_address(), _get_contact_fields(), and _get_address_fields() to build minimal whitelisted payer contact/address and initiate a PaymentController session, set payment_url/psl, and surface errors. Adds extensive unit tests for detection, routing, data extraction, and error cases (including a duplicated test class).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding v2 payment gateway support via PaymentController integration to ERPNext's Payment Request.
Description check ✅ Passed The description comprehensively explains the purpose, design decisions, flow, and dependencies of the v2 gateway support changes, clearly relating to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 90.20% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @erpnext/accounts/doctype/payment_request/payment_request.py:
- Around line 47-51: The current try/except around payment_app_import_guard()
won't catch the real exception because payment_app_import_guard converts
ImportError into a frappe.throw (frappe.ValidationError); update the import
handling so that you either (A) import payments.controllers.PaymentController
directly inside a try/except ImportError and return False on ImportError, or (B)
keep the guard but also catch frappe.ValidationError (or frappe.ValidationError
and ImportError) and return False; reference the existing
payment_app_import_guard and PaymentController symbols and ensure the function
returns False when the payments app is absent.
🧹 Nitpick comments (2)
erpnext/accounts/doctype/payment_request/payment_request.py (2)

52-55: Consider narrowing the exception catch or logging failures.

The broad except Exception is flagged by static analysis (Ruff BLE001). While this is acceptable for a detection function, silently swallowing all exceptions could hide unexpected issues during gateway resolution.

♻️ Optional: Log the exception for debugging
 	try:
 		controller = _get_payment_gateway_controller(payment_gateway)
-	except Exception:
+	except Exception as e:
+		frappe.log_error(f"Failed to resolve gateway {payment_gateway}: {e}", "V2 Gateway Detection")
 		return False

248-255: Consider adding error handling for PaymentController calls.

If PaymentController.initiate() or get_payment_url() fails (e.g., gateway API error, network issue), the exception will propagate and potentially leave the Payment Request in an inconsistent state (submitted but without a payment URL).

♻️ Suggested: Wrap in try-except with meaningful error
 def _process_v2_gateway(self):
 	"""Process payment using the new PaymentController interface (v2 gateways)."""
 	tx_data = self.get_tx_data()
 	with payment_app_import_guard():
 		from payments.controllers import PaymentController

-		_controller, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)
-		self.payment_url = PaymentController.get_payment_url(psl_name)
+		try:
+			_controller, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)
+			self.payment_url = PaymentController.get_payment_url(psl_name)
+		except Exception as e:
+			frappe.throw(
+				_("Failed to initiate payment with gateway {0}: {1}").format(self.payment_gateway, str(e)),
+				title=_("Payment Gateway Error")
+			)
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da87f35 and ff1465b.

📒 Files selected for processing (1)
  • erpnext/accounts/doctype/payment_request/payment_request.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
🧬 Code graph analysis (1)
erpnext/accounts/doctype/payment_request/payment_request.py (1)
erpnext/utilities/__init__.py (1)
  • payment_app_import_guard (44-53)
🪛 Ruff (0.14.10)
erpnext/accounts/doctype/payment_request/payment_request.py

54-54: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (2)
erpnext/accounts/doctype/payment_request/payment_request.py (2)

229-243: Control flow looks correct with clear separation of v2, phone, and legacy URL paths.

The branching logic properly routes:

  • v2 gateways → _process_v2_gateway() → email sent
  • Phone channel → request_phone_payment() → returns early (no email, as expected for phone)
  • Legacy URL → set_payment_request_url() → email sent

279-288: The review comment raises a valid concern about whether loyalty_points and discount_amount should be populated from the reference document in get_tx_data(). However, verification is inconclusive: the Payment Request itself doesn't store these fields, and loyalty adjustments are already deducted from the grand_total before being passed to PaymentController. Without access to the external frappe-payments package's TxData interface definition, it cannot be confirmed whether populating these fields is required, optional, or intentionally omitted.

Comment thread erpnext/accounts/doctype/payment_request/payment_request.py
Comment thread erpnext/accounts/doctype/payment_request/payment_request.py Outdated
@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch 2 times, most recently from 81c8ebf to 593ea56 Compare January 13, 2026 12:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
erpnext/accounts/doctype/payment_request/payment_request.py (2)

51-54: Consider logging unexpected exceptions for debuggability.

The broad except Exception is intentional for graceful fallback, but silently swallowing all exceptions could mask configuration errors or bugs in the gateway controller. Consider logging at debug level before returning False.

Proposed improvement
 	try:
 		controller = _get_payment_gateway_controller(payment_gateway)
 	except Exception:
+		frappe.logger().debug(f"Could not get controller for gateway {payment_gateway}", exc_info=True)
 		return False

264-279: Consider extracting the contact/address lookup into a helper.

The Customer and Supplier branches have nearly identical logic. While acceptable, extracting this pattern could reduce duplication and make it easier to support additional party types in the future.

Proposed refactor
+	def _get_party_contact_and_address(self, party, contact_field, address_field):
+		"""Helper to fetch primary contact and address for a party."""
+		payer_contact = {}
+		payer_address = {}
+		if contact_val := getattr(party, contact_field, None):
+			if frappe.db.exists("Contact", contact_val):
+				payer_contact = frappe.get_doc("Contact", contact_val).as_dict()
+		if address_val := getattr(party, address_field, None):
+			if frappe.db.exists("Address", address_val):
+				payer_address = frappe.get_doc("Address", address_val).as_dict()
+		return payer_contact, payer_address
+
 	def get_tx_data(self):
 		...
 		if party and self.party_type == "Customer":
-			if party.customer_primary_contact and frappe.db.exists("Contact", party.customer_primary_contact):
-				payer_contact = frappe.get_doc("Contact", party.customer_primary_contact).as_dict()
-			if party.customer_primary_address and frappe.db.exists("Address", party.customer_primary_address):
-				payer_address = frappe.get_doc("Address", party.customer_primary_address).as_dict()
+			payer_contact, payer_address = self._get_party_contact_and_address(
+				party, "customer_primary_contact", "customer_primary_address"
+			)
 		elif party and self.party_type == "Supplier":
-			if party.supplier_primary_contact and frappe.db.exists("Contact", party.supplier_primary_contact):
-				payer_contact = frappe.get_doc("Contact", party.supplier_primary_contact).as_dict()
-			if party.supplier_primary_address and frappe.db.exists("Address", party.supplier_primary_address):
-				payer_address = frappe.get_doc("Address", party.supplier_primary_address).as_dict()
+			payer_contact, payer_address = self._get_party_contact_and_address(
+				party, "supplier_primary_contact", "supplier_primary_address"
+			)
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff1465b and 593ea56.

📒 Files selected for processing (1)
  • erpnext/accounts/doctype/payment_request/payment_request.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
🪛 Ruff (0.14.10)
erpnext/accounts/doctype/payment_request/payment_request.py

53-53: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (3)
erpnext/accounts/doctype/payment_request/payment_request.py (3)

228-242: Logic is correct; consider adding a comment for clarity.

The control flow is sound: v2 gateways and v1 URL payments both proceed to send emails, while v1 Phone payments return early (skipping email). The asymmetry is intentional but may confuse future readers. A brief inline comment explaining this would help.


281-290: The hardcoded None values are correct. loyalty_points are processed during make_payment_request() before the Payment Request is created, and their adjustment is already reflected in grand_total. Since get_tx_data() is called during payment processing (in _process_v2_gateway()), the payment system needs these fields in TxData but they should be None as the values have already been applied to the transaction amount.

Likely an incorrect or invalid review comment.


247-254: Add error handling for failed payment initiation.

If PaymentController.initiate() fails or returns None for psl_name, get_payment_url(psl_name) may produce an invalid URL or raise an exception. Consider validating psl_name before proceeding.

Proposed fix
 	def _process_v2_gateway(self):
 		"""Process payment using the new PaymentController interface (v2 gateways)."""
 		tx_data = self.get_tx_data()
 		with payment_app_import_guard():
 			from payments.controllers import PaymentController
 
 			_controller, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)
+			if not psl_name:
+				frappe.throw(_("Failed to initiate payment session for gateway {0}").format(self.payment_gateway))
 			self.payment_url = PaymentController.get_payment_url(psl_name)

@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch 2 times, most recently from 05b4a81 to 487f4c3 Compare January 13, 2026 13:01
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
erpnext/accounts/doctype/payment_request/payment_request.py (1)

248-255: Consider error handling for PaymentController.initiate() failure.

If PaymentController.initiate() raises an exception, the submission will fail without payment_url being set. This is likely intentional (fail fast), but you may want to confirm this aligns with desired UX—users would see a generic error rather than a payment-specific message.

If graceful handling is preferred:

🔧 Optional: Add explicit error handling
 def _process_v2_gateway(self):
     """Process payment using the new PaymentController interface (v2 gateways)."""
     tx_data = self.get_tx_data()
     with payment_app_import_guard():
         from payments.controllers import PaymentController

-        _controller, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)
-        self.payment_url = PaymentController.get_payment_url(psl_name)
+        try:
+            _controller, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)
+            self.payment_url = PaymentController.get_payment_url(psl_name)
+        except Exception as e:
+            frappe.throw(_("Failed to initiate payment: {0}").format(str(e)))
erpnext/accounts/doctype/payment_request/test_payment_request.py (2)

913-971: Good coverage of tx_data and party contact/address handling.

The tests verify:

  • All required TxData fields are present
  • Values map correctly from PaymentRequest
  • Missing party returns empty dicts
  • Nonexistent party handles gracefully

Consider adding a test for the positive case where a Customer with primary contact/address exists, to verify the data is actually fetched correctly.


973-1028: Flow routing tests verify correct branching.

The tests correctly verify:

  • v2 gateways trigger _process_v2_gateway
  • v1 gateways trigger set_payment_request_url

The pr variables (lines 987, 1019) are flagged as unused by static analysis. This is intentional since the tests verify method calls during creation, not the returned object. You can silence the linter with underscore prefix:

-                        pr = make_payment_request(
+                        _pr = make_payment_request(

The v1 test has deep nesting (6 context managers). Consider using contextlib.ExitStack for readability in future tests:

🔧 Optional: Reduce nesting with ExitStack
from contextlib import ExitStack

def test_v1_gateway_uses_legacy_flow(self):
    so = make_sales_order(currency="INR")
    
    with ExitStack() as stack:
        stack.enter_context(patch(
            "...payment_request._is_v2_gateway", return_value=False))
        mock_set_url = stack.enter_context(patch(
            "...PaymentRequest.set_payment_request_url"))
        stack.enter_context(patch(
            "...PaymentRequest.get_payment_url", return_value=PAYMENT_URL))
        stack.enter_context(patch(
            "..._get_payment_gateway_controller"))
        stack.enter_context(patch(
            "...PaymentRequest.send_email"))
        
        make_payment_request(...)
        mock_set_url.assert_called_once()
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 593ea56 and 487f4c3.

📒 Files selected for processing (2)
  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧬 Code graph analysis (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)
erpnext/accounts/doctype/payment_request/payment_request.py (4)
  • _is_v2_gateway (39-58)
  • get_tx_data (257-274)
  • _get_party_contact_and_address (276-306)
  • make_payment_request (628-766)
🪛 Ruff (0.14.10)
erpnext/accounts/doctype/payment_request/payment_request.py

53-53: Do not catch blind exception: Exception

(BLE001)

erpnext/accounts/doctype/payment_request/test_payment_request.py

987-987: Local variable pr is assigned to but never used

Remove assignment to unused variable pr

(F841)


1019-1019: Local variable pr is assigned to but never used

Remove assignment to unused variable pr

(F841)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (5)
erpnext/accounts/doctype/payment_request/payment_request.py (4)

39-58: LGTM! Defensive v2 detection with graceful fallback.

The function handles all edge cases appropriately:

  • Falsy gateway check prevents unnecessary processing
  • ImportError fallback ensures v1 compatibility when payments module lacks PaymentController
  • The broad exception catch at line 53 is justified here since _get_payment_gateway_controller can fail in various ways (gateway not found, misconfiguration, etc.), and falling back to v1 is the correct behavior
  • Type vs instance check handles both class-based and instantiated controller returns

229-243: LGTM! Clean integration with preserved backward compatibility.

The branching logic correctly:

  • Routes v2 gateways to the new flow
  • Preserves phone channel's early return (no email) behavior
  • Maintains legacy URL flow for v1 gateways
  • Applies email/communication logic consistently for both v2 and legacy URL flows

257-274: LGTM! Properly structured TxData payload.

The tx_data dict correctly maps PaymentRequest fields to the expected TxData structure. The use of self.doctype and self.name as reference fields is appropriate since the Payment Session Log should reference the PaymentRequest.

Note: loyalty_points and discount_amount are hardcoded to None. If these should be populated from the reference document (e.g., Sales Order's loyalty program data), consider adding that in a follow-up.


276-306: LGTM! Robust party contact/address retrieval.

The implementation handles edge cases well:

  • Missing party_type/party returns empty dicts
  • Deleted party handled via DoesNotExistError
  • Existence checks before fetching Contact/Address prevent secondary errors
  • Explicit field_map limits support to Customer/Supplier (appropriate for payment context)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)

874-911: Mock pattern may not work when payments module is absent.

The tests patch payments.controllers.PaymentController with create=True, but the code under test does:

from payments.controllers import PaymentController

If the payments module isn't installed, this import raises ImportError before the patch takes effect. The create=True flag creates the attribute, not the module itself.

Consider patching the import mechanism to make the test self-contained:

🔧 More robust mock approach
 def test_is_v2_gateway_detects_v2_controller(self):
     """_is_v2_gateway returns True for PaymentController subclasses."""
     from erpnext.accounts.doctype.payment_request.payment_request import _is_v2_gateway
+    import sys
+    from unittest.mock import MagicMock

     # Create mock PaymentController
     class MockPaymentController:
         pass

     class MockV2Gateway(MockPaymentController):
         pass

+    # Mock the payments.controllers module
+    mock_module = MagicMock()
+    mock_module.PaymentController = MockPaymentController
+    sys.modules["payments.controllers"] = mock_module
+
+    try:
         with patch(
             "erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
             return_value=MockV2Gateway,
         ):
-            with patch("payments.controllers.PaymentController", MockPaymentController, create=True):
-                result = _is_v2_gateway("_Test Gateway")
-                self.assertTrue(result)
+            result = _is_v2_gateway("_Test Gateway")
+            self.assertTrue(result)
+    finally:
+        sys.modules.pop("payments.controllers", None)

If the payments app is always installed in the test environment, this may work as-is. Please confirm the test passes in CI.

@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch 2 times, most recently from c865a76 to 261173f Compare January 13, 2026 14:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
erpnext/accounts/doctype/payment_request/payment_request.py (2)

254-256: Use underscore for intentionally unused variable.

The _controller variable is assigned but never used. By convention, use a single underscore to indicate an intentionally discarded value.

♻️ Suggested fix
-			_controller, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)
+			_, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)

311-317: Consider combining existence check with document fetch.

The pattern of frappe.db.exists() followed by frappe.get_doc() makes two database round trips. You could consolidate into a single call with exception handling.

♻️ Suggested refactor
 	contact_name = getattr(party, contact_field, None)
-	if contact_name and frappe.db.exists("Contact", contact_name):
-		payer_contact = frappe.get_doc("Contact", contact_name).as_dict()
+	if contact_name:
+		try:
+			payer_contact = frappe.get_doc("Contact", contact_name).as_dict()
+		except frappe.DoesNotExistError:
+			pass

 	address_name = getattr(party, address_field, None)
-	if address_name and frappe.db.exists("Address", address_name):
-		payer_address = frappe.get_doc("Address", address_name).as_dict()
+	if address_name:
+		try:
+			payer_address = frappe.get_doc("Address", address_name).as_dict()
+		except frappe.DoesNotExistError:
+			pass
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 487f4c3 and 261173f.

📒 Files selected for processing (2)
  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
🪛 Ruff (0.14.10)
erpnext/accounts/doctype/payment_request/payment_request.py

53-53: Do not catch blind exception: Exception

(BLE001)


256-256: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (3)
erpnext/accounts/doctype/payment_request/payment_request.py (3)

39-58: LGTM - Gateway detection logic is well-structured.

The function handles both the ImportError case (when payments package lacks PaymentController) and controller retrieval failures gracefully. The broad exception catch at line 53 is contextually appropriate since various errors can occur during gateway controller resolution, and falling back to v1 behavior is the correct recovery strategy.

The dual check for both issubclass (class-based controllers) and isinstance (instantiated controllers) on lines 56-58 correctly handles both gateway implementation patterns.


229-243: Verify: V2 gateways with Phone channel now send emails.

With v1 gateways, Phone payments return early (line 236) and skip email/communication. With v2 gateways, all channels fall through to the email block (lines 241-243).

If a v2 gateway is configured with payment_channel == "Phone", this represents a behavioral change. Please confirm this is intentional for v2 gateway implementations.


268-287: LGTM - Transaction data structure is well-formed.

The method correctly prepares the TxData payload. The loyalty_points and discount_amount being None is appropriate since loyalty adjustments are applied to grand_total upstream in make_payment_request() before the PaymentRequest is created.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)

1007-1037: Consider using ExitStack to reduce nesting depth.

The test correctly verifies v1 flow routing, but the 5-level deep context manager nesting hurts readability.

♻️ Optional: Flatten with ExitStack
 def test_v1_gateway_uses_legacy_flow(self):
     """v1 gateways should use set_payment_request_url flow."""
+    from contextlib import ExitStack
     so = make_sales_order(currency="INR")

-    with patch(
-        "erpnext.accounts.doctype.payment_request.payment_request._is_v2_gateway",
-        return_value=False,
-    ):
-        with patch(
-            "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.set_payment_request_url"
-        ) as mock_set_url:
-            with patch(
-                "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
-                return_value=PAYMENT_URL,
-            ):
-                with patch(
-                    "erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller"
-                ):
-                    with patch(
-                        "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email"
-                    ):
-                        make_payment_request(...)
-                        mock_set_url.assert_called_once()
+    with ExitStack() as stack:
+        stack.enter_context(patch(
+            "erpnext.accounts.doctype.payment_request.payment_request._is_v2_gateway",
+            return_value=False,
+        ))
+        mock_set_url = stack.enter_context(patch(
+            "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.set_payment_request_url"
+        ))
+        stack.enter_context(patch(
+            "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
+            return_value=PAYMENT_URL,
+        ))
+        stack.enter_context(patch(
+            "erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller"
+        ))
+        stack.enter_context(patch(
+            "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email"
+        ))
+
+        make_payment_request(
+            dt="Sales Order",
+            dn=so.name,
+            recipient_id="test@example.com",
+            mute_email=True,
+            submit_doc=True,
+            return_doc=True,
+        )
+        mock_set_url.assert_called_once()
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 261173f and 6f04b7f.

📒 Files selected for processing (2)
  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧬 Code graph analysis (2)
erpnext/accounts/doctype/payment_request/payment_request.py (1)
erpnext/utilities/__init__.py (1)
  • payment_app_import_guard (44-53)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)
erpnext/accounts/doctype/payment_request/payment_request.py (4)
  • _is_v2_gateway (39-58)
  • get_tx_data (268-287)
  • _get_party_contact_and_address (289-325)
  • make_payment_request (647-785)
🪛 Ruff (0.14.10)
erpnext/accounts/doctype/payment_request/payment_request.py

53-53: Do not catch blind exception: Exception

(BLE001)


256-256: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (7)
erpnext/accounts/doctype/payment_request/payment_request.py (5)

39-59: Well-structured v2 gateway detection with appropriate defensive handling.

The function correctly handles:

  • Missing payments app (ImportError fallback)
  • Gateway resolution failures (broad exception catch with debug logging)
  • Both class and instance checks for PaymentController

The broad Exception catch on line 53 is intentional here since _get_payment_gateway_controller can raise various exceptions (gateway not found, misconfigured, etc.), and the goal is graceful fallback to v1 behavior. This matches the pattern used in payment_app_import_guard().


229-243: Clean branching logic preserving backward compatibility.

The control flow correctly:

  1. Routes v2 gateways to _process_v2_gateway()
  2. Preserves legacy phone payment flow with early return (no email)
  3. Falls back to legacy URL flow for v1 gateways
  4. Sends email/communication for non-phone flows when not muted

248-266: Solid v2 gateway processing with proper error handling.

The method correctly:

  • Uses payment_app_import_guard() for import safety
  • Catches initiation failures and provides user-friendly localized error messages
  • Validates that psl_name is returned before proceeding

The broad Exception catch on line 256 is appropriate here since PaymentController.initiate() could fail for various gateway-specific reasons, and the goal is to surface a meaningful error to the user.


289-325: LGTM!

Well-implemented with:

  • Defensive early returns for missing party data
  • Graceful DoesNotExistError handling for deleted parties/contacts/addresses
  • Clean mapping of party type to field names
  • Individual error handling for contact vs address fetches

This follows the refactoring pattern mentioned in the commit message (consolidating existence check with document fetch).


268-287: Verify intention behind loyalty_points and discount_amount being None.

The get_tx_data() method returns loyalty_points and discount_amount as None. While the test validates that these fields are present, it does not assert expected values. Loyalty points are processed separately in make_payment_request() (lines 666-671) through a different workflow. If these fields should contain values for the PaymentController.initiate() call, they need to be populated here; otherwise, ensure the comment clarifies that these are intentionally handled outside the transaction data flow.

erpnext/accounts/doctype/payment_request/test_payment_request.py (2)

5-7: LGTM!

The added imports (sys and MagicMock) are necessary for the mocking strategy that injects payments.controllers via sys.modules, enabling tests to run regardless of whether the payments app is installed.


857-981: Good test coverage for v2 gateway detection and helpers.

The tests effectively cover:

  • Edge cases for _is_v2_gateway (None, empty, nonexistent, v2 vs v1)
  • Required TxData fields in get_tx_data
  • Graceful handling of missing/deleted parties

The sys.modules patching approach is smart—it enables testing the v2 detection logic without requiring the payments app to be installed, which aligns with the commit message about CI compatibility.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@erpnext/accounts/doctype/payment_request/payment_request.py`:
- Around line 296-353: In _get_party_contact_and_address ensure all
payer_contact values are normalized to strings so None doesn't leak into
tx_data: change the construction of payer_contact in the Contact block (the code
that sets "first_name": contact.first_name, etc.) to default first_name to an
empty string like the other fields (e.g., "first_name": contact.first_name or
""), leaving the rest of the None-safe fallbacks intact and keeping the
try/except around frappe.get_doc("Contact", ...).
- Around line 39-51: The _is_v2_gateway function only catches
frappe.ValidationError when importing payments, but any exception raised by
payments.utils.is_v2_gateway (or during import) can bubble up and break
before_submit; update _is_v2_gateway (which calls payment_app_import_guard and
payments.utils.is_v2_gateway) to catch all exceptions (e.g., Exception) as a
fallback, log the unexpected error (use frappe.log_error or process logger) for
diagnostics, and return False to ensure the code gracefully falls back to the v1
flow instead of crashing.

In `@erpnext/accounts/doctype/payment_request/test_payment_request.py`:
- Around line 900-964: Tests create a Payment Request (pr) and call
pr.insert(ignore_permissions=True) without setting mandatory fields (e.g.,
company), causing flaky failures; instead instantiate a valid Payment Request
before calling get_tx_data. Fix by either populating required fields on pr (set
"company" and any required gateway/doctype fields) before pr.insert, or replace
manual creation with the helper make_payment_request(..., return_doc=True,
submit_doc=False) and then call get_tx_data() on the returned doc; update
references in the test functions test_get_tx_data_returns_required_fields and
test_get_tx_data_uses_request_amount_not_grand_total to use the valid PR
creation approach.
- Around line 857-899: The tests for _is_v2_gateway currently patch only
"payments.utils" which causes the parent package import to fail; update the two
delegation tests (test_is_v2_gateway_delegates_to_payments_util and
test_is_v2_gateway_returns_false_when_payments_util_returns_false) to patch both
the parent package and the submodule in sys.modules (e.g. put a MagicMock for
"payments" and for "payments.utils") so that "from payments.utils import
is_v2_gateway" succeeds and then assert the behavior and mock calls as before.
🧹 Nitpick comments (2)
erpnext/accounts/doctype/payment_request/payment_request.py (1)

242-268: Log tracebacks in _process_v2_gateway() and consider narrowing/structuring the catch.

frappe.log_error(..., message=f"...{e}") drops the traceback; also logging raw exception strings can unintentionally include sensitive details depending on upstream errors.

Proposed diff
 			try:
 				_controller, psl_name = PaymentController.initiate(tx_data, self.payment_gateway)
 			except Exception as e:
 				# Log full exception for debugging, show generic message to user
 				frappe.log_error(
 					title=_("Payment Initialization Failed"),
-					message=f"Gateway: {self.payment_gateway}, Error: {e}",
+					message=f"Gateway: {self.payment_gateway}\n{frappe.get_traceback()}",
 				)
 				frappe.throw(
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)

965-1017: PII minimization assertions are good; consider making the fixture deterministic.

Right now the test passes even when no primary contact/address exists (because it conditionally asserts). If you want this to be a strong regression test, set up a customer with known customer_primary_contact / customer_primary_address and assert exact keys/values.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51a264e and 91324be.

📒 Files selected for processing (2)
  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧬 Code graph analysis (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)
erpnext/accounts/doctype/payment_request/payment_request.py (5)
  • _is_v2_gateway (39-51)
  • get_tx_data (269-294)
  • get_request_amount (372-387)
  • _get_party_contact_and_address (296-352)
  • make_payment_request (674-812)
🪛 Ruff (0.14.11)
erpnext/accounts/doctype/payment_request/payment_request.py

250-250: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (4)
erpnext/accounts/doctype/payment_request/payment_request.py (2)

222-238: before_submit() v2/v1 branching looks consistent and preserves the legacy “Phone” early-return.

The ordering ensures: v2 routing happens when detected; legacy phone requests don’t emit email/communication (Line 226–230); legacy URL flow remains intact.


269-295: The field names in get_tx_data() are validated by the test suite.

The test test_get_tx_data_returns_required_fields() explicitly verifies all returned fields including reference_docname. The test confirms the field is present and correctly set to the Payment Request name. If you need absolute confirmation that these field names match the current payments.types.TxData definition, consult the payments module documentation or source directly for the currently installed version.

erpnext/accounts/doctype/payment_request/test_payment_request.py (2)

4-8: Imports are fine; the new tests rely on module-injection, so keep sys only if you fix the guard interaction below.

No functional concerns with the imports themselves.


1018-1073: The duplicate class definition check is unnecessary; there's only one TestPaymentRequestV2Gateway class in the file.

Consider patching PaymentRequest.make_communication_entry in these tests for robustness. While mute_email=True currently guards against its execution (since make_communication_entry() is only called when not (self.mute_email or self.flags.mute_email)), patching it would protect against unexpected database side effects if the mute email logic changes later.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread erpnext/accounts/doctype/payment_request/payment_request.py
Comment thread erpnext/accounts/doctype/payment_request/payment_request.py
Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py Outdated
Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@erpnext/accounts/doctype/payment_request/test_payment_request.py`:
- Around line 953-983: The test
test_get_party_contact_and_address_returns_whitelisted_fields_only can be
vacuous because the assertions are skipped if no primary Contact/Address exists;
ensure the test always exercises _get_party_contact_and_address by creating and
linking a Contact and Address for the Customer fixture before calling
pr._get_party_contact_and_address (or assert the fixtures contain a primary
contact/address). Specifically, create new Contact and Address docs (or fetch
and mark existing ones as primary) and link them to the Customer "_Test
Customer", save them, then call pr._get_party_contact_and_address so the
subsequent whitelist assertions run unconditionally.
- Around line 1006-1061: Tests rely on implicit/default payment gateway
selection which causes flakiness; update both tests to pass a known
payment_gateway_account (e.g., "_Test Gateway - INR - _TC") into
make_payment_request to pin the gateway, and add the complementary negative
assertion to each test so the non-selected flow is explicitly not called — in
test_v2_gateway_uses_process_v2_gateway pass payment_gateway_account="_Test
Gateway - INR - _TC" to make_payment_request and also assert
PaymentRequest.set_payment_request_url was not called; in
test_v1_gateway_uses_legacy_flow pass the same payment_gateway_account and
assert PaymentRequest._process_v2_gateway was not called; keep the existing
patches of _is_v2_gateway, PaymentRequest._process_v2_gateway,
PaymentRequest.set_payment_request_url, and PaymentRequest.send_email as-is.
♻️ Duplicate comments (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)

875-899: Fix sys.modules patching: mock the parent payments package too (current tests likely fail).

from payments.utils import is_v2_gateway requires the parent payments module to exist; patching only "payments.utils" is insufficient. This was already raised previously.

Proposed fix
 	def test_is_v2_gateway_delegates_to_payments_util(self):
 		"""_is_v2_gateway delegates to payments.utils.is_v2_gateway."""
 		from erpnext.accounts.doctype.payment_request.payment_request import _is_v2_gateway

 		# Mock the payments.utils module with is_v2_gateway returning True
 		mock_utils = MagicMock()
 		mock_utils.is_v2_gateway = MagicMock(return_value=True)

-		with patch.dict(sys.modules, {"payments.utils": mock_utils}):
+		mock_payments = MagicMock()
+		mock_payments.utils = mock_utils
+		with patch.dict(sys.modules, {"payments": mock_payments, "payments.utils": mock_utils}):
 			result = _is_v2_gateway("_Test Gateway")
 			self.assertTrue(result)
 			mock_utils.is_v2_gateway.assert_called_once_with("_Test Gateway")

 	def test_is_v2_gateway_returns_false_when_payments_util_returns_false(self):
 		"""_is_v2_gateway returns False when payments.utils.is_v2_gateway returns False."""
 		from erpnext.accounts.doctype.payment_request.payment_request import _is_v2_gateway

 		# Mock the payments.utils module with is_v2_gateway returning False
 		mock_utils = MagicMock()
 		mock_utils.is_v2_gateway = MagicMock(return_value=False)

-		with patch.dict(sys.modules, {"payments.utils": mock_utils}):
+		mock_payments = MagicMock()
+		mock_payments.utils = mock_utils
+		with patch.dict(sys.modules, {"payments": mock_payments, "payments.utils": mock_utils}):
 			result = _is_v2_gateway("_Test Gateway")
 			self.assertFalse(result)
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91324be and 7acfca7.

📒 Files selected for processing (1)
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧬 Code graph analysis (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)
erpnext/accounts/doctype/payment_request/payment_request.py (4)
  • _is_v2_gateway (39-51)
  • get_tx_data (269-294)
  • get_request_amount (372-387)
  • _get_party_contact_and_address (296-352)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (2)
erpnext/accounts/doctype/payment_request/test_payment_request.py (2)

4-8: Imports look fine (and sys is actually used).


857-1061: No duplication exists—the AI summary was incorrect. The class TestPaymentRequestV2Gateway appears only once (line 857).

However, the flow-selection tests test_v2_gateway_uses_process_v2_gateway and test_v1_gateway_uses_legacy_flow (lines 1006 and 1031) don't configure an actual payment gateway account. They rely entirely on mocking _is_v2_gateway, which makes it impossible to verify that the gateway selection logic correctly routes to either _process_v2_gateway or set_payment_request_url based on real gateway configuration. Consider adding a test variant that sets up an actual payment gateway document with v2 or v1 configuration before calling make_payment_request.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py
Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py
Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@erpnext/accounts/doctype/payment_request/test_payment_request.py`:
- Around line 1044-1045: The test's allowed_contact_fields set in
test_payment_request.py is missing the "email" key used by
_get_party_contact_and_address(); update the allowed_contact_fields set (used in
the assertion around contact.keys()) to include both "email_id" and "email" so
the subset assertion passes when the contact dict contains the alias "email".
♻️ Duplicate comments (1)
erpnext/accounts/doctype/payment_request/payment_request.py (1)

337-343: Normalize first_name to prevent None from leaking into tx_data.

All other contact fields use or "" for null-safety, but first_name is passed through as-is. If the contact has no first_name, this will pass None to the payment gateway, which may cause validation issues if it expects strings.

🐛 Proposed fix
 				payer_contact = {
-					"first_name": contact.first_name,
+					"first_name": contact.first_name or "",
 					"last_name": contact.last_name or "",
 					"email_id": contact.email_id or "",
 					"email": contact.email_id or "",  # Alias for gateway compatibility
 					"phone": contact.phone or contact.mobile_no or "",
 				}
🧹 Nitpick comments (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)

1083-1152: Comprehensive flow selection tests with proper isolation.

Both tests:

  • Pass explicit payment_gateway_account to avoid fixture coupling
  • Assert the expected flow is called
  • Assert the alternative flow is NOT called (mutual exclusivity)

The nested with statements could be simplified with contextlib.ExitStack, but functionality is correct.

♻️ Optional: Simplify nested context managers
from contextlib import ExitStack

def test_v2_gateway_uses_process_v2_gateway(self):
    """v2 gateways should use _process_v2_gateway flow, not legacy flow."""
    so = make_sales_order(currency="INR")

    with ExitStack() as stack:
        stack.enter_context(patch(
            "erpnext.accounts.doctype.payment_request.payment_request._is_v2_gateway",
            return_value=True,
        ))
        mock_process_v2 = stack.enter_context(patch(
            "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest._process_v2_gateway"
        ))
        mock_set_url = stack.enter_context(patch(
            "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.set_payment_request_url"
        ))
        stack.enter_context(patch(
            "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email"
        ))

        make_payment_request(...)

        mock_process_v2.assert_called_once()
        mock_set_url.assert_not_called()
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0a25d9 and e2f1c3c.

📒 Files selected for processing (2)
  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-16T16:00:08.157Z
Learnt from: barredterra
Repo: frappe/erpnext PR: 50159
File: erpnext/public/js/utils/sales_common.js:122-125
Timestamp: 2025-11-16T16:00:08.157Z
Learning: In ERPNext sales transactions (erpnext/public/js/utils/sales_common.js and erpnext/controllers/selling_controller.py), the company_contact_person field has different update behaviors: Frontend set_default_company_contact_person() should update the contact when the company field changes (intentional override on user action), while backend set_company_contact_person() in set_missing_values() should only fill empty fields (no override during save operations).

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/payment_request.py
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧬 Code graph analysis (2)
erpnext/accounts/doctype/payment_request/payment_request.py (1)
erpnext/utilities/__init__.py (1)
  • payment_app_import_guard (44-53)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)
erpnext/accounts/doctype/payment_request/payment_request.py (5)
  • _is_v2_gateway (39-57)
  • make_payment_request (685-823)
  • get_tx_data (279-304)
  • get_request_amount (383-398)
  • _get_party_contact_and_address (306-363)
🪛 Ruff (0.14.11)
erpnext/accounts/doctype/payment_request/payment_request.py

53-53: Do not catch blind exception: Exception

(BLE001)


256-256: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (9)
erpnext/accounts/doctype/payment_request/payment_request.py (4)

39-57: Well-structured defensive error handling for gateway detection.

The function properly delegates to payments.utils.is_v2_gateway() with robust fallback behavior:

  • Catches frappe.ValidationError when the payments app is missing (from payment_app_import_guard)
  • Catches unexpected exceptions with logging to prevent submission failures
  • Returns False on any error to gracefully fall back to v1 flow

The catch-all Exception handler (flagged by Ruff BLE001) is intentional here—this is a detection utility where failing silently to v1 is the correct behavior to preserve backward compatibility.


228-243: LGTM - Clean routing logic with proper flow separation.

The branching correctly handles:

  1. v2 gateways → _process_v2_gateway() then email/communication
  2. Phone channel → request_phone_payment() with early return (no email)
  3. Legacy v1 → set_payment_request_url() then email/communication

The early return on line 236 for Phone payments is correct since phone payments don't generate email/link communications.


248-277: Good error handling and defensive coding for external gateway integration.

The method correctly:

  • Logs detailed error info for debugging while showing user-friendly messages
  • Validates that psl_name is returned before proceeding
  • Uses hasattr check for payment_session_log to handle environments where the custom field hasn't been added yet

The catch-all Exception (Ruff BLE001) is appropriate here since PaymentController.initiate() is an external call that could raise various unpredictable exceptions.


279-304: LGTM - Clean tx_data preparation with good documentation.

The design decision to have reference_doctype/reference_docname point to the Payment Request (not the underlying document) is well-documented and makes sense for callback handling and reconciliation.

Using self.get_request_amount() correctly supports partial payment scenarios.

erpnext/accounts/doctype/payment_request/test_payment_request.py (5)

5-7: LGTM - Required imports for mocking.

The sys import is needed for patch.dict(sys.modules, ...) and MagicMock is used for the mock utilities helper.


860-870: Well-designed helper for mocking the payments module hierarchy.

The helper correctly mocks both the parent payments package and payments.utils submodule, which is required for Python's import mechanism to work. The docstring clearly explains the rationale.


872-941: Comprehensive tests for _is_v2_gateway edge cases and delegation.

Good coverage including:

  • Falsy inputs (None, empty string)
  • Nonexistent gateway
  • Proper delegation to payments.utils.is_v2_gateway
  • Exception handling for unexpected errors

The tests properly use the module mocking helper to isolate behavior.


942-993: LGTM - Good test setup using make_payment_request helper.

The tests properly use make_payment_request() to create valid Payment Request documents with all mandatory fields populated. The partial payment test correctly verifies that get_request_amount() is used rather than grand_total.


1061-1081: Good edge case tests for missing/deleted party handling.

These tests verify the graceful fallback behavior when the party doesn't exist or is deleted, matching the DoesNotExistError handling in the implementation.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@erpnext/accounts/doctype/payment_request/test_payment_request.py`:
- Around line 857-871: Add a tearDown method to TestPaymentRequestV2Gateway to
mirror TestPaymentRequest and ensure test isolation: implement def
tearDown(self): and call frappe.db.rollback() (optionally call
super().tearDown() if the base class defines it) so that any Contact/Address or
_Test Customer changes made in tests are rolled back after each test.
🧹 Nitpick comments (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)

1096-1165: LGTM!

Flow selection tests correctly verify:

  • v2 gateways use _process_v2_gateway and skip legacy URL flow
  • v1 gateways use set_payment_request_url and skip v2 flow
  • Both tests use explicit payment_gateway_account for deterministic behavior

Optional: Consider using @patch decorators or ExitStack to reduce the deep nesting, though the current approach is functionally correct.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9dcef4 and 64a1239.

📒 Files selected for processing (1)
  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-16T16:00:08.157Z
Learnt from: barredterra
Repo: frappe/erpnext PR: 50159
File: erpnext/public/js/utils/sales_common.js:122-125
Timestamp: 2025-11-16T16:00:08.157Z
Learning: In ERPNext sales transactions (erpnext/public/js/utils/sales_common.js and erpnext/controllers/selling_controller.py), the company_contact_person field has different update behaviors: Frontend set_default_company_contact_person() should update the contact when the company field changes (intentional override on user action), while backend set_company_contact_person() in set_missing_values() should only fill empty fields (no override during save operations).

Applied to files:

  • erpnext/accounts/doctype/payment_request/test_payment_request.py
📚 Learning: 2025-12-16T05:33:58.723Z
Learnt from: Abdeali099
Repo: frappe/erpnext PR: 51078
File: erpnext/accounts/doctype/financial_report_template/financial_report_engine.py:486-491
Timestamp: 2025-12-16T05:33:58.723Z
Learning: In ERPNext/Frappe codebase, query.run(as_dict=True) returns frappe._dict objects that support both dict-style access (obj["key"]) and attribute-style access (obj.key). Therefore, attribute access on query results is valid and will not raise AttributeError. When reviewing Python code, prefer attribute access (obj.key) for readability where the key is known to exist, but ensure existence checks or fallback handling if there is any doubt about missing keys.

Applied to files:

  • erpnext/accounts/doctype/payment_request/test_payment_request.py
🧬 Code graph analysis (1)
erpnext/accounts/doctype/payment_request/test_payment_request.py (1)
erpnext/accounts/doctype/payment_request/payment_request.py (5)
  • _is_v2_gateway (39-57)
  • make_payment_request (685-823)
  • get_tx_data (279-304)
  • get_request_amount (383-398)
  • _get_party_contact_and_address (306-363)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Summary
🔇 Additional comments (6)
erpnext/accounts/doctype/payment_request/test_payment_request.py (6)

5-7: LGTM!

The sys and MagicMock imports are necessary for the module-level mocking pattern used in the v2 gateway tests.


873-883: LGTM!

Good helper pattern that properly mocks both the parent payments module and the child payments.utils module. This addresses Python's module import requirements and enables clean test isolation.


885-954: LGTM!

Comprehensive test coverage for _is_v2_gateway:

  • Edge cases (None, empty string, nonexistent gateway) properly delegate to the mocked utility
  • Delegation tests verify correct interaction with payments.utils.is_v2_gateway
  • Exception handling test confirms graceful fallback behavior

The module mocking pattern correctly addresses Python's import requirements.


955-1006: LGTM!

Well-structured tests for get_tx_data:

  • Uses make_payment_request to properly construct valid Payment Requests
  • Verifies all TxData-required fields are present
  • Correctly tests partial payment support by mocking get_request_amount

1008-1072: LGTM!

Comprehensive whitelist test that:

  • Creates fixture data to ensure assertions always run (addresses past review)
  • Includes both email_id and email in allowed fields (addresses past review)
  • Verifies sensitive fields are excluded from the response

The fixture creation pattern is acceptable for integration tests with transaction rollback.


1074-1094: LGTM!

Good edge case coverage for _get_party_contact_and_address:

  • Missing party (None values) returns empty dicts
  • Deleted/nonexistent party handles DoesNotExistError gracefully

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py Outdated
@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch from 5c9e518 to 6ed0a8a Compare January 15, 2026 10:39
@0spinboson
Copy link
Copy Markdown
Contributor Author

0spinboson commented Jan 15, 2026

okay, should be done now, apologies for the earlier noise, should've done a PR in my own fork first.

@mahsem
Copy link
Copy Markdown
Contributor

mahsem commented Jan 16, 2026

okay, should be done now, apologies for the earlier noise, should've done a PR in my own fork first.

@0spinboson backport v15 v16 ?

@0spinboson
Copy link
Copy Markdown
Contributor Author

okay, should be done now, apologies for the earlier noise, should've done a PR in my own fork first.

@0spinboson backport v15 v16 ?

should work fine in v15 as well I think, aside from the payments CI updates that were needed for develop/v16.

@ruthra-kumar ruthra-kumar self-assigned this Jan 19, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 19, 2026

Codecov Report

❌ Patch coverage is 98.79032% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.86%. Comparing base (0cd0b82) to head (b9e40a4).
⚠️ Report is 15 commits behind head on develop.

Files with missing lines Patch % Lines
...ts/doctype/payment_request/test_payment_request.py 98.61% 6 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #51723      +/-   ##
===========================================
+ Coverage    79.78%   79.86%   +0.07%     
===========================================
  Files         1159     1159              
  Lines       126362   126852     +490     
===========================================
+ Hits        100822   101310     +488     
- Misses       25540    25542       +2     
Files with missing lines Coverage Δ
...ccounts/doctype/payment_request/payment_request.py 72.96% <100.00%> (+2.77%) ⬆️
...ts/doctype/payment_request/test_payment_request.py 99.31% <98.61%> (-0.69%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

0spinboson added a commit to nlvegan/frappe_io that referenced this pull request Jan 19, 2026
Adds documentation for the new PaymentController v2 architecture in the
payments app, covering:

- Architecture overview and components
- Integration with ERPNext Payment Request
- Implementing v2 payment gateways
- Stripe v2 configuration guide
- API reference
- Troubleshooting guide

Related: frappe/erpnext#51723, frappe/payments#192
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@erpnext/accounts/doctype/payment_request/test_payment_request.py`:
- Around line 1388-1395: The test unpacks two values from Payment
Request._get_party_contact_and_address but never uses the second one causing a
lint error; change the unpack target from "address" to a dummy name like "_" or
"_address" so the value is ignored (e.g., contact, _ =
pr._get_party_contact_and_address()), keeping the call to
_get_party_contact_and_address and the assert on contact intact.
- Around line 1409-1416: The test unpacks pr._get_party_contact_and_address()
into an unused variable named contact which triggers lint; change the unpack to
use a dummy/underscore name (e.g., _contact or _) instead of contact so the
address assertion remains the same. Locate the unpack in test_payment_request.py
where _get_party_contact_and_address() is called and replace the first variable
(contact) with a dummy name to satisfy Ruff while leaving the subsequent address
assertion untouched.
- Around line 1425-1450: The test assigns the return of make_payment_request to
an unused local variable pr in test_v2_gateway_sends_email_when_not_muted;
remove the unused assignment by calling make_payment_request(...) without
assigning its result (or replace pr with a throwaway name) so the function is
still invoked but no unused local remains, updating the
test_v2_gateway_sends_email_when_not_muted function accordingly.

Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py
Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py
Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py
@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch from f45179c to 5187e97 Compare January 19, 2026 11:16
@0spinboson
Copy link
Copy Markdown
Contributor Author

0spinboson commented Feb 20, 2026

@0spinboson This does look like a good addition, but we'll have to wait till the test suite refactor ongoing in ERPNext. It will mostly be done by this month. Until then, can you address the broken CI test in this PR.

Moreover frappe/payments#192 has lots of unrelated CI changes as well and won't be easy to review in one go. Remove all unrelated code and break changes into smaller chunks.

Thanks, will do. To clarify, how would you prefer I split up the PRs for the payments side, and/or what would you consider unrelated code?

Like so?:

  • Separate PR for the CI version bumps + commitlint fix
  • full Paymentcontroller base arch (~2500 loc)
  • new stripe implementation on top of this (~2300 loc)
  • e2e tests (playwright+CI workflow) (~2300 loc)

or would you want it to be split up further?

@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch 3 times, most recently from 54632e9 to 232a1b5 Compare February 20, 2026 11:53
@ruthra-kumar
Copy link
Copy Markdown
Member

  • Separate PR for the CI version bumps + commitlint fix
  • full Paymentcontroller base arch (~2500 loc)
  • new stripe implementation on top of this (~2300 loc)

Start with just the payment controller changes. Once the ERPNext side and Payments side controller changes are done, then others can be handled in separate PR.

  • e2e tests (playwright+CI workflow) (~2300 loc)

Is this needed?

@0spinboson
Copy link
Copy Markdown
Contributor Author

Okay, will do in a day or two, thank you

@0spinboson
Copy link
Copy Markdown
Contributor Author

should be done

@stale
Copy link
Copy Markdown

stale Bot commented Mar 20, 2026

This pull request has been automatically marked as inactive because it has not had recent activity. It will be closed within 3 days if no further activity occurs, but it only takes a comment to keep a contribution alive :) Also, even if it is closed, you can always reopen the PR when you're ready. Thank you for contributing.

@stale stale Bot added the inactive label Mar 20, 2026
@0spinboson
Copy link
Copy Markdown
Contributor Author

should be active in the background

@stale stale Bot removed the inactive label Mar 21, 2026
@stale
Copy link
Copy Markdown

stale Bot commented Apr 11, 2026

This pull request has been automatically marked as inactive because it has not had recent activity. It will be closed within 3 days if no further activity occurs, but it only takes a comment to keep a contribution alive :) Also, even if it is closed, you can always reopen the PR when you're ready. Thank you for contributing.

@stale stale Bot added the inactive label Apr 11, 2026
@0spinboson
Copy link
Copy Markdown
Contributor Author

Hiya. Hope you are well. any idea when I can expect feedback?

Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py Outdated
Comment thread erpnext/accounts/doctype/payment_request/test_payment_request.py Outdated
@stale stale Bot removed the inactive label Apr 15, 2026
@0spinboson
Copy link
Copy Markdown
Contributor Author

what shall I do about the test failures do to the change to ledger entries (now submittable)? Leave it for now?

@ruthra-kumar
Copy link
Copy Markdown
Member

what shall I do about the test failures do to the change to ledger entries (now submittable)? Leave it for now?

Resolve the conflicts so CI can run. So that we can find out which tests break.

@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch 2 times, most recently from b721df4 to 13fdf35 Compare April 18, 2026 11:02
@0spinboson
Copy link
Copy Markdown
Contributor Author

done

@ruthra-kumar
Copy link
Copy Markdown
Member

Will review by this week

@ruthra-kumar
Copy link
Copy Markdown
Member

@0spinboson
Change looks good. Can you rebase on latest develop.

0spinboson added 2 commits May 4, 2026 21:49
Add support for the new PaymentController interface from frappe/payments,
enabling Payment Request to work with v2 gateways while maintaining
backward compatibility with v1.

Related: frappe/payments#192
@0spinboson 0spinboson force-pushed the feat/payment-controller-v2-support branch from 13fdf35 to b9e40a4 Compare May 4, 2026 19:52
@0spinboson
Copy link
Copy Markdown
Contributor Author

done

@ruthra-kumar ruthra-kumar merged commit 385835a into frappe:develop May 8, 2026
13 of 14 checks passed
@mahsem
Copy link
Copy Markdown
Contributor

mahsem commented May 8, 2026

@ruthra-kumar @0spinboson backport v-15 v-16 ?

@0spinboson 0spinboson deleted the feat/payment-controller-v2-support branch May 17, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-tests This PR needs automated unit-tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants