From 39d65643f9f86b9045b3d1fc9b2e173a86d60b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radek=20Hol=C3=BD?= Date: Wed, 24 Apr 2024 18:25:10 +0200 Subject: [PATCH] PayPal: Perform full refund if amount=None --- CHANGELOG.rst | 1 + payments/paypal/__init__.py | 16 +++++++----- payments/paypal/test_paypal.py | 46 ++++++++++++++++++++++++++++++---- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c14c8b243..3432d5a20 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ v3.0.0 - New :ref:`webhook settings ` - Fixed PayPal backends not saving captured_amount when processing data. - Fixed ``base_payment.refund()`` not making any refund +- PayPal backends now perform a full refund if ``amount=None``. v2.0.0 ------ diff --git a/payments/paypal/__init__.py b/payments/paypal/__init__.py index 002736e53..c4fe30c3e 100644 --- a/payments/paypal/__init__.py +++ b/payments/paypal/__init__.py @@ -313,15 +313,19 @@ def release(self, payment): self.post(payment, url) def refund(self, payment, amount=None): - if amount is None: - amount = payment.captured_amount - amount_data = self.get_amount_data(payment, amount) - refund_data = {"amount": amount_data} + refund_data = {} + if amount is not None: + refund_data["amount"] = self.get_amount_data(payment, amount) links = self._get_links(payment) url = links["refund"]["href"] - self.post(payment, url, data=refund_data) + response = self.post(payment, url, data=refund_data) payment.change_status(PaymentStatus.REFUNDED) - return amount + if response["amount"]["currency"] != payment.currency: + raise NotImplementedError( + f"refund's currency other than {payment.currency} not supported yet: " + f"{response['amount']['currency']}" + ) + return Decimal(response["amount"]["total"]) class PaypalCardProvider(PaypalProvider): diff --git a/payments/paypal/test_paypal.py b/payments/paypal/test_paypal.py index 0611071ee..41bdb2a13 100644 --- a/payments/paypal/test_paypal.py +++ b/payments/paypal/test_paypal.py @@ -234,17 +234,53 @@ def test_provider_handles_captured_payment(self, mocked_post): self.assertEqual(self.payment.status, PaymentStatus.CONFIRMED) @patch("requests.post") - def test_provider_refunds_payment(self, mocked_post): + def test_provider_refunds_payment_fully(self, mocked_post): data = MagicMock() - data.return_value = { - "token_type": "test_token_type", - "access_token": "test_access_token", - } + data.side_effect = [ + { + "token_type": "test_token_type", + "access_token": "test_access_token", + }, + {"amount": {"total": "220.00", "currency": "USD"}}, + ] post = MagicMock() post.json = data post.status_code = 200 mocked_post.return_value = post self.provider.refund(self.payment) + mocked_post.assert_called_with( + "http://refund.com", + headers={ + "Content-Type": "application/json", + "Authorization": "test_token_type test_access_token", + }, + data="{}", + ) + self.assertEqual(self.payment.status, PaymentStatus.REFUNDED) + + @patch("requests.post") + def test_provider_refunds_payment_partially(self, mocked_post): + data = MagicMock() + data.side_effect = [ + { + "token_type": "test_token_type", + "access_token": "test_access_token", + }, + {"amount": {"total": "1.00", "currency": "USD"}}, + ] + post = MagicMock() + post.json = data + post.status_code = 200 + mocked_post.return_value = post + self.provider.refund(self.payment, amount=Decimal(1)) + mocked_post.assert_called_with( + "http://refund.com", + headers={ + "Content-Type": "application/json", + "Authorization": "test_token_type test_access_token", + }, + data='{"amount": {"currency": "USD", "total": "1.00"}}', + ) self.assertEqual(self.payment.status, PaymentStatus.REFUNDED) @patch("requests.post")