From f90788667968ffc3adbe3622a6d4aa5331a1a9d9 Mon Sep 17 00:00:00 2001 From: Bianca Severino Date: Thu, 30 Apr 2026 09:04:46 -0400 Subject: [PATCH 1/2] fix: cast refund amount to string for paypal --- commerce_coordinator/apps/paypal/clients.py | 5 ++- .../apps/paypal/tests/test_clients.py | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 commerce_coordinator/apps/paypal/tests/test_clients.py diff --git a/commerce_coordinator/apps/paypal/clients.py b/commerce_coordinator/apps/paypal/clients.py index b44680adf..2f2038c26 100644 --- a/commerce_coordinator/apps/paypal/clients.py +++ b/commerce_coordinator/apps/paypal/clients.py @@ -32,6 +32,9 @@ def refund_order(self, capture_id, amount): Args: capture_id (str): The identifier of the PayPal order to capture refund. + amount (decimal.Decimal | int | float): Refund amount; coerced to str for the + request body because the PayPal SDK serializes the body with jsonpickle, + which encodes Decimal as null (PayPal expects Money.value as a string). Returns: The response from PayPal. @@ -45,7 +48,7 @@ def refund_order(self, capture_id, amount): "prefer": "return=representation", "body": { "amount": { - "value": amount, + "value": str(amount), "currency_code": "USD" } } diff --git a/commerce_coordinator/apps/paypal/tests/test_clients.py b/commerce_coordinator/apps/paypal/tests/test_clients.py new file mode 100644 index 000000000..253d743a8 --- /dev/null +++ b/commerce_coordinator/apps/paypal/tests/test_clients.py @@ -0,0 +1,40 @@ +"""Tests for PayPal API client.""" + +import json +from decimal import Decimal +from unittest.mock import MagicMock, patch + +from django.test import TestCase + +from commerce_coordinator.apps.paypal.clients import PayPalClient + + +class PayPalClientRefundOrderTests(TestCase): + """Regression tests for refund_order request shaping.""" + + @patch("commerce_coordinator.apps.paypal.clients.PaypalServersdkClient") + @patch("commerce_coordinator.apps.paypal.clients.ApiHelper.json_serialize") + def test_refund_order_serializes_decimal_amount_as_string( + self, mock_json_serialize, mock_sdk_class + ): + """ + PayPal Money.value is a string. The SDK body uses jsonpickle, which emits null + for Decimal unless we pass a string (see get_line_item_price_to_refund Decimal). + """ + mock_json_serialize.return_value = json.dumps( + { + "id": "REFUND_ID", + "create_time": "2024-01-01T00:00:00Z", + "status": "COMPLETED", + "amount": {"value": "49.00", "currency_code": "USD"}, + } + ) + mock_payments = MagicMock() + mock_sdk_class.return_value.payments = mock_payments + + client = PayPalClient() + client.refund_order("CAPTURE_ID", Decimal("49.00")) + + mock_payments.refund_captured_payment.assert_called_once() + collect = mock_payments.refund_captured_payment.call_args[0][0] + self.assertEqual(collect["body"]["amount"]["value"], "49.00") From 5d81724981f690f6037915754aa3ffa56539e4d9 Mon Sep 17 00:00:00 2001 From: Bianca Severino Date: Thu, 30 Apr 2026 09:12:33 -0400 Subject: [PATCH 2/2] fix: clarify test docstring Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- commerce_coordinator/apps/paypal/tests/test_clients.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/commerce_coordinator/apps/paypal/tests/test_clients.py b/commerce_coordinator/apps/paypal/tests/test_clients.py index 253d743a8..0ef5ab73b 100644 --- a/commerce_coordinator/apps/paypal/tests/test_clients.py +++ b/commerce_coordinator/apps/paypal/tests/test_clients.py @@ -18,8 +18,9 @@ def test_refund_order_serializes_decimal_amount_as_string( self, mock_json_serialize, mock_sdk_class ): """ - PayPal Money.value is a string. The SDK body uses jsonpickle, which emits null - for Decimal unless we pass a string (see get_line_item_price_to_refund Decimal). + PayPal Money.value is a string. The refund amount may come from + get_line_item_price_to_refund as a Decimal, and the SDK body uses + jsonpickle, which emits null for Decimal unless we pass a string. """ mock_json_serialize.return_value = json.dumps( {