From 1433a0221c18c5dcde3ef6044b9baee7bfa9156c Mon Sep 17 00:00:00 2001 From: Michele Mazzucchi Date: Sun, 18 Aug 2013 16:38:01 +0200 Subject: [PATCH 1/6] add IPN signal for failed payments in subscription --- paypal/standard/ipn/models.py | 6 ++++-- paypal/standard/ipn/signals.py | 5 ++++- paypal/standard/models.py | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/paypal/standard/ipn/models.py b/paypal/standard/ipn/models.py index 9495463..0236c47 100644 --- a/paypal/standard/ipn/models.py +++ b/paypal/standard/ipn/models.py @@ -42,7 +42,7 @@ def send_signals(self): recurring_skipped.send(sender=self) elif self.is_recurring_failed(): recurring_failed.send(sender=self) - # Subscription signals: + # Subscription signals: else: if self.is_subscription_cancellation(): subscription_cancel.send(sender=self) @@ -51,4 +51,6 @@ def send_signals(self): elif self.is_subscription_end_of_term(): subscription_eot.send(sender=self) elif self.is_subscription_modified(): - subscription_modify.send(sender=self) \ No newline at end of file + subscription_modify.send(sender=self) + elif self.is_subscription_failed(): + subscription_failed.send(sender=self) diff --git a/paypal/standard/ipn/signals.py b/paypal/standard/ipn/signals.py index c5aa14b..603e511 100644 --- a/paypal/standard/ipn/signals.py +++ b/paypal/standard/ipn/signals.py @@ -24,6 +24,9 @@ # Sent when a subscription is created. subscription_signup = Signal() +# Sent when a subscription's payment fails +subscription_failed = Signal() + # recurring_payment_profile_created recurring_create = Signal() @@ -34,4 +37,4 @@ recurring_skipped = Signal() -recurring_failed = Signal() \ No newline at end of file +recurring_failed = Signal() diff --git a/paypal/standard/models.py b/paypal/standard/models.py index c07e2fd..e3ab5e8 100644 --- a/paypal/standard/models.py +++ b/paypal/standard/models.py @@ -211,6 +211,9 @@ def is_subscription_modified(self): def is_subscription_signup(self): return self.txn_type == "subscr_signup" + def is_subscription_failed(self): + return self.txn_type == "subscr_failed" + def is_recurring_create(self): return self.txn_type == "recurring_payment_profile_created" From 304d07bbae42b42a6141694bfb0e9017675ac189 Mon Sep 17 00:00:00 2001 From: michele Date: Sat, 7 Dec 2013 02:02:34 +0100 Subject: [PATCH 2/6] Support locating Subscription ID in all forms coming from PayPal PayPal sends the Subscription ID as "recurring_payment_id" for transaction types = recurring_payment_*, and as "subscr_id" for transaction types = subscr_* This follows on the everlasting ambiguity PayPal uses to call recurring payments. Transaction examples: * subscription created, cancelled, payment -> txn_type = subscr_* * subscription suspended, suspended due to tryout -> txn_type = recurring_payment_* --- paypal/standard/ipn/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paypal/standard/ipn/admin.py b/paypal/standard/ipn/admin.py index 173c97c..ae50081 100644 --- a/paypal/standard/ipn/admin.py +++ b/paypal/standard/ipn/admin.py @@ -63,7 +63,7 @@ class PayPalIPNAdmin(admin.ModelAdmin): "__unicode__", "flag", "flag_info", "invoice", "custom", "payment_status", "created_at" ] - search_fields = ["txn_id", "recurring_payment_id"] + search_fields = ["txn_id", "recurring_payment_id", "subscr_id"] -admin.site.register(PayPalIPN, PayPalIPNAdmin) \ No newline at end of file +admin.site.register(PayPalIPN, PayPalIPNAdmin) From 7a7cee880b05c713764958b3b457a7e4b133d6a0 Mon Sep 17 00:00:00 2001 From: Michele Mazzucchi Date: Thu, 12 Dec 2013 06:58:35 +0100 Subject: [PATCH 3/6] add partial implementation of client to Name-Value Pair API --- paypal/standard/nvp/__init__.py | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 paypal/standard/nvp/__init__.py diff --git a/paypal/standard/nvp/__init__.py b/paypal/standard/nvp/__init__.py new file mode 100644 index 0000000..c2c3074 --- /dev/null +++ b/paypal/standard/nvp/__init__.py @@ -0,0 +1,64 @@ +# Partial implementation of the NameValuePair interface of PayPal + +# currently allows: +# * change status of subscription/recurring payment + +import urllib +import urllib2 +import urlparse + +from django.conf import settings + +base_params = { + 'USER': settings.PAYPAL_API_USERNAME, + 'PWD': settings.PAYPAL_API_PASSWORD, + 'SIGNATURE': settings.PAYPAL_API_SIGNATURE, + 'VERSION': '60.0' + } + + + +class PayPalResponse: + def __init__(self, raw_data): + self.raw = raw_data + self.parse() + + def parse(self): + self.params = urlparse.parse_qs(self.raw) + + +class RecurringSubscription: + def __init__(self, profileid): + self.profileid = profileid + + def __unicode__(self): + return str(self.profileid) + + def __str__(self): + return self.__unicode__() + + def updateStatus(self, new_status): + """Update the status of the recurring payment profile. + + new_status is in { Cancel, Suspend, Reactivate }.""" + + if new_status not in ('Cancel', 'Suspend', 'Reactivate'): + raise ValueError("Invalid value '%s' for new_status. Must be in Cancel, Suspend, Reactivate." % new_status) + + pars = { + 'METHOD': 'ManageRecurringPaymentsProfileStatus', + 'PROFILEID': self.profileid, + 'ACTION': new_status + } + + ok, params = self.issue_cmd(pars) + return ok + + def issue_cmd(self, parameters): + parameters = dict(base_params.items() + parameters.items()) + data = urllib.urlencode(parameters) + req = urllib2.Request(settings.PAYPAL_API_NVP_ENDPOINT, data, {}) + resp = urllib2.urlopen(req).read() + resp_params = urlparse.parse_qs(resp) + return all([x.lower() == 'success' for x in resp_params['ACK']]), resp_params + From d0d9a5216dac32bc45de7da6a70ba59c73b4e8b1 Mon Sep 17 00:00:00 2001 From: Michele Mazzucchi Date: Thu, 12 Dec 2013 07:03:58 +0100 Subject: [PATCH 4/6] prevent browser Security Warnings in img inclusion --- paypal/standard/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paypal/standard/conf.py b/paypal/standard/conf.py index 5c5fd45..979504f 100644 --- a/paypal/standard/conf.py +++ b/paypal/standard/conf.py @@ -15,7 +15,7 @@ class PayPalSettingsError(Exception): SANDBOX_POSTBACK_ENDPOINT = "https://www.sandbox.paypal.com/cgi-bin/webscr" # Images -IMAGE = getattr(settings, "PAYPAL_IMAGE", "http://images.paypal.com/images/x-click-but01.gif") +IMAGE = getattr(settings, "PAYPAL_IMAGE", "https://images.paypal.com/images/x-click-but01.gif") SUBSCRIPTION_IMAGE = getattr(settings, "PAYPAL_SUBSCRIPTION_IMAGE", "https://www.paypal.com/en_US/i/btn/btn_subscribeCC_LG.gif") DONATION_IMAGE = getattr(settings, "PAYPAL_DONATION_IMAGE", "https://www.paypal.com/en_US/i/btn/btn_donateCC_LG.gif") SANDBOX_IMAGE = getattr(settings, "PAYPAL_SANDBOX_IMAGE", "https://www.sandbox.paypal.com/en_US/i/btn/btn_buynowCC_LG.gif") From 16795a7db1e10f4c609e8f1471b668a04b79234a Mon Sep 17 00:00:00 2001 From: Michele Mazzucchi Date: Thu, 12 Dec 2013 07:09:20 +0100 Subject: [PATCH 5/6] add smart_render() to output for production/debug --- paypal/standard/forms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/paypal/standard/forms.py b/paypal/standard/forms.py index e9992a5..a026163 100644 --- a/paypal/standard/forms.py +++ b/paypal/standard/forms.py @@ -99,6 +99,10 @@ def __init__(self, button_type="buy", *args, **kwargs): super(PayPalPaymentsForm, self).__init__(*args, **kwargs) self.button_type = button_type + def smart_render(self): + """Render for PayPal if not PAYPAL_DEBUG, for PayPay Sandbox if PAYPAL_DEBUG.""" + return self.render() if not settings.PAYPAL_DEBUG else self.sandbox() + def render(self): return mark_safe(u"""
%s From ea3ddefed951852b38893e23a1d7a2e7f5df0ab3 Mon Sep 17 00:00:00 2001 From: Michele Mazzucchi Date: Thu, 12 Dec 2013 07:13:56 +0100 Subject: [PATCH 6/6] fix subscr_failed IPN events not being accepted Indicate the datetime format of all DateTimeFields so they can be correctly parsed. This change is made in particular for retry_at. When users create a subscription which later fails an instalment, paypal issues an IPN event with txn_type='subscr_failed' which indicates the date of the next attempt. This event failed parsing by django-paypal, and gets stored into the database with raw content and a flag. This fixes the issue. --- paypal/standard/forms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/paypal/standard/forms.py b/paypal/standard/forms.py index a026163..5912c01 100644 --- a/paypal/standard/forms.py +++ b/paypal/standard/forms.py @@ -221,3 +221,8 @@ class PayPalStandardBaseForm(forms.ModelForm): next_payment_date = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT) subscr_date = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT) subscr_effective = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT) + auction_closing_date = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT) + case_creation_date = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT) + # format not documented by PayPal, but empirically consistent + retry_at = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT) +