diff --git a/paypal/pro/helpers.py b/paypal/pro/helpers.py index 4942bf2..f6bf283 100644 --- a/paypal/pro/helpers.py +++ b/paypal/pro/helpers.py @@ -190,7 +190,13 @@ def manangeRecurringPaymentsProfileStatus(self, params, fail_silently=False): return nvp_obj def refundTransaction(self, params): - raise NotImplementedError + defaults = {"method": "refundTransaction", "refundtype": 'Full'} + required = L("transactionid") + + nvp_obj = self._fetch(params, required, defaults) + if nvp_obj.flag: + raise PayPalFailure(nvp_obj.flag_info) + return nvp_obj def _is_recurring(self, params): """Returns True if the item passed is a recurring transaction.""" diff --git a/paypal/standard/ipn/models.py b/paypal/standard/ipn/models.py index 932972f..fe4f5f6 100644 --- a/paypal/standard/ipn/models.py +++ b/paypal/standard/ipn/models.py @@ -24,9 +24,11 @@ def _verify_postback(self): def send_signals(self): """Shout for the world to hear whether a txn was successful.""" # Transaction signals: - if self.is_transaction(): + if self.is_transaction() and not self.is_recurring(): if self.flag: payment_was_flagged.send(sender=self) + elif self.is_refund(): + payment_refunded.send(sender=self) else: payment_was_successful.send(sender=self) # Recurring payment signals: @@ -38,6 +40,9 @@ def send_signals(self): recurring_payment.send(sender=self) elif self.is_recurring_cancel(): recurring_cancel.send(sender=self) + elif self.is_refund(): + recurring_refunded.send(sender=self) + # Subscription signals: else: if self.is_subscription_cancellation(): diff --git a/paypal/standard/ipn/signals.py b/paypal/standard/ipn/signals.py index f0d010f..22b91c9 100644 --- a/paypal/standard/ipn/signals.py +++ b/paypal/standard/ipn/signals.py @@ -12,6 +12,9 @@ # Sent when a payment is flagged. payment_was_flagged = Signal() +# Sent when a payment is refunded +payment_refunded = Signal() + # Sent when a subscription was cancelled. subscription_cancel = Signal() @@ -30,4 +33,6 @@ # recurring_payment recurring_payment = Signal() -recurring_cancel = Signal() \ No newline at end of file +recurring_cancel = Signal() + +recurring_refunded = Signal() diff --git a/paypal/standard/ipn/tests/test_ipn.py b/paypal/standard/ipn/tests/test_ipn.py index 376b51d..17e9007 100644 --- a/paypal/standard/ipn/tests/test_ipn.py +++ b/paypal/standard/ipn/tests/test_ipn.py @@ -3,17 +3,17 @@ from django.test import TestCase from django.test.client import Client -from paypal.standard.models import ST_PP_CANCELLED +from paypal.standard.models import ST_PP_CANCELLED, ST_PP_REFUNDED from paypal.standard.ipn.models import PayPalIPN from paypal.standard.ipn.signals import (payment_was_successful, - payment_was_flagged) + payment_was_flagged, recurring_refunded, payment_refunded) IPN_POST_PARAMS = { "protection_eligibility": "Ineligible", "last_name": "User", "txn_id": "51403485VH153354B", - "receiver_email": settings.PAYPAL_RECEIVER_EMAIL, + "receiver_email": str(settings.PAYPAL_RECEIVER_EMAIL), "payment_status": "Completed", "payment_gross": "10.00", "tax": "0.00", @@ -60,7 +60,7 @@ def tearDown(self): settings.DEBUG = self.old_debug PayPalIPN._postback = self.old_postback - def assertGotSignal(self, signal, flagged): + def assertGotSignal(self, signal, flagged, params=IPN_POST_PARAMS): # Check the signal was sent. These get lost if they don't reference self. self.got_signal = False self.signal_obj = None @@ -70,7 +70,7 @@ def handle_signal(sender, **kwargs): self.signal_obj = sender signal.connect(handle_signal) - response = self.client.post("/ipn/", IPN_POST_PARAMS) + response = self.client.post("/ipn/", params) self.assertEqual(response.status_code, 200) ipns = PayPalIPN.objects.all() self.assertEqual(len(ipns), 1) @@ -115,11 +115,33 @@ def test_vaid_payment_status_cancelled(self): ipn_obj = PayPalIPN.objects.all()[0] self.assertEqual(ipn_obj.flag, False) - def test_duplicate_txn_id(self): self.client.post("/ipn/", IPN_POST_PARAMS) self.client.post("/ipn/", IPN_POST_PARAMS) self.assertEqual(len(PayPalIPN.objects.all()), 2) ipn_obj = PayPalIPN.objects.order_by('-created_at')[0] self.assertEqual(ipn_obj.flag, True) - self.assertEqual(ipn_obj.flag_info, "Duplicate txn_id. (51403485VH153354B)") \ No newline at end of file + self.assertEqual(ipn_obj.flag_info, "Duplicate txn_id. (51403485VH153354B)") + + def test_refund_for_recurring_payment(self): + update = { + "payment_status": ST_PP_REFUNDED, + "reason_code": "refund", + "parent_txn_id": "1NK420530S625752Y", + "recurring_payment_id": "I-N8KNB3KKD9NF" + } + params = IPN_POST_PARAMS.copy() + params.update(update) + + self.assertGotSignal(recurring_refunded, False, params) + + def test_refund_for_non_recurring_payment(self): + update = { + "payment_status": ST_PP_REFUNDED, + "reason_code": "refund", + "parent_txn_id": "1NK420530S625752Y" + } + params = IPN_POST_PARAMS.copy() + params.update(update) + + self.assertGotSignal(payment_refunded, False, params) \ No newline at end of file diff --git a/paypal/standard/models.py b/paypal/standard/models.py index 7a2cb4d..bb55728 100644 --- a/paypal/standard/models.py +++ b/paypal/standard/models.py @@ -14,6 +14,7 @@ ST_PP_PENDING = 'Pending' ST_PP_PROCESSED = 'Processed' ST_PP_REFUSED = 'Refused' +ST_PP_REFUNDED = 'Refunded' ST_PP_REVERSED = 'Reversed' ST_PP_REWARDED = 'Rewarded' ST_PP_UNCLAIMED = 'Unclaimed' @@ -29,7 +30,7 @@ class PayPalStandardBase(Model): # @@@ Might want to add all these one distant day. # FLAG_CODE_CHOICES = ( # PAYMENT_STATUS_CHOICES = "Canceled_ Reversal Completed Denied Expired Failed Pending Processed Refunded Reversed Voided".split() - PAYMENT_STATUS_CHOICES = (ST_PP_ACTIVE, ST_PP_CANCELLED, ST_PP_CLEARED, ST_PP_COMPLETED, ST_PP_DENIED, ST_PP_PAID, ST_PP_PENDING, ST_PP_PROCESSED, ST_PP_REFUSED, ST_PP_REVERSED, ST_PP_REWARDED, ST_PP_UNCLAIMED, ST_PP_UNCLEARED) + PAYMENT_STATUS_CHOICES = (ST_PP_ACTIVE, ST_PP_CANCELLED, ST_PP_CLEARED, ST_PP_COMPLETED, ST_PP_DENIED, ST_PP_PAID, ST_PP_PENDING, ST_PP_PROCESSED, ST_PP_REFUSED, ST_PP_REFUNDED, ST_PP_REVERSED, ST_PP_REWARDED, ST_PP_UNCLAIMED, ST_PP_UNCLEARED) # AUTH_STATUS_CHOICES = "Completed Pending Voided".split() # ADDRESS_STATUS_CHOICES = "confirmed unconfirmed".split() # PAYER_STATUS_CHOICES = "verified / unverified".split() @@ -219,6 +220,9 @@ def is_recurring_payment(self): def is_recurring_cancel(self): return self.txn_type == "recurring_payment_profile_cancel" + + def is_refund(self): + return self.reason_code == "refund" def set_flag(self, info, code=None): """Sets a flag on the transaction and also sets a reason."""