diff --git a/payments/authorizenet/test_authorizenet.py b/payments/authorizenet/test_authorizenet.py index 1843b4cd0..c2fc1d9aa 100644 --- a/payments/authorizenet/test_authorizenet.py +++ b/payments/authorizenet/test_authorizenet.py @@ -1,3 +1,4 @@ +from datetime import date from unittest import TestCase from unittest.mock import MagicMock from unittest.mock import Mock @@ -15,7 +16,7 @@ PROCESS_DATA = { "number": "4007000000027", "expiration_0": "5", - "expiration_1": "2023", + "expiration_1": date.today().year + 1, "cvv2": "123", } diff --git a/payments/braintree/test_braintree.py b/payments/braintree/test_braintree.py index 06597c03a..c5d27f826 100644 --- a/payments/braintree/test_braintree.py +++ b/payments/braintree/test_braintree.py @@ -1,3 +1,4 @@ +from datetime import date from unittest import TestCase from unittest.mock import MagicMock from unittest.mock import Mock @@ -15,7 +16,7 @@ "name": "John Doe", "number": "371449635398431", "expiration_0": "5", - "expiration_1": "2023", + "expiration_1": date.today().year + 1, "cvv2": "1234", } diff --git a/payments/cybersource/test_cybersource.py b/payments/cybersource/test_cybersource.py index 6155e819f..061107ef2 100644 --- a/payments/cybersource/test_cybersource.py +++ b/payments/cybersource/test_cybersource.py @@ -1,3 +1,4 @@ +from datetime import date from decimal import Decimal from typing import Dict from unittest import TestCase @@ -23,7 +24,7 @@ "name": "John Doe", "number": "371449635398431", "expiration_0": "5", - "expiration_1": "2023", + "expiration_1": date.today().year + 1, "cvv2": "1234", "fingerprint": "abcd1234", } diff --git a/payments/models.py b/payments/models.py index 4a7807390..aa9fb2bf0 100644 --- a/payments/models.py +++ b/payments/models.py @@ -299,9 +299,9 @@ def refund(self, amount=None): raise ValueError( "Refund amount can not be greater then captured amount" ) - provider = provider_factory(self.variant, self) - amount = provider.refund(self, amount) - self.captured_amount -= amount + provider = provider_factory(self.variant, self) + amount = provider.refund(self, amount) + self.captured_amount -= amount if self.captured_amount == 0 and self.status != PaymentStatus.REFUNDED: self.change_status(PaymentStatus.REFUNDED) self.save() diff --git a/payments/paypal/__init__.py b/payments/paypal/__init__.py index 58caac6b4..35f9ee4f7 100644 --- a/payments/paypal/__init__.py +++ b/payments/paypal/__init__.py @@ -413,15 +413,16 @@ 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: {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 dd47722c7..c0a7710b1 100644 --- a/payments/paypal/test_paypal.py +++ b/payments/paypal/test_paypal.py @@ -1,4 +1,5 @@ import json +from datetime import date from decimal import Decimal from unittest import TestCase from unittest.mock import MagicMock @@ -24,7 +25,7 @@ "name": "John Doe", "number": "371449635398431", "expiration_0": "5", - "expiration_1": "2023", + "expiration_1": date.today().year + 1, "cvv2": "1234", } @@ -125,17 +126,39 @@ 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") diff --git a/payments/test_core.py b/payments/test_core.py index a8f640b63..a6e034a85 100644 --- a/payments/test_core.py +++ b/payments/test_core.py @@ -1,3 +1,4 @@ +from datetime import date from decimal import Decimal from unittest import TestCase from unittest.mock import NonCallableMock @@ -110,20 +111,18 @@ def test_refund_too_high_amount(self): @patch("payments.dummy.DummyProvider.refund") def test_refund_without_amount(self, mocked_refund_method): - refund_amount = None + captured_amount = Decimal("200") with patch.object(BasePayment, "save") as mocked_save_method: mocked_save_method.return_value = None - mocked_refund_method.return_value = refund_amount + mocked_refund_method.return_value = captured_amount - captured_amount = Decimal("200") - status = PaymentStatus.CONFIRMED payment = Payment( - variant="default", status=status, captured_amount=captured_amount + variant="default", status=PaymentStatus.CONFIRMED, captured_amount=captured_amount ) - payment.refund(refund_amount) - self.assertEqual(payment.status, status) - self.assertEqual(payment.captured_amount, captured_amount) - self.assertEqual(mocked_refund_method.call_count, 0) + payment.refund() + self.assertEqual(payment.status, PaymentStatus.REFUNDED) + self.assertEqual(payment.captured_amount, Decimal(0)) + self.assertEqual(mocked_refund_method.call_count, 1) @patch("payments.dummy.DummyProvider.refund") def test_refund_partial_success(self, mocked_refund_method): @@ -167,7 +166,7 @@ def setUp(self): "name": "John Doe", "number": "4716124728800975", "expiration_0": "5", - "expiration_1": "2023", + "expiration_1": date.today().year + 1, "cvv2": "123", } diff --git a/setup.cfg b/setup.cfg index 7e71486e6..eb0235a18 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,3 +14,4 @@ addopts = --color=yes testpaths = payments DJANGO_SETTINGS_MODULE = test_settings +pythonpath = .