From a3758c85e6f4e62716cf21aae5d4a04a61d3c195 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 13:17:57 -0700 Subject: [PATCH 01/45] Add models and managers based on sample app --- authorizenet/customers/__init__.py | 0 authorizenet/customers/exceptions.py | 3 + authorizenet/customers/managers.py | 61 +++++++++++++++++++ authorizenet/customers/models.py | 88 ++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 authorizenet/customers/__init__.py create mode 100644 authorizenet/customers/exceptions.py create mode 100644 authorizenet/customers/managers.py create mode 100644 authorizenet/customers/models.py diff --git a/authorizenet/customers/__init__.py b/authorizenet/customers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authorizenet/customers/exceptions.py b/authorizenet/customers/exceptions.py new file mode 100644 index 0000000..26ecafe --- /dev/null +++ b/authorizenet/customers/exceptions.py @@ -0,0 +1,3 @@ +class BillingError(Exception): + + """Error due to Authorize.NET request""" diff --git a/authorizenet/customers/managers.py b/authorizenet/customers/managers.py new file mode 100644 index 0000000..c1779e3 --- /dev/null +++ b/authorizenet/customers/managers.py @@ -0,0 +1,61 @@ +from django.db import models + +from authorizenet.cim import add_profile, create_payment_profile + +from .exceptions import BillingError + + +class CustomerProfileManager(models.Manager): + + def create(self, **kwargs): + + """Create new Authorize.NET customer profile""" + from .models import CustomerPaymentProfile + + user = kwargs.get('user') + payment_data = kwargs.pop('payment_data', {}) + billing_data = kwargs.pop('billing_data', {}) + + # Create the customer profile with Authorize.NET CIM call + output = add_profile(user.pk, payment_data, billing_data) + if not output['response'].success: + raise BillingError("Error creating customer profile") + kwargs['profile_id'] = output['profile_id'] + + # Store customer profile data locally + instance = super(CustomerProfileManager, self).create(**kwargs) + + # Store customer payment profile data locally + for payment_profile_id in output['payment_profile_ids']: + CustomerPaymentProfile.objects.create( + customer_profile=instance, + payment_profile_id=payment_profile_id, + billing_data=billing_data, + payment_data=payment_data, + make_cim_request=False, + ) + + return instance + + +class CustomerPaymentProfileManager(models.Manager): + + def create(self, **kwargs): + """Create new Authorize.NET customer payment profile""" + customer_profile = kwargs.get('customer_profile') + payment_data = kwargs.pop('payment_data', {}) + billing_data = kwargs.pop('billing_data', {}) + if kwargs.pop('make_cim_request', True): + # Create the customer payment profile with Authorize.NET CIM call + response, payment_profile_id = create_payment_profile( + customer_profile.profile_id, payment_data, billing_data) + if not response.success: + raise BillingError() + kwargs['payment_profile_id'] = payment_profile_id + kwargs.update(billing_data) + kwargs.update(payment_data) + kwargs.pop('expiration_date') + kwargs.pop('card_code') + if 'card_number' in kwargs: + kwargs['card_number'] = "XXXX%s" % kwargs['card_number'][-4:] + return super(CustomerPaymentProfileManager, self).create(**kwargs) diff --git a/authorizenet/customers/models.py b/authorizenet/customers/models.py new file mode 100644 index 0000000..6aa9d8d --- /dev/null +++ b/authorizenet/customers/models.py @@ -0,0 +1,88 @@ +from django.db import models + +from authorizenet.cim import (get_profile, update_payment_profile, + delete_payment_profile) + +from .managers import CustomerProfileManager, CustomerPaymentProfileManager +from .exceptions import BillingError + + +class CustomerProfile(models.Model): + + """Authorize.NET customer profile""" + + user = models.ForeignKey('auth.User', unique=True) + profile_id = models.CharField(max_length=50) + + def sync(self): + """Overwrite local customer profile data with remote data""" + response, payment_profiles = get_profile(self.profile_id) + if not response.success: + raise BillingError("Error syncing remote customer profile") + for payment_profile in payment_profiles: + instance, created = CustomerPaymentProfile.objects.get_or_create( + customer_profile=self, + payment_profile_id=payment_profile['payment_profile_id'] + ) + instance.sync(payment_profile) + + objects = CustomerProfileManager() + + +class CustomerPaymentProfile(models.Model): + + """Authorize.NET customer payment profile""" + + customer_profile = models.ForeignKey('CustomerProfile', + related_name='payment_profiles') + first_name = models.CharField(max_length=50, blank=True) + last_name = models.CharField(max_length=50, blank=True) + company = models.CharField(max_length=50, blank=True) + address = models.CharField(max_length=60, blank=True) + city = models.CharField(max_length=40, blank=True) + state = models.CharField(max_length=40, blank=True) + zip = models.CharField(max_length=20, blank=True, verbose_name="ZIP") + country = models.CharField(max_length=60, blank=True) + phone = models.CharField(max_length=25, blank=True) + fax = models.CharField(max_length=25, blank=True) + payment_profile_id = models.CharField(max_length=50) + card_number = models.CharField(max_length=16, blank=True) + + def raw_data(self): + """Return data suitable for use in payment and billing forms""" + return model_to_dict(self) + + def sync(self, data): + """Overwrite local customer payment profile data with remote data""" + for k, v in data.get('billing', {}).items(): + setattr(self, k, v) + self.card_number = data.get('credit_card', {}).get('card_number', + self.card_number) + self.save() + + def delete(self): + """Delete the customer payment profile remotely and locally""" + delete_payment_profile(self.customer_profile.profile_id, + self.payment_profile_id) + + def update(self, payment_data, billing_data): + """Update the customer payment profile remotely and locally""" + response = update_payment_profile(self.customer_profile.profile_id, + self.payment_profile_id, + payment_data, billing_data) + if not response.success: + raise BillingError() + for k, v in billing_data.items(): + setattr(self, k, v) + for k, v in payment_data.items(): + # Do not store expiration date and mask credit card number + if k != 'expiration_date' and k != 'card_code': + if k == 'card_number': + v = "XXXX%s" % v[-4:] + setattr(self, k, v) + self.save() + + def __unicode__(self): + return self.card_number + + objects = CustomerPaymentProfileManager() From 0911c6db405c88e453318ee16fe8bf54b57b5733 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 13:18:49 -0700 Subject: [PATCH 02/45] Add generic view for creating payment profiles --- .../customers/customer_tests/__init__.py | 0 .../customers/customer_tests/models.py | 0 .../payment_profile_creation.html | 6 ++ .../customers/customer_tests/tests.py | 46 +++++++++++ authorizenet/customers/customer_tests/urls.py | 8 ++ .../customers/customer_tests/views.py | 25 ++++++ authorizenet/customers/views.py | 77 +++++++++++++++++++ runtests.py | 10 ++- 8 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 authorizenet/customers/customer_tests/__init__.py create mode 100644 authorizenet/customers/customer_tests/models.py create mode 100644 authorizenet/customers/customer_tests/templates/authorizenet/payment_profile_creation.html create mode 100644 authorizenet/customers/customer_tests/tests.py create mode 100644 authorizenet/customers/customer_tests/urls.py create mode 100644 authorizenet/customers/customer_tests/views.py create mode 100644 authorizenet/customers/views.py diff --git a/authorizenet/customers/customer_tests/__init__.py b/authorizenet/customers/customer_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/authorizenet/customers/customer_tests/models.py b/authorizenet/customers/customer_tests/models.py new file mode 100644 index 0000000..e69de29 diff --git a/authorizenet/customers/customer_tests/templates/authorizenet/payment_profile_creation.html b/authorizenet/customers/customer_tests/templates/authorizenet/payment_profile_creation.html new file mode 100644 index 0000000..fdf4d1f --- /dev/null +++ b/authorizenet/customers/customer_tests/templates/authorizenet/payment_profile_creation.html @@ -0,0 +1,6 @@ +
+ {{ payment_form }} + + {{ billing_form}} + +
diff --git a/authorizenet/customers/customer_tests/tests.py b/authorizenet/customers/customer_tests/tests.py new file mode 100644 index 0000000..22f83f3 --- /dev/null +++ b/authorizenet/customers/customer_tests/tests.py @@ -0,0 +1,46 @@ +from django.test import LiveServerTestCase +from django.contrib.auth.models import User + + +def create_user(username='', password=''): + user = User(username=username) + user.set_password(password) + user.save() + return user + + +class PaymentProfileCreationTests(LiveServerTestCase): + + def test_create_new_customer_get(self): + create_user(username='billy', password='password') + self.client.login(username='billy', password='password') + response = self.client.get('/customers/create') + self.assertNotIn("This field is required", response.content) + self.assertIn("Credit Card Number", response.content) + self.assertIn("City", response.content) + + def test_create_new_customer_post_error(self): + create_user(username='billy', password='password') + self.client.login(username='billy', password='password') + response = self.client.post('/customers/create') + self.assertIn("This field is required", response.content) + self.assertIn("Credit Card Number", response.content) + self.assertIn("City", response.content) + + def test_create_new_customer_post_success(self): + create_user(username='billy', password='password') + self.client.login(username='billy', password='password') + response = self.client.post('/customers/create', { + 'card_number': "5586086832001747", + 'expiration_date_0': "4", + 'expiration_date_1': "2020", + 'card_code': "123", + 'first_name': "Billy", + 'last_name': "Monaco", + 'address': "101 Broadway Ave", + 'city': "San Diego", + 'state': "CA", + 'country': "US", + 'zip': "92101", + }, follow=True) + self.assertIn("success", response.content) diff --git a/authorizenet/customers/customer_tests/urls.py b/authorizenet/customers/customer_tests/urls.py new file mode 100644 index 0000000..408e7f6 --- /dev/null +++ b/authorizenet/customers/customer_tests/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import url, patterns +from .views import CreateCustomerView, success_view + +urlpatterns = patterns( + '', + url(r"^customers/create$", CreateCustomerView.as_view()), + url(r"^success$", success_view), +) diff --git a/authorizenet/customers/customer_tests/views.py b/authorizenet/customers/customer_tests/views.py new file mode 100644 index 0000000..c32d969 --- /dev/null +++ b/authorizenet/customers/customer_tests/views.py @@ -0,0 +1,25 @@ +from django.http import HttpResponse +from ..views import PaymentProfileCreationView + + +from httmock import HTTMock +from authorizenet.tests.tests import cim_url_match, success_response + + +@cim_url_match +def create_customer_success(url, request): + return success_response.format('createCustomerProfileResponse') + + +class CreateCustomerView(PaymentProfileCreationView): + + def get_success_url(self): + return '/success' + + def forms_valid(self, *args, **kwargs): + with HTTMock(create_customer_success): + return super(CreateCustomerView, self).forms_valid(*args, **kwargs) + + +def success_view(request): + return HttpResponse("success") diff --git a/authorizenet/customers/views.py b/authorizenet/customers/views.py new file mode 100644 index 0000000..a001f2c --- /dev/null +++ b/authorizenet/customers/views.py @@ -0,0 +1,77 @@ +from django.http import HttpResponseRedirect +from django.views.generic import TemplateView + +from authorizenet.forms import CIMPaymentForm, BillingAddressForm +from .models import CustomerProfile, CustomerPaymentProfile + + +class PaymentProfileCreationView(TemplateView): + template_name = 'authorizenet/payment_profile_creation.html' + payment_form_class = CIMPaymentForm + billing_form_class = BillingAddressForm + + def post(self, *args, **kwargs): + payment_form = self.get_payment_form() + billing_form = self.get_billing_form() + if payment_form.is_valid() and billing_form.is_valid(): + return self.forms_valid(payment_form, billing_form) + else: + return self.forms_invalid(payment_form, billing_form) + + def get(self, *args, **kwargs): + return self.render_to_response(self.get_context_data( + payment_form=self.get_payment_form(), + billing_form=self.get_billing_form(), + )) + + def forms_valid(self, payment_form, billing_form): + """If the form is valid, save the payment profile and redirect""" + self.create_payment_profile( + payment_data=payment_form.cleaned_data, + billing_data=billing_form.cleaned_data, + ) + return HttpResponseRedirect(self.get_success_url()) + + def forms_invalid(self, payment_form, billing_form): + """If the form is invalid, show the page again""" + return self.render_to_response(self.get_context_data( + payment_form=payment_form, + billing_form=billing_form, + )) + + def get_payment_form(self): + """Returns an instance of the payment form""" + if self.request.method in ('POST', 'PUT'): + kwargs = {'data': self.request.POST} + else: + kwargs = {} + return self.payment_form_class(**kwargs) + + def get_billing_form(self): + """Returns an instance of the billing form""" + if self.request.method in ('POST', 'PUT'): + kwargs = {'data': self.request.POST} + else: + kwargs = {} + return self.billing_form_class(**kwargs) + + def create_payment_profile(self, **kwargs): + """Create and return payment profile""" + customer_profile = self.get_customer_profile() + if customer_profile: + return CustomerPaymentProfile.objects.create( + customer_profile=customer_profile, **kwargs) + else: + customer_profile = CustomerProfile.objects.create( + user=self.request.user, **kwargs) + return customer_profile.payment_profiles.get() + + def get_customer_profile(self): + """Return customer profile or ``None`` if none exists""" + try: + return CustomerProfile.objects.get(user=self.request.user) + except CustomerProfile.DoesNotExist: + return None + + def get_context_data(self, **kwargs): + return kwargs diff --git a/runtests.py b/runtests.py index d02ca06..97377df 100644 --- a/runtests.py +++ b/runtests.py @@ -11,14 +11,22 @@ AUTHNET_LOGIN_ID="loginid", AUTHNET_TRANSACTION_KEY="key", INSTALLED_APPS=( + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', 'authorizenet.tests', 'authorizenet', + 'authorizenet.customers.customer_tests', + 'authorizenet.customers', ), + ROOT_URLCONF='authorizenet.customers.customer_tests.urls', + STATIC_URL='/static/', DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}}, ) def runtests(): from django.test.simple import DjangoTestSuiteRunner - failures = DjangoTestSuiteRunner(failfast=False).run_tests(['tests']) + failures = DjangoTestSuiteRunner(failfast=False).run_tests([ + 'tests', 'customer_tests']) sys.exit(failures) From 9b1bd019bce6791a8782390f71577a3515acca2d Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 13:31:24 -0700 Subject: [PATCH 03/45] Fix URL patterns import --- authorizenet/customers/customer_tests/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizenet/customers/customer_tests/urls.py b/authorizenet/customers/customer_tests/urls.py index 408e7f6..3df0606 100644 --- a/authorizenet/customers/customer_tests/urls.py +++ b/authorizenet/customers/customer_tests/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import url, patterns +from django.conf.urls import url, patterns from .views import CreateCustomerView, success_view urlpatterns = patterns( From 467cd0fb59d2a9726d7f4e358a99f5924dad683b Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 13:43:45 -0700 Subject: [PATCH 04/45] Consolidate tests into single app --- .../customers/customer_tests/__init__.py | 0 .../customers/customer_tests/models.py | 0 .../customers/customer_tests/tests.py | 46 ---------- authorizenet/tests/mocks.py | 22 +++++ .../payment_profile_creation.html | 0 authorizenet/tests/tests.py | 89 +++++++++---------- .../customer_tests => tests}/urls.py | 0 authorizenet/tests/utils.py | 34 +++++++ .../customer_tests => tests}/views.py | 2 +- runtests.py | 6 +- 10 files changed, 100 insertions(+), 99 deletions(-) delete mode 100644 authorizenet/customers/customer_tests/__init__.py delete mode 100644 authorizenet/customers/customer_tests/models.py delete mode 100644 authorizenet/customers/customer_tests/tests.py create mode 100644 authorizenet/tests/mocks.py rename authorizenet/{customers/customer_tests => tests}/templates/authorizenet/payment_profile_creation.html (100%) rename authorizenet/{customers/customer_tests => tests}/urls.py (100%) create mode 100644 authorizenet/tests/utils.py rename authorizenet/{customers/customer_tests => tests}/views.py (90%) diff --git a/authorizenet/customers/customer_tests/__init__.py b/authorizenet/customers/customer_tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/authorizenet/customers/customer_tests/models.py b/authorizenet/customers/customer_tests/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/authorizenet/customers/customer_tests/tests.py b/authorizenet/customers/customer_tests/tests.py deleted file mode 100644 index 22f83f3..0000000 --- a/authorizenet/customers/customer_tests/tests.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.test import LiveServerTestCase -from django.contrib.auth.models import User - - -def create_user(username='', password=''): - user = User(username=username) - user.set_password(password) - user.save() - return user - - -class PaymentProfileCreationTests(LiveServerTestCase): - - def test_create_new_customer_get(self): - create_user(username='billy', password='password') - self.client.login(username='billy', password='password') - response = self.client.get('/customers/create') - self.assertNotIn("This field is required", response.content) - self.assertIn("Credit Card Number", response.content) - self.assertIn("City", response.content) - - def test_create_new_customer_post_error(self): - create_user(username='billy', password='password') - self.client.login(username='billy', password='password') - response = self.client.post('/customers/create') - self.assertIn("This field is required", response.content) - self.assertIn("Credit Card Number", response.content) - self.assertIn("City", response.content) - - def test_create_new_customer_post_success(self): - create_user(username='billy', password='password') - self.client.login(username='billy', password='password') - response = self.client.post('/customers/create', { - 'card_number': "5586086832001747", - 'expiration_date_0': "4", - 'expiration_date_1': "2020", - 'card_code': "123", - 'first_name': "Billy", - 'last_name': "Monaco", - 'address': "101 Broadway Ave", - 'city': "San Diego", - 'state': "CA", - 'country': "US", - 'zip': "92101", - }, follow=True) - self.assertIn("success", response.content) diff --git a/authorizenet/tests/mocks.py b/authorizenet/tests/mocks.py new file mode 100644 index 0000000..f3c1aa1 --- /dev/null +++ b/authorizenet/tests/mocks.py @@ -0,0 +1,22 @@ +from httmock import urlmatch + + +cim_url_match = urlmatch(scheme='https', netloc=r'^api\.authorize\.net$', + path=r'^/xml/v1/request\.api$') + + +success_response = ( + '' + '<{0}>' + '' + 'Ok' + 'I00001Successful.' + '' + '6666' + '' + '7777' + '' + '' + '' + '' +) diff --git a/authorizenet/customers/customer_tests/templates/authorizenet/payment_profile_creation.html b/authorizenet/tests/templates/authorizenet/payment_profile_creation.html similarity index 100% rename from authorizenet/customers/customer_tests/templates/authorizenet/payment_profile_creation.html rename to authorizenet/tests/templates/authorizenet/payment_profile_creation.html diff --git a/authorizenet/tests/tests.py b/authorizenet/tests/tests.py index 1706576..95e0d58 100644 --- a/authorizenet/tests/tests.py +++ b/authorizenet/tests/tests.py @@ -1,57 +1,50 @@ from datetime import datetime -from django.test import TestCase +from django.test import TestCase, LiveServerTestCase from xml.dom.minidom import parseString -from httmock import urlmatch, HTTMock +from httmock import HTTMock from authorizenet.cim import extract_form_data, extract_payment_form_data, \ add_profile - -cim_url_match = urlmatch(scheme='https', netloc=r'^api\.authorize\.net$', - path=r'^/xml/v1/request\.api$') - - -success_response = ( - '' - '<{0}>' - '' - 'Ok' - 'I00001Successful.' - '' - '6666' - '' - '7777' - '' - '' - '' - '' -) - - -def xml_to_dict(node): - """Recursively convert minidom XML node to dictionary""" - node_data = {} - if node.nodeType == node.TEXT_NODE: - node_data = node.data - elif node.nodeType not in (node.DOCUMENT_NODE, node.DOCUMENT_TYPE_NODE): - node_data.update(node.attributes.items()) - if node.nodeType not in (node.TEXT_NODE, node.DOCUMENT_TYPE_NODE): - for child in node.childNodes: - child_name, child_data = xml_to_dict(child) - if not child_data: - child_data = '' - if child_name not in node_data: - node_data[child_name] = child_data - else: - if not isinstance(node_data[child_name], list): - node_data[child_name] = [node_data[child_name]] - node_data[child_name].append(child_data) - if node_data.keys() == ['#text']: - node_data = node_data['#text'] - if node.nodeType == node.DOCUMENT_NODE: - return node_data - else: - return node.nodeName, node_data +from .utils import create_user, xml_to_dict +from .mocks import cim_url_match, success_response + + +class PaymentProfileCreationTests(LiveServerTestCase): + + def test_create_new_customer_get(self): + create_user(username='billy', password='password') + self.client.login(username='billy', password='password') + response = self.client.get('/customers/create') + self.assertNotIn("This field is required", response.content) + self.assertIn("Credit Card Number", response.content) + self.assertIn("City", response.content) + + def test_create_new_customer_post_error(self): + create_user(username='billy', password='password') + self.client.login(username='billy', password='password') + response = self.client.post('/customers/create') + self.assertIn("This field is required", response.content) + self.assertIn("Credit Card Number", response.content) + self.assertIn("City", response.content) + + def test_create_new_customer_post_success(self): + create_user(username='billy', password='password') + self.client.login(username='billy', password='password') + response = self.client.post('/customers/create', { + 'card_number': "5586086832001747", + 'expiration_date_0': "4", + 'expiration_date_1': "2020", + 'card_code': "123", + 'first_name': "Billy", + 'last_name': "Monaco", + 'address': "101 Broadway Ave", + 'city': "San Diego", + 'state': "CA", + 'country': "US", + 'zip': "92101", + }, follow=True) + self.assertIn("success", response.content) class ExtractFormDataTests(TestCase): diff --git a/authorizenet/customers/customer_tests/urls.py b/authorizenet/tests/urls.py similarity index 100% rename from authorizenet/customers/customer_tests/urls.py rename to authorizenet/tests/urls.py diff --git a/authorizenet/tests/utils.py b/authorizenet/tests/utils.py new file mode 100644 index 0000000..315cf91 --- /dev/null +++ b/authorizenet/tests/utils.py @@ -0,0 +1,34 @@ +from django.contrib.auth.models import User + + +def create_user(username='', password=''): + user = User(username=username) + user.set_password(password) + user.save() + return user + + +def xml_to_dict(node): + """Recursively convert minidom XML node to dictionary""" + node_data = {} + if node.nodeType == node.TEXT_NODE: + node_data = node.data + elif node.nodeType not in (node.DOCUMENT_NODE, node.DOCUMENT_TYPE_NODE): + node_data.update(node.attributes.items()) + if node.nodeType not in (node.TEXT_NODE, node.DOCUMENT_TYPE_NODE): + for child in node.childNodes: + child_name, child_data = xml_to_dict(child) + if not child_data: + child_data = '' + if child_name not in node_data: + node_data[child_name] = child_data + else: + if not isinstance(node_data[child_name], list): + node_data[child_name] = [node_data[child_name]] + node_data[child_name].append(child_data) + if node_data.keys() == ['#text']: + node_data = node_data['#text'] + if node.nodeType == node.DOCUMENT_NODE: + return node_data + else: + return node.nodeName, node_data diff --git a/authorizenet/customers/customer_tests/views.py b/authorizenet/tests/views.py similarity index 90% rename from authorizenet/customers/customer_tests/views.py rename to authorizenet/tests/views.py index c32d969..6e93ba2 100644 --- a/authorizenet/customers/customer_tests/views.py +++ b/authorizenet/tests/views.py @@ -1,5 +1,5 @@ from django.http import HttpResponse -from ..views import PaymentProfileCreationView +from authorizenet.customers.views import PaymentProfileCreationView from httmock import HTTMock diff --git a/runtests.py b/runtests.py index 97377df..3783bc3 100644 --- a/runtests.py +++ b/runtests.py @@ -16,10 +16,9 @@ 'django.contrib.sessions', 'authorizenet.tests', 'authorizenet', - 'authorizenet.customers.customer_tests', 'authorizenet.customers', ), - ROOT_URLCONF='authorizenet.customers.customer_tests.urls', + ROOT_URLCONF='authorizenet.tests.urls', STATIC_URL='/static/', DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}}, ) @@ -27,6 +26,5 @@ def runtests(): from django.test.simple import DjangoTestSuiteRunner - failures = DjangoTestSuiteRunner(failfast=False).run_tests([ - 'tests', 'customer_tests']) + failures = DjangoTestSuiteRunner(failfast=False).run_tests(['tests']) sys.exit(failures) From 858654830b2d7652eb929abf9808d98c161da943 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 13:46:59 -0700 Subject: [PATCH 05/45] Move authorizenet.tests app to tests --- .coveragerc | 2 +- runtests.py | 4 ++-- {authorizenet/tests => tests}/__init__.py | 0 {authorizenet/tests => tests}/mocks.py | 0 {authorizenet/tests => tests}/models.py | 0 .../templates/authorizenet/payment_profile_creation.html | 0 {authorizenet/tests => tests}/tests.py | 0 {authorizenet/tests => tests}/urls.py | 0 {authorizenet/tests => tests}/utils.py | 0 {authorizenet/tests => tests}/views.py | 2 +- 10 files changed, 4 insertions(+), 4 deletions(-) rename {authorizenet/tests => tests}/__init__.py (100%) rename {authorizenet/tests => tests}/mocks.py (100%) rename {authorizenet/tests => tests}/models.py (100%) rename {authorizenet/tests => tests}/templates/authorizenet/payment_profile_creation.html (100%) rename {authorizenet/tests => tests}/tests.py (100%) rename {authorizenet/tests => tests}/urls.py (100%) rename {authorizenet/tests => tests}/utils.py (100%) rename {authorizenet/tests => tests}/views.py (89%) diff --git a/.coveragerc b/.coveragerc index 92747ee..619746d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,4 @@ [run] include = authorizenet/* -omit = authorizenet/tests/* +omit = tests/* branch = 1 diff --git a/runtests.py b/runtests.py index 3783bc3..face91a 100644 --- a/runtests.py +++ b/runtests.py @@ -14,11 +14,11 @@ 'django.contrib.contenttypes', 'django.contrib.auth', 'django.contrib.sessions', - 'authorizenet.tests', + 'tests', 'authorizenet', 'authorizenet.customers', ), - ROOT_URLCONF='authorizenet.tests.urls', + ROOT_URLCONF='tests.urls', STATIC_URL='/static/', DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}}, ) diff --git a/authorizenet/tests/__init__.py b/tests/__init__.py similarity index 100% rename from authorizenet/tests/__init__.py rename to tests/__init__.py diff --git a/authorizenet/tests/mocks.py b/tests/mocks.py similarity index 100% rename from authorizenet/tests/mocks.py rename to tests/mocks.py diff --git a/authorizenet/tests/models.py b/tests/models.py similarity index 100% rename from authorizenet/tests/models.py rename to tests/models.py diff --git a/authorizenet/tests/templates/authorizenet/payment_profile_creation.html b/tests/templates/authorizenet/payment_profile_creation.html similarity index 100% rename from authorizenet/tests/templates/authorizenet/payment_profile_creation.html rename to tests/templates/authorizenet/payment_profile_creation.html diff --git a/authorizenet/tests/tests.py b/tests/tests.py similarity index 100% rename from authorizenet/tests/tests.py rename to tests/tests.py diff --git a/authorizenet/tests/urls.py b/tests/urls.py similarity index 100% rename from authorizenet/tests/urls.py rename to tests/urls.py diff --git a/authorizenet/tests/utils.py b/tests/utils.py similarity index 100% rename from authorizenet/tests/utils.py rename to tests/utils.py diff --git a/authorizenet/tests/views.py b/tests/views.py similarity index 89% rename from authorizenet/tests/views.py rename to tests/views.py index 6e93ba2..6dab17a 100644 --- a/authorizenet/tests/views.py +++ b/tests/views.py @@ -3,7 +3,7 @@ from httmock import HTTMock -from authorizenet.tests.tests import cim_url_match, success_response +from .mocks import cim_url_match, success_response @cim_url_match From ded3c6699e7d06dddf47d5b7a9c3054b7e44325e Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 14:52:53 -0700 Subject: [PATCH 06/45] Greatly simplify payment profile creation view --- authorizenet/customers/views.py | 57 +++---------------- authorizenet/forms.py | 17 ++++++ .../payment_profile_creation.html | 4 +- tests/views.py | 4 +- 4 files changed, 29 insertions(+), 53 deletions(-) diff --git a/authorizenet/customers/views.py b/authorizenet/customers/views.py index a001f2c..ea8eab7 100644 --- a/authorizenet/customers/views.py +++ b/authorizenet/customers/views.py @@ -1,59 +1,20 @@ -from django.http import HttpResponseRedirect -from django.views.generic import TemplateView +from django.views.generic.edit import FormView -from authorizenet.forms import CIMPaymentForm, BillingAddressForm +from authorizenet.forms import CustomerPaymentForm from .models import CustomerProfile, CustomerPaymentProfile -class PaymentProfileCreationView(TemplateView): +class PaymentProfileCreationView(FormView): template_name = 'authorizenet/payment_profile_creation.html' - payment_form_class = CIMPaymentForm - billing_form_class = BillingAddressForm + form_class = CustomerPaymentForm - def post(self, *args, **kwargs): - payment_form = self.get_payment_form() - billing_form = self.get_billing_form() - if payment_form.is_valid() and billing_form.is_valid(): - return self.forms_valid(payment_form, billing_form) - else: - return self.forms_invalid(payment_form, billing_form) - - def get(self, *args, **kwargs): - return self.render_to_response(self.get_context_data( - payment_form=self.get_payment_form(), - billing_form=self.get_billing_form(), - )) - - def forms_valid(self, payment_form, billing_form): - """If the form is valid, save the payment profile and redirect""" + def form_valid(self, form): + """If the form is valid, save the payment profile""" self.create_payment_profile( - payment_data=payment_form.cleaned_data, - billing_data=billing_form.cleaned_data, + payment_data=form.payment_data, + billing_data=form.billing_data, ) - return HttpResponseRedirect(self.get_success_url()) - - def forms_invalid(self, payment_form, billing_form): - """If the form is invalid, show the page again""" - return self.render_to_response(self.get_context_data( - payment_form=payment_form, - billing_form=billing_form, - )) - - def get_payment_form(self): - """Returns an instance of the payment form""" - if self.request.method in ('POST', 'PUT'): - kwargs = {'data': self.request.POST} - else: - kwargs = {} - return self.payment_form_class(**kwargs) - - def get_billing_form(self): - """Returns an instance of the billing form""" - if self.request.method in ('POST', 'PUT'): - kwargs = {'data': self.request.POST} - else: - kwargs = {} - return self.billing_form_class(**kwargs) + return super(PaymentProfileCreationView, self).form_valid(form) def create_payment_profile(self, **kwargs): """Create and return payment profile""" diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 648bb82..b559404 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -91,6 +91,23 @@ class CIMPaymentForm(forms.Form): card_code = CreditCardCVV2Field(label="Card Security Code") +def create_customer_form(payment_form, address_form=None, shipping_form=None): + """Create new form class from given parent form classes""" + def fields_from_form(form): + return lambda self: dict((k, v) for (k, v) in self.cleaned_data.items() + if k in form.base_fields) + bases = tuple(c for c in (payment_form, address_form, shipping_form) + if c is not None) + return type('CustomerPaymentForm', bases, { + 'billing_data': property(fields_from_form(address_form)), + 'payment_data': property(fields_from_form(payment_form)), + 'shipping_data': property(fields_from_form(shipping_form)), + }) + + +CustomerPaymentForm = create_customer_form(CIMPaymentForm, BillingAddressForm) + + class HostedCIMProfileForm(forms.Form): token = forms.CharField(widget=forms.HiddenInput) def __init__(self, token, *args, **kwargs): diff --git a/tests/templates/authorizenet/payment_profile_creation.html b/tests/templates/authorizenet/payment_profile_creation.html index fdf4d1f..c592814 100644 --- a/tests/templates/authorizenet/payment_profile_creation.html +++ b/tests/templates/authorizenet/payment_profile_creation.html @@ -1,6 +1,4 @@
- {{ payment_form }} - - {{ billing_form}} + {{ form }}
diff --git a/tests/views.py b/tests/views.py index 6dab17a..bf00281 100644 --- a/tests/views.py +++ b/tests/views.py @@ -16,9 +16,9 @@ class CreateCustomerView(PaymentProfileCreationView): def get_success_url(self): return '/success' - def forms_valid(self, *args, **kwargs): + def form_valid(self, *args, **kwargs): with HTTMock(create_customer_success): - return super(CreateCustomerView, self).forms_valid(*args, **kwargs) + return super(CreateCustomerView, self).form_valid(*args, **kwargs) def success_view(request): From 706533f2ac0c1f464ac84cc14b38fa3e4fd4158e Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 14:55:55 -0700 Subject: [PATCH 07/45] Simplify generic view further --- authorizenet/customers/views.py | 4 ++-- authorizenet/forms.py | 17 ++--------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/authorizenet/customers/views.py b/authorizenet/customers/views.py index ea8eab7..b8ab5b2 100644 --- a/authorizenet/customers/views.py +++ b/authorizenet/customers/views.py @@ -11,8 +11,8 @@ class PaymentProfileCreationView(FormView): def form_valid(self, form): """If the form is valid, save the payment profile""" self.create_payment_profile( - payment_data=form.payment_data, - billing_data=form.billing_data, + payment_data=form.cleaned_data, + billing_data=form.cleaned_data, ) return super(PaymentProfileCreationView, self).form_valid(form) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index b559404..8dbcf2d 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -91,21 +91,8 @@ class CIMPaymentForm(forms.Form): card_code = CreditCardCVV2Field(label="Card Security Code") -def create_customer_form(payment_form, address_form=None, shipping_form=None): - """Create new form class from given parent form classes""" - def fields_from_form(form): - return lambda self: dict((k, v) for (k, v) in self.cleaned_data.items() - if k in form.base_fields) - bases = tuple(c for c in (payment_form, address_form, shipping_form) - if c is not None) - return type('CustomerPaymentForm', bases, { - 'billing_data': property(fields_from_form(address_form)), - 'payment_data': property(fields_from_form(payment_form)), - 'shipping_data': property(fields_from_form(shipping_form)), - }) - - -CustomerPaymentForm = create_customer_form(CIMPaymentForm, BillingAddressForm) +class CustomerPaymentForm(CIMPaymentForm, BillingAddressForm): + """Base customer payment form without shipping address""" class HostedCIMProfileForm(forms.Form): From 187ae59d7ea80dfe44d48403bc2f5c5cf6b69da0 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 14:56:28 -0700 Subject: [PATCH 08/45] Verify correct request data sent for generic view --- tests/tests.py | 103 +++++++++++++++++++++++++++---------------------- tests/utils.py | 3 +- tests/views.py | 13 ------- 3 files changed, 59 insertions(+), 60 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 95e0d58..9fb0ef0 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -10,6 +10,39 @@ from .mocks import cim_url_match, success_response +create_profile_success_data = { + 'createCustomerProfileRequest': { + 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', + 'profile': { + 'merchantCustomerId': '42', + 'paymentProfiles': { + 'billTo': { + 'firstName': 'Danielle', + 'lastName': 'Thompson', + 'company': '', + 'address': '101 Broadway Avenue', + 'city': 'San Diego', + 'state': 'CA', + 'zip': '92101', + 'country': 'US' + }, + 'payment': { + 'creditCard': { + 'cardCode': '123', + 'cardNumber': "5586086832001747", + 'expirationDate': '2020-05' + } + } + } + }, + 'merchantAuthentication': { + 'transactionKey': 'key', + 'name': 'loginid' + }, + } +} + + class PaymentProfileCreationTests(LiveServerTestCase): def test_create_new_customer_get(self): @@ -29,21 +62,29 @@ def test_create_new_customer_post_error(self): self.assertIn("City", response.content) def test_create_new_customer_post_success(self): - create_user(username='billy', password='password') + @cim_url_match + def create_customer_success(url, request): + request_xml = parseString(request.body) + self.assertEqual(xml_to_dict(request_xml), + create_profile_success_data) + return success_response.format('createCustomerProfileResponse') + create_user(id=42, username='billy', password='password') self.client.login(username='billy', password='password') - response = self.client.post('/customers/create', { - 'card_number': "5586086832001747", - 'expiration_date_0': "4", - 'expiration_date_1': "2020", - 'card_code': "123", - 'first_name': "Billy", - 'last_name': "Monaco", - 'address': "101 Broadway Ave", - 'city': "San Diego", - 'state': "CA", - 'country': "US", - 'zip': "92101", - }, follow=True) + self.maxDiff = None + with HTTMock(create_customer_success): + response = self.client.post('/customers/create', { + 'card_number': "5586086832001747", + 'expiration_date_0': "5", + 'expiration_date_1': "2020", + 'card_code': "123", + 'first_name': "Danielle", + 'last_name': "Thompson", + 'address': "101 Broadway Avenue", + 'city': "San Diego", + 'state': "CA", + 'country': "US", + 'zip': "92101", + }, follow=True) self.assertIn("success", response.content) @@ -74,7 +115,7 @@ class AddProfileTests(TestCase): def setUp(self): self.payment_form_data = { - 'card_number': "1111222233334444", + 'card_number': "5586086832001747", 'expiration_date': datetime(2020, 5, 1), 'card_code': "123", } @@ -88,37 +129,7 @@ def setUp(self): 'country': "US", 'zip': "92101", } - self.request_data = { - 'createCustomerProfileRequest': { - 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', - 'profile': { - 'merchantCustomerId': '42', - 'paymentProfiles': { - 'billTo': { - 'firstName': 'Danielle', - 'lastName': 'Thompson', - 'company': '', - 'address': '101 Broadway Avenue', - 'city': 'San Diego', - 'state': 'CA', - 'zip': '92101', - 'country': 'US' - }, - 'payment': { - 'creditCard': { - 'cardCode': '123', - 'cardNumber': '1111222233334444', - 'expirationDate': '2020-05' - } - } - } - }, - 'merchantAuthentication': { - 'transactionKey': 'key', - 'name': 'loginid' - }, - } - } + self.request_data = create_profile_success_data def test_add_profile_minimal(self): """Success test with minimal complexity""" diff --git a/tests/utils.py b/tests/utils.py index 315cf91..e4079eb 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,9 @@ from django.contrib.auth.models import User -def create_user(username='', password=''): +def create_user(id=None, username='', password=''): user = User(username=username) + user.id = id user.set_password(password) user.save() return user diff --git a/tests/views.py b/tests/views.py index bf00281..873f022 100644 --- a/tests/views.py +++ b/tests/views.py @@ -2,24 +2,11 @@ from authorizenet.customers.views import PaymentProfileCreationView -from httmock import HTTMock -from .mocks import cim_url_match, success_response - - -@cim_url_match -def create_customer_success(url, request): - return success_response.format('createCustomerProfileResponse') - - class CreateCustomerView(PaymentProfileCreationView): def get_success_url(self): return '/success' - def form_valid(self, *args, **kwargs): - with HTTMock(create_customer_success): - return super(CreateCustomerView, self).form_valid(*args, **kwargs) - def success_view(request): return HttpResponse("success") From 8ee211a49194b289382a7c8db142d5d9afa5074d Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 15:01:16 -0700 Subject: [PATCH 09/45] Fixup code style in generic view --- authorizenet/customers/views.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/authorizenet/customers/views.py b/authorizenet/customers/views.py index b8ab5b2..4761d2e 100644 --- a/authorizenet/customers/views.py +++ b/authorizenet/customers/views.py @@ -10,10 +10,8 @@ class PaymentProfileCreationView(FormView): def form_valid(self, form): """If the form is valid, save the payment profile""" - self.create_payment_profile( - payment_data=form.cleaned_data, - billing_data=form.cleaned_data, - ) + data = form.cleaned_data + self.create_payment_profile(payment_data=data, billing_data=data) return super(PaymentProfileCreationView, self).form_valid(form) def create_payment_profile(self, **kwargs): @@ -33,6 +31,3 @@ def get_customer_profile(self): return CustomerProfile.objects.get(user=self.request.user) except CustomerProfile.DoesNotExist: return None - - def get_context_data(self, **kwargs): - return kwargs From a06f00431acbc05d94a868465780914e1a849bbe Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 15:14:25 -0700 Subject: [PATCH 10/45] Rename payment profile creation template --- authorizenet/customers/views.py | 2 +- ...ayment_profile_creation.html => create_payment_profile.html} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/templates/authorizenet/{payment_profile_creation.html => create_payment_profile.html} (100%) diff --git a/authorizenet/customers/views.py b/authorizenet/customers/views.py index 4761d2e..fe68147 100644 --- a/authorizenet/customers/views.py +++ b/authorizenet/customers/views.py @@ -5,7 +5,7 @@ class PaymentProfileCreationView(FormView): - template_name = 'authorizenet/payment_profile_creation.html' + template_name = 'authorizenet/create_payment_profile.html' form_class = CustomerPaymentForm def form_valid(self, form): diff --git a/tests/templates/authorizenet/payment_profile_creation.html b/tests/templates/authorizenet/create_payment_profile.html similarity index 100% rename from tests/templates/authorizenet/payment_profile_creation.html rename to tests/templates/authorizenet/create_payment_profile.html From 9fc862deac193e867431e91a8fe95f30c11f2bb0 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 15:25:38 -0700 Subject: [PATCH 11/45] Merge authorizenet.customers into authorizenet --- authorizenet/cim.py | 3 +- authorizenet/customers/__init__.py | 0 authorizenet/customers/models.py | 88 ---------------------- authorizenet/customers/views.py | 33 -------- authorizenet/{customers => }/exceptions.py | 0 authorizenet/{customers => }/managers.py | 0 authorizenet/models.py | 87 +++++++++++++++++++++ authorizenet/views.py | 33 +++++++- runtests.py | 1 - tests/views.py | 2 +- 10 files changed, 122 insertions(+), 125 deletions(-) delete mode 100644 authorizenet/customers/__init__.py delete mode 100644 authorizenet/customers/models.py delete mode 100644 authorizenet/customers/views.py rename authorizenet/{customers => }/exceptions.py (100%) rename authorizenet/{customers => }/managers.py (100%) diff --git a/authorizenet/cim.py b/authorizenet/cim.py index 0da2a65..77c74d4 100644 --- a/authorizenet/cim.py +++ b/authorizenet/cim.py @@ -6,7 +6,6 @@ import requests from authorizenet import AUTHNET_CIM_URL, AUTHNET_TEST_CIM_URL -from authorizenet.models import CIMResponse, Response from authorizenet.signals import customer_was_created, customer_was_flagged, \ payment_was_successful, payment_was_flagged @@ -317,6 +316,7 @@ def get_text_node(self, node_name, text): return node def create_response_object(self): + from authorizenet.models import CIMResponse return CIMResponse.objects.create(result=self.result, result_code=self.resultCode, result_text=self.resultText) @@ -755,6 +755,7 @@ def add_extra_options(self): self.root.appendChild(extra_options_node) def create_response_object(self): + from authorizenet.models import CIMResponse, Response try: response = Response.objects.create_from_list( self.transaction_result) diff --git a/authorizenet/customers/__init__.py b/authorizenet/customers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/authorizenet/customers/models.py b/authorizenet/customers/models.py deleted file mode 100644 index 6aa9d8d..0000000 --- a/authorizenet/customers/models.py +++ /dev/null @@ -1,88 +0,0 @@ -from django.db import models - -from authorizenet.cim import (get_profile, update_payment_profile, - delete_payment_profile) - -from .managers import CustomerProfileManager, CustomerPaymentProfileManager -from .exceptions import BillingError - - -class CustomerProfile(models.Model): - - """Authorize.NET customer profile""" - - user = models.ForeignKey('auth.User', unique=True) - profile_id = models.CharField(max_length=50) - - def sync(self): - """Overwrite local customer profile data with remote data""" - response, payment_profiles = get_profile(self.profile_id) - if not response.success: - raise BillingError("Error syncing remote customer profile") - for payment_profile in payment_profiles: - instance, created = CustomerPaymentProfile.objects.get_or_create( - customer_profile=self, - payment_profile_id=payment_profile['payment_profile_id'] - ) - instance.sync(payment_profile) - - objects = CustomerProfileManager() - - -class CustomerPaymentProfile(models.Model): - - """Authorize.NET customer payment profile""" - - customer_profile = models.ForeignKey('CustomerProfile', - related_name='payment_profiles') - first_name = models.CharField(max_length=50, blank=True) - last_name = models.CharField(max_length=50, blank=True) - company = models.CharField(max_length=50, blank=True) - address = models.CharField(max_length=60, blank=True) - city = models.CharField(max_length=40, blank=True) - state = models.CharField(max_length=40, blank=True) - zip = models.CharField(max_length=20, blank=True, verbose_name="ZIP") - country = models.CharField(max_length=60, blank=True) - phone = models.CharField(max_length=25, blank=True) - fax = models.CharField(max_length=25, blank=True) - payment_profile_id = models.CharField(max_length=50) - card_number = models.CharField(max_length=16, blank=True) - - def raw_data(self): - """Return data suitable for use in payment and billing forms""" - return model_to_dict(self) - - def sync(self, data): - """Overwrite local customer payment profile data with remote data""" - for k, v in data.get('billing', {}).items(): - setattr(self, k, v) - self.card_number = data.get('credit_card', {}).get('card_number', - self.card_number) - self.save() - - def delete(self): - """Delete the customer payment profile remotely and locally""" - delete_payment_profile(self.customer_profile.profile_id, - self.payment_profile_id) - - def update(self, payment_data, billing_data): - """Update the customer payment profile remotely and locally""" - response = update_payment_profile(self.customer_profile.profile_id, - self.payment_profile_id, - payment_data, billing_data) - if not response.success: - raise BillingError() - for k, v in billing_data.items(): - setattr(self, k, v) - for k, v in payment_data.items(): - # Do not store expiration date and mask credit card number - if k != 'expiration_date' and k != 'card_code': - if k == 'card_number': - v = "XXXX%s" % v[-4:] - setattr(self, k, v) - self.save() - - def __unicode__(self): - return self.card_number - - objects = CustomerPaymentProfileManager() diff --git a/authorizenet/customers/views.py b/authorizenet/customers/views.py deleted file mode 100644 index fe68147..0000000 --- a/authorizenet/customers/views.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.views.generic.edit import FormView - -from authorizenet.forms import CustomerPaymentForm -from .models import CustomerProfile, CustomerPaymentProfile - - -class PaymentProfileCreationView(FormView): - template_name = 'authorizenet/create_payment_profile.html' - form_class = CustomerPaymentForm - - def form_valid(self, form): - """If the form is valid, save the payment profile""" - data = form.cleaned_data - self.create_payment_profile(payment_data=data, billing_data=data) - return super(PaymentProfileCreationView, self).form_valid(form) - - def create_payment_profile(self, **kwargs): - """Create and return payment profile""" - customer_profile = self.get_customer_profile() - if customer_profile: - return CustomerPaymentProfile.objects.create( - customer_profile=customer_profile, **kwargs) - else: - customer_profile = CustomerProfile.objects.create( - user=self.request.user, **kwargs) - return customer_profile.payment_profiles.get() - - def get_customer_profile(self): - """Return customer profile or ``None`` if none exists""" - try: - return CustomerProfile.objects.get(user=self.request.user) - except CustomerProfile.DoesNotExist: - return None diff --git a/authorizenet/customers/exceptions.py b/authorizenet/exceptions.py similarity index 100% rename from authorizenet/customers/exceptions.py rename to authorizenet/exceptions.py diff --git a/authorizenet/customers/managers.py b/authorizenet/managers.py similarity index 100% rename from authorizenet/customers/managers.py rename to authorizenet/managers.py diff --git a/authorizenet/models.py b/authorizenet/models.py index c1cfe0b..7cba1bb 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -1,5 +1,11 @@ from django.db import models +from .cim import get_profile, update_payment_profile, delete_payment_profile + +from .managers import CustomerProfileManager, CustomerPaymentProfileManager +from .exceptions import BillingError + + RESPONSE_CHOICES = ( ('1', 'Approved'), ('2', 'Declined'), @@ -194,3 +200,84 @@ class CIMResponse(models.Model): @property def success(self): return self.result == 'Ok' + + +class CustomerProfile(models.Model): + + """Authorize.NET customer profile""" + + user = models.ForeignKey('auth.User', unique=True) + profile_id = models.CharField(max_length=50) + + def sync(self): + """Overwrite local customer profile data with remote data""" + response, payment_profiles = get_profile(self.profile_id) + if not response.success: + raise BillingError("Error syncing remote customer profile") + for payment_profile in payment_profiles: + instance, created = CustomerPaymentProfile.objects.get_or_create( + customer_profile=self, + payment_profile_id=payment_profile['payment_profile_id'] + ) + instance.sync(payment_profile) + + objects = CustomerProfileManager() + + +class CustomerPaymentProfile(models.Model): + + """Authorize.NET customer payment profile""" + + customer_profile = models.ForeignKey('CustomerProfile', + related_name='payment_profiles') + first_name = models.CharField(max_length=50, blank=True) + last_name = models.CharField(max_length=50, blank=True) + company = models.CharField(max_length=50, blank=True) + address = models.CharField(max_length=60, blank=True) + city = models.CharField(max_length=40, blank=True) + state = models.CharField(max_length=40, blank=True) + zip = models.CharField(max_length=20, blank=True, verbose_name="ZIP") + country = models.CharField(max_length=60, blank=True) + phone = models.CharField(max_length=25, blank=True) + fax = models.CharField(max_length=25, blank=True) + payment_profile_id = models.CharField(max_length=50) + card_number = models.CharField(max_length=16, blank=True) + + def raw_data(self): + """Return data suitable for use in payment and billing forms""" + return model_to_dict(self) + + def sync(self, data): + """Overwrite local customer payment profile data with remote data""" + for k, v in data.get('billing', {}).items(): + setattr(self, k, v) + self.card_number = data.get('credit_card', {}).get('card_number', + self.card_number) + self.save() + + def delete(self): + """Delete the customer payment profile remotely and locally""" + delete_payment_profile(self.customer_profile.profile_id, + self.payment_profile_id) + + def update(self, payment_data, billing_data): + """Update the customer payment profile remotely and locally""" + response = update_payment_profile(self.customer_profile.profile_id, + self.payment_profile_id, + payment_data, billing_data) + if not response.success: + raise BillingError() + for k, v in billing_data.items(): + setattr(self, k, v) + for k, v in payment_data.items(): + # Do not store expiration date and mask credit card number + if k != 'expiration_date' and k != 'card_code': + if k == 'card_number': + v = "XXXX%s" % v[-4:] + setattr(self, k, v) + self.save() + + def __unicode__(self): + return self.card_number + + objects = CustomerPaymentProfileManager() diff --git a/authorizenet/views.py b/authorizenet/views.py index f030578..17981ad 100644 --- a/authorizenet/views.py +++ b/authorizenet/views.py @@ -6,8 +6,10 @@ from django.conf import settings from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt +from django.views.generic.edit import FormView -from authorizenet.forms import AIMPaymentForm, BillingAddressForm +from authorizenet.forms import AIMPaymentForm, BillingAddressForm, CustomerPaymentForm +from authorizenet.models import CustomerProfile, CustomerPaymentProfile from authorizenet.models import Response from authorizenet.signals import payment_was_successful, payment_was_flagged from authorizenet.utils import process_payment, combine_form_data @@ -121,3 +123,32 @@ def validate_payment_form(self): self.payment_template, self.context ) + + +class PaymentProfileCreationView(FormView): + template_name = 'authorizenet/create_payment_profile.html' + form_class = CustomerPaymentForm + + def form_valid(self, form): + """If the form is valid, save the payment profile""" + data = form.cleaned_data + self.create_payment_profile(payment_data=data, billing_data=data) + return super(PaymentProfileCreationView, self).form_valid(form) + + def create_payment_profile(self, **kwargs): + """Create and return payment profile""" + customer_profile = self.get_customer_profile() + if customer_profile: + return CustomerPaymentProfile.objects.create( + customer_profile=customer_profile, **kwargs) + else: + customer_profile = CustomerProfile.objects.create( + user=self.request.user, **kwargs) + return customer_profile.payment_profiles.get() + + def get_customer_profile(self): + """Return customer profile or ``None`` if none exists""" + try: + return CustomerProfile.objects.get(user=self.request.user) + except CustomerProfile.DoesNotExist: + return None diff --git a/runtests.py b/runtests.py index face91a..89cb44f 100644 --- a/runtests.py +++ b/runtests.py @@ -16,7 +16,6 @@ 'django.contrib.sessions', 'tests', 'authorizenet', - 'authorizenet.customers', ), ROOT_URLCONF='tests.urls', STATIC_URL='/static/', diff --git a/tests/views.py b/tests/views.py index 873f022..fb98c77 100644 --- a/tests/views.py +++ b/tests/views.py @@ -1,5 +1,5 @@ from django.http import HttpResponse -from authorizenet.customers.views import PaymentProfileCreationView +from authorizenet.views import PaymentProfileCreationView class CreateCustomerView(PaymentProfileCreationView): From d46338c02c483e6e4cf20af7b77f99c9907c4648 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 16:03:42 -0700 Subject: [PATCH 12/45] Move payment profile creation logic into form --- authorizenet/forms.py | 34 ++++++++++++++++++++++++++++++++++ authorizenet/views.py | 31 ++++++------------------------- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 8dbcf2d..6e0357f 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -2,6 +2,7 @@ from django.conf import settings from authorizenet.fields import CreditCardField, CreditCardExpiryField, \ CreditCardCVV2Field, CountryField +from authorizenet.models import CustomerProfile, CustomerPaymentProfile class SIMPaymentForm(forms.Form): @@ -92,8 +93,41 @@ class CIMPaymentForm(forms.Form): class CustomerPaymentForm(CIMPaymentForm, BillingAddressForm): + """Base customer payment form without shipping address""" + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop('instance', None) + self.user = kwargs.pop('user', None) + return super(CustomerPaymentForm, self).__init__(*args, **kwargs) + + def create_payment_profile(self, **kwargs): + """Create and return payment profile""" + customer_profile = self.get_customer_profile() + if customer_profile: + return CustomerPaymentProfile.objects.create( + customer_profile=customer_profile, **kwargs) + else: + customer_profile = CustomerProfile.objects.create( + user=self.user, **kwargs) + return customer_profile.payment_profiles.get() + + def get_customer_profile(self): + """Return customer profile or ``None`` if none exists""" + try: + return CustomerProfile.objects.get(user=self.user) + except CustomerProfile.DoesNotExist: + return None + + def save(self): + return self.create_payment_profile( + payment_data=self.cleaned_data, + billing_data=self.cleaned_data, + ) + + class Meta: + model = CustomerPaymentProfile + class HostedCIMProfileForm(forms.Form): token = forms.CharField(widget=forms.HiddenInput) diff --git a/authorizenet/views.py b/authorizenet/views.py index 17981ad..eabded5 100644 --- a/authorizenet/views.py +++ b/authorizenet/views.py @@ -6,7 +6,7 @@ from django.conf import settings from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt -from django.views.generic.edit import FormView +from django.views.generic.edit import CreateView from authorizenet.forms import AIMPaymentForm, BillingAddressForm, CustomerPaymentForm from authorizenet.models import CustomerProfile, CustomerPaymentProfile @@ -125,30 +125,11 @@ def validate_payment_form(self): ) -class PaymentProfileCreationView(FormView): +class PaymentProfileCreationView(CreateView): template_name = 'authorizenet/create_payment_profile.html' form_class = CustomerPaymentForm - def form_valid(self, form): - """If the form is valid, save the payment profile""" - data = form.cleaned_data - self.create_payment_profile(payment_data=data, billing_data=data) - return super(PaymentProfileCreationView, self).form_valid(form) - - def create_payment_profile(self, **kwargs): - """Create and return payment profile""" - customer_profile = self.get_customer_profile() - if customer_profile: - return CustomerPaymentProfile.objects.create( - customer_profile=customer_profile, **kwargs) - else: - customer_profile = CustomerProfile.objects.create( - user=self.request.user, **kwargs) - return customer_profile.payment_profiles.get() - - def get_customer_profile(self): - """Return customer profile or ``None`` if none exists""" - try: - return CustomerProfile.objects.get(user=self.request.user) - except CustomerProfile.DoesNotExist: - return None + def get_form_kwargs(self): + kwargs = super(PaymentProfileCreationView, self).get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs From 0c691fe62a1c27244ac7a78e19b4df9ac0e6e9a1 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 16:37:31 -0700 Subject: [PATCH 13/45] Add generic view to update payment profile --- authorizenet/forms.py | 14 ++++-- authorizenet/models.py | 4 +- authorizenet/views.py | 16 +++++-- tests/tests.py | 99 +++++++++++++++++++++++++----------------- tests/urls.py | 3 +- tests/views.py | 12 ++++- 6 files changed, 96 insertions(+), 52 deletions(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 6e0357f..6d47f1b 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -120,10 +120,16 @@ def get_customer_profile(self): return None def save(self): - return self.create_payment_profile( - payment_data=self.cleaned_data, - billing_data=self.cleaned_data, - ) + if not self.instance or self.instance.id is None: + return self.create_payment_profile( + payment_data=self.cleaned_data, + billing_data=self.cleaned_data, + ) + else: + return self.instance.update( + payment_data=self.cleaned_data, + billing_data=self.cleaned_data, + ) class Meta: model = CustomerPaymentProfile diff --git a/authorizenet/models.py b/authorizenet/models.py index 7cba1bb..1e7e37d 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -206,7 +206,8 @@ class CustomerProfile(models.Model): """Authorize.NET customer profile""" - user = models.ForeignKey('auth.User', unique=True) + user = models.OneToOneField('auth.User', unique=True, + related_name='customer_profile') profile_id = models.CharField(max_length=50) def sync(self): @@ -276,6 +277,7 @@ def update(self, payment_data, billing_data): v = "XXXX%s" % v[-4:] setattr(self, k, v) self.save() + return self def __unicode__(self): return self.card_number diff --git a/authorizenet/views.py b/authorizenet/views.py index eabded5..f493da3 100644 --- a/authorizenet/views.py +++ b/authorizenet/views.py @@ -6,7 +6,7 @@ from django.conf import settings from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt -from django.views.generic.edit import CreateView +from django.views.generic.edit import CreateView, UpdateView from authorizenet.forms import AIMPaymentForm, BillingAddressForm, CustomerPaymentForm from authorizenet.models import CustomerProfile, CustomerPaymentProfile @@ -125,11 +125,21 @@ def validate_payment_form(self): ) -class PaymentProfileCreationView(CreateView): +class PaymentProfileCreateView(CreateView): template_name = 'authorizenet/create_payment_profile.html' form_class = CustomerPaymentForm def get_form_kwargs(self): - kwargs = super(PaymentProfileCreationView, self).get_form_kwargs() + kwargs = super(PaymentProfileCreateView, self).get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + +class PaymentProfileUpdateView(UpdateView): + template_name = 'authorizenet/update_payment_profile.html' + form_class = CustomerPaymentForm + + def get_form_kwargs(self): + kwargs = super(PaymentProfileUpdateView, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs diff --git a/tests/tests.py b/tests/tests.py index 9fb0ef0..5f004bc 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -5,57 +5,26 @@ from authorizenet.cim import extract_form_data, extract_payment_form_data, \ add_profile +from authorizenet.models import CustomerProfile, CustomerPaymentProfile from .utils import create_user, xml_to_dict from .mocks import cim_url_match, success_response - - -create_profile_success_data = { - 'createCustomerProfileRequest': { - 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', - 'profile': { - 'merchantCustomerId': '42', - 'paymentProfiles': { - 'billTo': { - 'firstName': 'Danielle', - 'lastName': 'Thompson', - 'company': '', - 'address': '101 Broadway Avenue', - 'city': 'San Diego', - 'state': 'CA', - 'zip': '92101', - 'country': 'US' - }, - 'payment': { - 'creditCard': { - 'cardCode': '123', - 'cardNumber': "5586086832001747", - 'expirationDate': '2020-05' - } - } - } - }, - 'merchantAuthentication': { - 'transactionKey': 'key', - 'name': 'loginid' - }, - } -} +from .test_data import create_profile_success, update_profile_success class PaymentProfileCreationTests(LiveServerTestCase): - def test_create_new_customer_get(self): - create_user(username='billy', password='password') + def setUp(self): + self.user = create_user(id=42, username='billy', password='password') self.client.login(username='billy', password='password') + + def test_create_new_customer_get(self): response = self.client.get('/customers/create') self.assertNotIn("This field is required", response.content) self.assertIn("Credit Card Number", response.content) self.assertIn("City", response.content) def test_create_new_customer_post_error(self): - create_user(username='billy', password='password') - self.client.login(username='billy', password='password') response = self.client.post('/customers/create') self.assertIn("This field is required", response.content) self.assertIn("Credit Card Number", response.content) @@ -66,10 +35,8 @@ def test_create_new_customer_post_success(self): def create_customer_success(url, request): request_xml = parseString(request.body) self.assertEqual(xml_to_dict(request_xml), - create_profile_success_data) + create_profile_success) return success_response.format('createCustomerProfileResponse') - create_user(id=42, username='billy', password='password') - self.client.login(username='billy', password='password') self.maxDiff = None with HTTMock(create_customer_success): response = self.client.post('/customers/create', { @@ -88,6 +55,56 @@ def create_customer_success(url, request): self.assertIn("success", response.content) +class PaymentProfileUpdateTests(LiveServerTestCase): + + def setUp(self): + self.user = create_user(id=42, username='billy', password='password') + profile = CustomerProfile(user=self.user, profile_id='6666') + profile.save() + self.payment_profile = CustomerPaymentProfile( + customer_profile=profile, + payment_profile_id='7777', + ) + self.payment_profile.save() + self.client.login(username='billy', password='password') + + def test_update_profile_get(self): + response = self.client.get('/customers/update') + self.assertNotIn("This field is required", response.content) + self.assertIn("Credit Card Number", response.content) + self.assertIn("City", response.content) + + def test_update_profile_post_error(self): + response = self.client.post('/customers/update') + self.assertIn("This field is required", response.content) + self.assertIn("Credit Card Number", response.content) + self.assertIn("City", response.content) + + def test_update_profile_post_success(self): + @cim_url_match + def create_customer_success(url, request): + request_xml = parseString(request.body) + self.assertEqual(xml_to_dict(request_xml), + update_profile_success) + return success_response.format('updateCustomerProfileResponse') + self.maxDiff = None + with HTTMock(create_customer_success): + response = self.client.post('/customers/update', { + 'card_number': "5586086832001747", + 'expiration_date_0': "5", + 'expiration_date_1': "2020", + 'card_code': "123", + 'first_name': "Danielle", + 'last_name': "Thompson", + 'address': "101 Broadway Avenue", + 'city': "San Diego", + 'state': "CA", + 'country': "US", + 'zip': "92101", + }, follow=True) + self.assertIn("success", response.content) + + class ExtractFormDataTests(TestCase): """Tests for utility functions converting form data to CIM data""" @@ -129,7 +146,7 @@ def setUp(self): 'country': "US", 'zip': "92101", } - self.request_data = create_profile_success_data + self.request_data = create_profile_success def test_add_profile_minimal(self): """Success test with minimal complexity""" diff --git a/tests/urls.py b/tests/urls.py index 3df0606..8db5d51 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,8 +1,9 @@ from django.conf.urls import url, patterns -from .views import CreateCustomerView, success_view +from .views import CreateCustomerView, UpdateCustomerView, success_view urlpatterns = patterns( '', url(r"^customers/create$", CreateCustomerView.as_view()), + url(r"^customers/update$", UpdateCustomerView.as_view()), url(r"^success$", success_view), ) diff --git a/tests/views.py b/tests/views.py index fb98c77..64b861d 100644 --- a/tests/views.py +++ b/tests/views.py @@ -1,8 +1,16 @@ from django.http import HttpResponse -from authorizenet.views import PaymentProfileCreationView +from authorizenet.views import PaymentProfileCreateView, PaymentProfileUpdateView -class CreateCustomerView(PaymentProfileCreationView): +class CreateCustomerView(PaymentProfileCreateView): + def get_success_url(self): + return '/success' + + +class UpdateCustomerView(PaymentProfileUpdateView): + + def get_object(self): + return self.request.user.customer_profile.payment_profiles.get() def get_success_url(self): return '/success' From 02fbc2c8685e23cc3737b8b1809d1dae786ea7fc Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 16:49:21 -0700 Subject: [PATCH 14/45] Add missing test_data module --- tests/test_data.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_data.py diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..a265d86 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,63 @@ +create_profile_success = { + 'createCustomerProfileRequest': { + 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', + 'profile': { + 'merchantCustomerId': '42', + 'paymentProfiles': { + 'billTo': { + 'firstName': 'Danielle', + 'lastName': 'Thompson', + 'company': '', + 'address': '101 Broadway Avenue', + 'city': 'San Diego', + 'state': 'CA', + 'zip': '92101', + 'country': 'US' + }, + 'payment': { + 'creditCard': { + 'cardCode': '123', + 'cardNumber': "5586086832001747", + 'expirationDate': '2020-05' + } + } + } + }, + 'merchantAuthentication': { + 'transactionKey': 'key', + 'name': 'loginid' + }, + } +} + + +update_profile_success = { + 'updateCustomerPaymentProfileRequest': { + 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', + 'customerProfileId': '6666', + 'paymentProfile': { + 'customerPaymentProfileId': '7777', + 'billTo': { + 'firstName': 'Danielle', + 'lastName': 'Thompson', + 'company': '', + 'address': '101 Broadway Avenue', + 'city': 'San Diego', + 'state': 'CA', + 'zip': '92101', + 'country': 'US' + }, + 'payment': { + 'creditCard': { + 'cardCode': '123', + 'cardNumber': "5586086832001747", + 'expirationDate': '2020-05' + } + } + }, + 'merchantAuthentication': { + 'transactionKey': 'key', + 'name': 'loginid' + }, + } +} From 89f3f6d2e456d9d1e843df3cc5b9c1507a5cf991 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 16:55:58 -0700 Subject: [PATCH 15/45] Add missing test template --- tests/templates/authorizenet/update_payment_profile.html | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/templates/authorizenet/update_payment_profile.html diff --git a/tests/templates/authorizenet/update_payment_profile.html b/tests/templates/authorizenet/update_payment_profile.html new file mode 100644 index 0000000..c592814 --- /dev/null +++ b/tests/templates/authorizenet/update_payment_profile.html @@ -0,0 +1,4 @@ +
+ {{ form }} + +
From 9bd9d87a27d65eb4d2b7116e3d020291820fa4d9 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 18:11:39 -0700 Subject: [PATCH 16/45] Test payment profile data in update view --- authorizenet/models.py | 1 + tests/tests.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/authorizenet/models.py b/authorizenet/models.py index 1e7e37d..876cdd1 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.forms.models import model_to_dict from .cim import get_profile, update_payment_profile, delete_payment_profile diff --git a/tests/tests.py b/tests/tests.py index 5f004bc..af72f95 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -87,7 +87,6 @@ def create_customer_success(url, request): self.assertEqual(xml_to_dict(request_xml), update_profile_success) return success_response.format('updateCustomerProfileResponse') - self.maxDiff = None with HTTMock(create_customer_success): response = self.client.post('/customers/update', { 'card_number': "5586086832001747", @@ -103,6 +102,23 @@ def create_customer_success(url, request): 'zip': "92101", }, follow=True) self.assertIn("success", response.content) + payment_profile = self.user.customer_profile.payment_profiles.get() + self.assertEqual(payment_profile.raw_data(), { + 'id': payment_profile.id, + 'customer_profile': self.user.customer_profile.id, + 'payment_profile_id': '7777', + 'card_number': 'XXXX1747', + 'first_name': 'Danielle', + 'last_name': 'Thompson', + 'company': '', + 'fax': '', + 'phone': '', + 'address': '101 Broadway Avenue', + 'city': 'San Diego', + 'state': 'CA', + 'country': 'US', + 'zip': '92101', + }) class ExtractFormDataTests(TestCase): From 05f9e7eebd2cbe3f2708535bb90a1dc00b182067 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 22:44:26 -0700 Subject: [PATCH 17/45] Move more logic from managers/forms into models --- authorizenet/forms.py | 19 +++-------- authorizenet/managers.py | 62 ++++++++++++++++-------------------- authorizenet/models.py | 69 ++++++++++++++++++++++++++++++---------- tests/mocks.py | 15 ++++++++- tests/test_data.py | 39 +++++++++++++++++++++-- tests/tests.py | 44 ++++++++++++++++++------- 6 files changed, 168 insertions(+), 80 deletions(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 6d47f1b..732d7bd 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -101,15 +101,15 @@ def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) return super(CustomerPaymentForm, self).__init__(*args, **kwargs) - def create_payment_profile(self, **kwargs): + def create_payment_profile(self): """Create and return payment profile""" customer_profile = self.get_customer_profile() if customer_profile: return CustomerPaymentProfile.objects.create( - customer_profile=customer_profile, **kwargs) + customer_profile=customer_profile, **self.cleaned_data) else: customer_profile = CustomerProfile.objects.create( - user=self.user, **kwargs) + user=self.user, **self.cleaned_data) return customer_profile.payment_profiles.get() def get_customer_profile(self): @@ -121,18 +121,9 @@ def get_customer_profile(self): def save(self): if not self.instance or self.instance.id is None: - return self.create_payment_profile( - payment_data=self.cleaned_data, - billing_data=self.cleaned_data, - ) + return self.create_payment_profile() else: - return self.instance.update( - payment_data=self.cleaned_data, - billing_data=self.cleaned_data, - ) - - class Meta: - model = CustomerPaymentProfile + return self.instance.update(**self.cleaned_data) class HostedCIMProfileForm(forms.Form): diff --git a/authorizenet/managers.py b/authorizenet/managers.py index c1779e3..f4dcfed 100644 --- a/authorizenet/managers.py +++ b/authorizenet/managers.py @@ -1,39 +1,43 @@ from django.db import models -from authorizenet.cim import add_profile, create_payment_profile +from authorizenet.cim import add_profile from .exceptions import BillingError class CustomerProfileManager(models.Manager): - def create(self, **kwargs): + def create(self, **data): """Create new Authorize.NET customer profile""" from .models import CustomerPaymentProfile - user = kwargs.get('user') - payment_data = kwargs.pop('payment_data', {}) - billing_data = kwargs.pop('billing_data', {}) + kwargs = data + sync = kwargs.pop('sync', True) + kwargs = { + 'user': kwargs.pop('user', None), + 'profile_id': kwargs.pop('profile_id', None), + } # Create the customer profile with Authorize.NET CIM call - output = add_profile(user.pk, payment_data, billing_data) - if not output['response'].success: - raise BillingError("Error creating customer profile") - kwargs['profile_id'] = output['profile_id'] + if sync: + output = add_profile(kwargs['user'].pk, data, data) + if not output['response'].success: + raise BillingError("Error creating customer profile") + kwargs['profile_id'] = output['profile_id'] # Store customer profile data locally instance = super(CustomerProfileManager, self).create(**kwargs) - # Store customer payment profile data locally - for payment_profile_id in output['payment_profile_ids']: - CustomerPaymentProfile.objects.create( - customer_profile=instance, - payment_profile_id=payment_profile_id, - billing_data=billing_data, - payment_data=payment_data, - make_cim_request=False, - ) + if sync: + # Store customer payment profile data locally + for payment_profile_id in output['payment_profile_ids']: + CustomerPaymentProfile.objects.create( + customer_profile=instance, + payment_profile_id=payment_profile_id, + sync=False, + **data + ) return instance @@ -42,20 +46,8 @@ class CustomerPaymentProfileManager(models.Manager): def create(self, **kwargs): """Create new Authorize.NET customer payment profile""" - customer_profile = kwargs.get('customer_profile') - payment_data = kwargs.pop('payment_data', {}) - billing_data = kwargs.pop('billing_data', {}) - if kwargs.pop('make_cim_request', True): - # Create the customer payment profile with Authorize.NET CIM call - response, payment_profile_id = create_payment_profile( - customer_profile.profile_id, payment_data, billing_data) - if not response.success: - raise BillingError() - kwargs['payment_profile_id'] = payment_profile_id - kwargs.update(billing_data) - kwargs.update(payment_data) - kwargs.pop('expiration_date') - kwargs.pop('card_code') - if 'card_number' in kwargs: - kwargs['card_number'] = "XXXX%s" % kwargs['card_number'][-4:] - return super(CustomerPaymentProfileManager, self).create(**kwargs) + sync = kwargs.pop('sync', True) + obj = self.model(**kwargs) + self._for_write = True + obj.save(force_insert=True, using=self.db, sync=sync) + return obj diff --git a/authorizenet/models.py b/authorizenet/models.py index 876cdd1..606ada2 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -1,7 +1,7 @@ from django.db import models from django.forms.models import model_to_dict -from .cim import get_profile, update_payment_profile, delete_payment_profile +from .cim import get_profile, update_payment_profile, create_payment_profile, delete_payment_profile from .managers import CustomerProfileManager, CustomerPaymentProfileManager from .exceptions import BillingError @@ -230,6 +230,10 @@ class CustomerPaymentProfile(models.Model): """Authorize.NET customer payment profile""" + BILLING_FIELDS = ['first_name', 'last_name', 'company', 'address', 'city', + 'state', 'zip', 'country', 'phone_number', 'fax_number'] + PAYMENT_FIELDS = ['card_number', 'expiration_date', 'card_code'] + customer_profile = models.ForeignKey('CustomerProfile', related_name='payment_profiles') first_name = models.CharField(max_length=50, blank=True) @@ -240,10 +244,44 @@ class CustomerPaymentProfile(models.Model): state = models.CharField(max_length=40, blank=True) zip = models.CharField(max_length=20, blank=True, verbose_name="ZIP") country = models.CharField(max_length=60, blank=True) - phone = models.CharField(max_length=25, blank=True) - fax = models.CharField(max_length=25, blank=True) + phone_number = models.CharField(max_length=25, blank=True) + fax_number = models.CharField(max_length=25, blank=True) payment_profile_id = models.CharField(max_length=50) card_number = models.CharField(max_length=16, blank=True) + expiration_date = None + card_code = None + + def __init__(self, *args, **kwargs): + self.card_code = kwargs.pop('card_code', None) + self.expiration_date = kwargs.pop('expiration_date', None) + return super(CustomerPaymentProfile, self).__init__(*args, **kwargs) + + def save(self, *args, **kwargs): + if kwargs.pop('sync', True): + self.push_to_server() + self.expiration_date = None + self.card_code = None + self.card_number = "XXXX%s" % self.card_number[-4:] + super(CustomerPaymentProfile, self).save(*args, **kwargs) + + def push_to_server(self): + if self.payment_profile_id: + response = update_payment_profile( + self.customer_profile.profile_id, + self.payment_profile_id, + self.payment_data, + self.billing_data, + ) + else: + output = create_payment_profile( + self.customer_profile.profile_id, + self.payment_data, + self.billing_data, + ) + response = output['response'] + self.payment_profile_id = output['payment_profile_id'] + if not response.success: + raise BillingError() def raw_data(self): """Return data suitable for use in payment and billing forms""" @@ -262,24 +300,21 @@ def delete(self): delete_payment_profile(self.customer_profile.profile_id, self.payment_profile_id) - def update(self, payment_data, billing_data): + def update(self, **data): """Update the customer payment profile remotely and locally""" - response = update_payment_profile(self.customer_profile.profile_id, - self.payment_profile_id, - payment_data, billing_data) - if not response.success: - raise BillingError() - for k, v in billing_data.items(): - setattr(self, k, v) - for k, v in payment_data.items(): - # Do not store expiration date and mask credit card number - if k != 'expiration_date' and k != 'card_code': - if k == 'card_number': - v = "XXXX%s" % v[-4:] - setattr(self, k, v) + for key, value in data.items(): + setattr(self, key, value) self.save() return self + @property + def payment_data(self): + return dict((k, getattr(self, k)) for k in self.PAYMENT_FIELDS) + + @property + def billing_data(self): + return dict((k, getattr(self, k)) for k in self.BILLING_FIELDS) + def __unicode__(self): return self.card_number diff --git a/tests/mocks.py b/tests/mocks.py index f3c1aa1..632c1e7 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -5,7 +5,7 @@ path=r'^/xml/v1/request\.api$') -success_response = ( +customer_profile_success = ( '' '<{0}>' '' @@ -20,3 +20,16 @@ '' '' ) + + +payment_profile_success = ( + '' + '<{0}>' + '' + 'Ok' + 'I00001Successful.' + '' + '6666' + '7777' + '' +) diff --git a/tests/test_data.py b/tests/test_data.py index a265d86..589bc71 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -25,7 +25,7 @@ }, 'merchantAuthentication': { 'transactionKey': 'key', - 'name': 'loginid' + 'name': 'loginid', }, } } @@ -41,6 +41,8 @@ 'firstName': 'Danielle', 'lastName': 'Thompson', 'company': '', + 'phoneNumber': '', + 'faxNumber': '', 'address': '101 Broadway Avenue', 'city': 'San Diego', 'state': 'CA', @@ -57,7 +59,40 @@ }, 'merchantAuthentication': { 'transactionKey': 'key', - 'name': 'loginid' + 'name': 'loginid', + }, + } +} + + +create_payment_profile_success = { + 'createCustomerPaymentProfileRequest': { + 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', + 'customerProfileId': '6666', + 'paymentProfile': { + 'billTo': { + 'firstName': 'Danielle', + 'lastName': 'Thompson', + 'phoneNumber': '', + 'faxNumber': '', + 'company': '', + 'address': '101 Broadway Avenue', + 'city': 'San Diego', + 'state': 'CA', + 'zip': '92101', + 'country': 'US' + }, + 'payment': { + 'creditCard': { + 'cardCode': '123', + 'cardNumber': "5586086832001747", + 'expirationDate': '2020-05' + } + } + }, + 'merchantAuthentication': { + 'transactionKey': 'key', + 'name': 'loginid', }, } } diff --git a/tests/tests.py b/tests/tests.py index af72f95..a7e7487 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -8,8 +8,8 @@ from authorizenet.models import CustomerProfile, CustomerPaymentProfile from .utils import create_user, xml_to_dict -from .mocks import cim_url_match, success_response -from .test_data import create_profile_success, update_profile_success +from .mocks import cim_url_match, customer_profile_success, payment_profile_success +from .test_data import create_profile_success, update_profile_success, create_payment_profile_success class PaymentProfileCreationTests(LiveServerTestCase): @@ -34,10 +34,8 @@ def test_create_new_customer_post_success(self): @cim_url_match def create_customer_success(url, request): request_xml = parseString(request.body) - self.assertEqual(xml_to_dict(request_xml), - create_profile_success) - return success_response.format('createCustomerProfileResponse') - self.maxDiff = None + self.assertEqual(xml_to_dict(request_xml), create_profile_success) + return customer_profile_success.format('createCustomerProfileResponse') with HTTMock(create_customer_success): response = self.client.post('/customers/create', { 'card_number': "5586086832001747", @@ -54,6 +52,30 @@ def create_customer_success(url, request): }, follow=True) self.assertIn("success", response.content) + def test_create_new_payment_profile_post_success(self): + @cim_url_match + def request_handler(url, request): + request_xml = parseString(request.body) + self.assertEqual(xml_to_dict(request_xml), + create_payment_profile_success) + return payment_profile_success.format('createCustomerPaymentProfileResponse') + CustomerProfile.objects.create(user=self.user, profile_id='6666', sync=False) + with HTTMock(request_handler): + response = self.client.post('/customers/create', { + 'card_number': "5586086832001747", + 'expiration_date_0': "5", + 'expiration_date_1': "2020", + 'card_code': "123", + 'first_name': "Danielle", + 'last_name': "Thompson", + 'address': "101 Broadway Avenue", + 'city': "San Diego", + 'state': "CA", + 'country': "US", + 'zip': "92101", + }, follow=True) + self.assertIn("success", response.content) + class PaymentProfileUpdateTests(LiveServerTestCase): @@ -65,7 +87,7 @@ def setUp(self): customer_profile=profile, payment_profile_id='7777', ) - self.payment_profile.save() + self.payment_profile.save(sync=False) self.client.login(username='billy', password='password') def test_update_profile_get(self): @@ -86,7 +108,7 @@ def create_customer_success(url, request): request_xml = parseString(request.body) self.assertEqual(xml_to_dict(request_xml), update_profile_success) - return success_response.format('updateCustomerProfileResponse') + return customer_profile_success.format('updateCustomerProfileResponse') with HTTMock(create_customer_success): response = self.client.post('/customers/update', { 'card_number': "5586086832001747", @@ -111,8 +133,8 @@ def create_customer_success(url, request): 'first_name': 'Danielle', 'last_name': 'Thompson', 'company': '', - 'fax': '', - 'phone': '', + 'fax_number': '', + 'phone_number': '', 'address': '101 Broadway Avenue', 'city': 'San Diego', 'state': 'CA', @@ -170,7 +192,7 @@ def test_add_profile_minimal(self): def request_handler(url, request): request_xml = parseString(request.body) self.assertEqual(xml_to_dict(request_xml), self.request_data) - return success_response.format('createCustomerProfileResponse') + return customer_profile_success.format('createCustomerProfileResponse') with HTTMock(request_handler): result = add_profile(42, self.payment_form_data, self.billing_form_data) From 21f226c3443f41e48de08b83ee30702c612760f5 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 11 Jun 2013 22:54:04 -0700 Subject: [PATCH 18/45] Split tests into separate files --- tests/tests/__init__.py | 2 + tests/tests/cim.py | 76 ++++++++++++++++++++++++++++++ tests/{ => tests}/mocks.py | 0 tests/{ => tests}/test_data.py | 0 tests/{ => tests}/utils.py | 0 tests/{tests.py => tests/views.py} | 75 ++--------------------------- 6 files changed, 83 insertions(+), 70 deletions(-) create mode 100644 tests/tests/__init__.py create mode 100644 tests/tests/cim.py rename tests/{ => tests}/mocks.py (100%) rename tests/{ => tests}/test_data.py (100%) rename tests/{ => tests}/utils.py (100%) rename tests/{tests.py => tests/views.py} (69%) diff --git a/tests/tests/__init__.py b/tests/tests/__init__.py new file mode 100644 index 0000000..6ecfb67 --- /dev/null +++ b/tests/tests/__init__.py @@ -0,0 +1,2 @@ +from .cim import * +from .views import * diff --git a/tests/tests/cim.py b/tests/tests/cim.py new file mode 100644 index 0000000..8e45588 --- /dev/null +++ b/tests/tests/cim.py @@ -0,0 +1,76 @@ +from datetime import datetime +from django.test import TestCase +from xml.dom.minidom import parseString +from httmock import HTTMock + +from authorizenet.cim import extract_form_data, extract_payment_form_data, \ + add_profile + +from .utils import xml_to_dict +from .mocks import cim_url_match, customer_profile_success +from .test_data import create_profile_success + + +class ExtractFormDataTests(TestCase): + + """Tests for utility functions converting form data to CIM data""" + + def test_extract_form_data(self): + new_data = extract_form_data({'word': "1", 'multi_word_str': "2"}) + self.assertEqual(new_data, {'word': "1", 'multiWordStr': "2"}) + + def test_extract_payment_form_data(self): + data = extract_payment_form_data({ + 'card_number': "1111", + 'expiration_date': datetime(2020, 5, 1), + 'card_code': "123", + }) + self.assertEqual(data, { + 'cardNumber': "1111", + 'expirationDate': "2020-05", + 'cardCode': "123", + }) + + +class AddProfileTests(TestCase): + + """Tests for add_profile utility function""" + + def setUp(self): + self.payment_form_data = { + 'card_number': "5586086832001747", + 'expiration_date': datetime(2020, 5, 1), + 'card_code': "123", + } + self.billing_form_data = { + 'first_name': "Danielle", + 'last_name': "Thompson", + 'company': "", + 'address': "101 Broadway Avenue", + 'city': "San Diego", + 'state': "CA", + 'country': "US", + 'zip': "92101", + } + self.request_data = create_profile_success + + def test_add_profile_minimal(self): + """Success test with minimal complexity""" + @cim_url_match + def request_handler(url, request): + request_xml = parseString(request.body) + self.assertEqual(xml_to_dict(request_xml), self.request_data) + return customer_profile_success.format('createCustomerProfileResponse') + with HTTMock(request_handler): + result = add_profile(42, self.payment_form_data, + self.billing_form_data) + response = result.pop('response') + self.assertEqual(result, { + 'profile_id': '6666', + 'payment_profile_ids': ['7777'], + 'shipping_profile_ids': [], + }) + self.assertEqual(response.result, 'Ok') + self.assertEqual(response.result_code, 'I00001') + self.assertEqual(response.result_text, 'Successful.') + self.assertIsNone(response.transaction_response) diff --git a/tests/mocks.py b/tests/tests/mocks.py similarity index 100% rename from tests/mocks.py rename to tests/tests/mocks.py diff --git a/tests/test_data.py b/tests/tests/test_data.py similarity index 100% rename from tests/test_data.py rename to tests/tests/test_data.py diff --git a/tests/utils.py b/tests/tests/utils.py similarity index 100% rename from tests/utils.py rename to tests/tests/utils.py diff --git a/tests/tests.py b/tests/tests/views.py similarity index 69% rename from tests/tests.py rename to tests/tests/views.py index a7e7487..f4549d6 100644 --- a/tests/tests.py +++ b/tests/tests/views.py @@ -1,15 +1,14 @@ -from datetime import datetime -from django.test import TestCase, LiveServerTestCase +from django.test import LiveServerTestCase from xml.dom.minidom import parseString from httmock import HTTMock -from authorizenet.cim import extract_form_data, extract_payment_form_data, \ - add_profile from authorizenet.models import CustomerProfile, CustomerPaymentProfile from .utils import create_user, xml_to_dict -from .mocks import cim_url_match, customer_profile_success, payment_profile_success -from .test_data import create_profile_success, update_profile_success, create_payment_profile_success +from .mocks import cim_url_match, customer_profile_success, \ + payment_profile_success +from .test_data import create_profile_success, update_profile_success, \ + create_payment_profile_success class PaymentProfileCreationTests(LiveServerTestCase): @@ -142,67 +141,3 @@ def create_customer_success(url, request): 'zip': '92101', }) - -class ExtractFormDataTests(TestCase): - - """Tests for utility functions converting form data to CIM data""" - - def test_extract_form_data(self): - new_data = extract_form_data({'word': "1", 'multi_word_str': "2"}) - self.assertEqual(new_data, {'word': "1", 'multiWordStr': "2"}) - - def test_extract_payment_form_data(self): - data = extract_payment_form_data({ - 'card_number': "1111", - 'expiration_date': datetime(2020, 5, 1), - 'card_code': "123", - }) - self.assertEqual(data, { - 'cardNumber': "1111", - 'expirationDate': "2020-05", - 'cardCode': "123", - }) - - -class AddProfileTests(TestCase): - - """Tests for add_profile utility function""" - - def setUp(self): - self.payment_form_data = { - 'card_number': "5586086832001747", - 'expiration_date': datetime(2020, 5, 1), - 'card_code': "123", - } - self.billing_form_data = { - 'first_name': "Danielle", - 'last_name': "Thompson", - 'company': "", - 'address': "101 Broadway Avenue", - 'city': "San Diego", - 'state': "CA", - 'country': "US", - 'zip': "92101", - } - self.request_data = create_profile_success - - def test_add_profile_minimal(self): - """Success test with minimal complexity""" - @cim_url_match - def request_handler(url, request): - request_xml = parseString(request.body) - self.assertEqual(xml_to_dict(request_xml), self.request_data) - return customer_profile_success.format('createCustomerProfileResponse') - with HTTMock(request_handler): - result = add_profile(42, self.payment_form_data, - self.billing_form_data) - response = result.pop('response') - self.assertEqual(result, { - 'profile_id': '6666', - 'payment_profile_ids': ['7777'], - 'shipping_profile_ids': [], - }) - self.assertEqual(response.result, 'Ok') - self.assertEqual(response.result_code, 'I00001') - self.assertEqual(response.result_text, 'Successful.') - self.assertIsNone(response.transaction_response) From 0dec84d5c7b7d61dbfd882065e43a1e7fa6505f9 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 12 Jun 2013 15:13:34 -0700 Subject: [PATCH 19/45] Add user to customer payment profile models --- authorizenet/forms.py | 4 +++- authorizenet/managers.py | 2 +- authorizenet/models.py | 46 +++++++++++++++++++++------------------- tests/tests/views.py | 6 +++++- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 732d7bd..dd6717a 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -106,7 +106,9 @@ def create_payment_profile(self): customer_profile = self.get_customer_profile() if customer_profile: return CustomerPaymentProfile.objects.create( - customer_profile=customer_profile, **self.cleaned_data) + customer_profile=customer_profile, + user=self.user, + **self.cleaned_data) else: customer_profile = CustomerProfile.objects.create( user=self.user, **self.cleaned_data) diff --git a/authorizenet/managers.py b/authorizenet/managers.py index f4dcfed..e5d86a1 100644 --- a/authorizenet/managers.py +++ b/authorizenet/managers.py @@ -15,7 +15,7 @@ def create(self, **data): kwargs = data sync = kwargs.pop('sync', True) kwargs = { - 'user': kwargs.pop('user', None), + 'user': kwargs.get('user', None), 'profile_id': kwargs.pop('profile_id', None), } diff --git a/authorizenet/models.py b/authorizenet/models.py index 606ada2..31eac4a 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -1,7 +1,8 @@ from django.db import models from django.forms.models import model_to_dict -from .cim import get_profile, update_payment_profile, create_payment_profile, delete_payment_profile +from .cim import add_profile, get_profile, update_payment_profile, \ + create_payment_profile, delete_payment_profile from .managers import CustomerProfileManager, CustomerPaymentProfileManager from .exceptions import BillingError @@ -230,23 +231,20 @@ class CustomerPaymentProfile(models.Model): """Authorize.NET customer payment profile""" - BILLING_FIELDS = ['first_name', 'last_name', 'company', 'address', 'city', - 'state', 'zip', 'country', 'phone_number', 'fax_number'] - PAYMENT_FIELDS = ['card_number', 'expiration_date', 'card_code'] - + user = models.ForeignKey('auth.User', related_name='payment_profiles') customer_profile = models.ForeignKey('CustomerProfile', related_name='payment_profiles') + payment_profile_id = models.CharField(max_length=50) first_name = models.CharField(max_length=50, blank=True) last_name = models.CharField(max_length=50, blank=True) company = models.CharField(max_length=50, blank=True) + phone_number = models.CharField(max_length=25, blank=True) + fax_number = models.CharField(max_length=25, blank=True) address = models.CharField(max_length=60, blank=True) city = models.CharField(max_length=40, blank=True) state = models.CharField(max_length=40, blank=True) zip = models.CharField(max_length=20, blank=True, verbose_name="ZIP") country = models.CharField(max_length=60, blank=True) - phone_number = models.CharField(max_length=25, blank=True) - fax_number = models.CharField(max_length=25, blank=True) - payment_profile_id = models.CharField(max_length=50) card_number = models.CharField(max_length=16, blank=True) expiration_date = None card_code = None @@ -269,23 +267,35 @@ def push_to_server(self): response = update_payment_profile( self.customer_profile.profile_id, self.payment_profile_id, - self.payment_data, - self.billing_data, + self.raw_data, + self.raw_data, ) - else: + elif self.customer_profile: output = create_payment_profile( self.customer_profile.profile_id, - self.payment_data, - self.billing_data, + self.raw_data, + self.raw_data, + ) + response = output['response'] + self.payment_profile_id = output['payment_profile_id'] + else: + output = add_profile( + self.user, + self.raw_data, + self.raw_data, ) response = output['response'] self.payment_profile_id = output['payment_profile_id'] if not response.success: raise BillingError() + @property def raw_data(self): """Return data suitable for use in payment and billing forms""" - return model_to_dict(self) + data = model_to_dict(self) + data.update(dict((k, getattr(self, k)) + for k in ('expiration_date', 'card_code'))) + return data def sync(self, data): """Overwrite local customer payment profile data with remote data""" @@ -307,14 +317,6 @@ def update(self, **data): self.save() return self - @property - def payment_data(self): - return dict((k, getattr(self, k)) for k in self.PAYMENT_FIELDS) - - @property - def billing_data(self): - return dict((k, getattr(self, k)) for k in self.BILLING_FIELDS) - def __unicode__(self): return self.card_number diff --git a/tests/tests/views.py b/tests/tests/views.py index f4549d6..bdcb179 100644 --- a/tests/tests/views.py +++ b/tests/tests/views.py @@ -83,6 +83,7 @@ def setUp(self): profile = CustomerProfile(user=self.user, profile_id='6666') profile.save() self.payment_profile = CustomerPaymentProfile( + user=self.user, customer_profile=profile, payment_profile_id='7777', ) @@ -124,11 +125,14 @@ def create_customer_success(url, request): }, follow=True) self.assertIn("success", response.content) payment_profile = self.user.customer_profile.payment_profiles.get() - self.assertEqual(payment_profile.raw_data(), { + self.assertEqual(payment_profile.raw_data, { 'id': payment_profile.id, 'customer_profile': self.user.customer_profile.id, + 'user': self.user.id, 'payment_profile_id': '7777', 'card_number': 'XXXX1747', + 'expiration_date': None, + 'card_code': None, 'first_name': 'Danielle', 'last_name': 'Thompson', 'company': '', From 313774889257cca6b59759460b547164897bd1f8 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 12 Jun 2013 15:26:48 -0700 Subject: [PATCH 20/45] Always create payment profile directly in form --- authorizenet/forms.py | 12 ++++-------- authorizenet/models.py | 11 ++++++++--- tests/tests/cim.py | 6 +++++- tests/tests/test_data.py | 2 ++ 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index dd6717a..a237bd8 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -103,16 +103,12 @@ def __init__(self, *args, **kwargs): def create_payment_profile(self): """Create and return payment profile""" + kwargs = {'user': self.user} + kwargs.update(self.cleaned_data) customer_profile = self.get_customer_profile() if customer_profile: - return CustomerPaymentProfile.objects.create( - customer_profile=customer_profile, - user=self.user, - **self.cleaned_data) - else: - customer_profile = CustomerProfile.objects.create( - user=self.user, **self.cleaned_data) - return customer_profile.payment_profiles.get() + kwargs['customer_profile'] = customer_profile + return CustomerPaymentProfile.objects.create(**kwargs) def get_customer_profile(self): """Return customer profile or ``None`` if none exists""" diff --git a/authorizenet/models.py b/authorizenet/models.py index 31eac4a..5baf761 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -270,7 +270,7 @@ def push_to_server(self): self.raw_data, self.raw_data, ) - elif self.customer_profile: + elif self.customer_profile_id: output = create_payment_profile( self.customer_profile.profile_id, self.raw_data, @@ -280,12 +280,17 @@ def push_to_server(self): self.payment_profile_id = output['payment_profile_id'] else: output = add_profile( - self.user, + self.user.id, self.raw_data, self.raw_data, ) response = output['response'] - self.payment_profile_id = output['payment_profile_id'] + self.customer_profile = CustomerProfile.objects.create( + user=self.user, + profile_id=output['profile_id'], + sync=False, + ) + self.payment_profile_id = output['payment_profile_ids'][0] if not response.success: raise BillingError() diff --git a/tests/tests/cim.py b/tests/tests/cim.py index 8e45588..dfbb27f 100644 --- a/tests/tests/cim.py +++ b/tests/tests/cim.py @@ -1,3 +1,4 @@ +from copy import deepcopy from datetime import datetime from django.test import TestCase from xml.dom.minidom import parseString @@ -52,7 +53,10 @@ def setUp(self): 'country': "US", 'zip': "92101", } - self.request_data = create_profile_success + self.request_data = deepcopy(create_profile_success) + profile = self.request_data['createCustomerProfileRequest']['profile'] + del profile['paymentProfiles']['billTo']['phoneNumber'] + del profile['paymentProfiles']['billTo']['faxNumber'] def test_add_profile_minimal(self): """Success test with minimal complexity""" diff --git a/tests/tests/test_data.py b/tests/tests/test_data.py index 589bc71..b301b0d 100644 --- a/tests/tests/test_data.py +++ b/tests/tests/test_data.py @@ -8,6 +8,8 @@ 'firstName': 'Danielle', 'lastName': 'Thompson', 'company': '', + 'phoneNumber': '', + 'faxNumber': '', 'address': '101 Broadway Avenue', 'city': 'San Diego', 'state': 'CA', From 035377450f2da05e40cc483a31bca1ae65030a8a Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 12 Jun 2013 15:42:15 -0700 Subject: [PATCH 21/45] Use ModelForm for creating payment profile --- authorizenet/forms.py | 40 ++++++++++++++++++---------------------- authorizenet/models.py | 6 ++++++ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index a237bd8..44c81c0 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -92,36 +92,32 @@ class CIMPaymentForm(forms.Form): card_code = CreditCardCVV2Field(label="Card Security Code") -class CustomerPaymentForm(CIMPaymentForm, BillingAddressForm): +class CustomerPaymentForm(forms.ModelForm): """Base customer payment form without shipping address""" + country = CountryField(label="Country", initial="US") + card_number = CreditCardField(label="Credit Card Number") + expiration_date = CreditCardExpiryField(label="Expiration Date") + card_code = CreditCardCVV2Field(label="Card Security Code") + def __init__(self, *args, **kwargs): - self.instance = kwargs.pop('instance', None) self.user = kwargs.pop('user', None) return super(CustomerPaymentForm, self).__init__(*args, **kwargs) - def create_payment_profile(self): - """Create and return payment profile""" - kwargs = {'user': self.user} - kwargs.update(self.cleaned_data) - customer_profile = self.get_customer_profile() - if customer_profile: - kwargs['customer_profile'] = customer_profile - return CustomerPaymentProfile.objects.create(**kwargs) - - def get_customer_profile(self): - """Return customer profile or ``None`` if none exists""" - try: - return CustomerProfile.objects.get(user=self.user) - except CustomerProfile.DoesNotExist: - return None - def save(self): - if not self.instance or self.instance.id is None: - return self.create_payment_profile() - else: - return self.instance.update(**self.cleaned_data) + instance = super(CustomerPaymentForm, self).save(commit=False) + instance.user = self.user + instance.expiration_date = self.cleaned_data['expiration_date'] + instance.card_code = self.cleaned_data['card_code'] + instance.save() + return instance + + class Meta: + model = CustomerPaymentProfile + fields = ('first_name', 'last_name', 'company', 'address', 'city', + 'state', 'country', 'zip', 'card_number', + 'expiration_date', 'card_code') class HostedCIMProfileForm(forms.Form): diff --git a/authorizenet/models.py b/authorizenet/models.py index 5baf761..d16e0dc 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -263,6 +263,12 @@ def save(self, *args, **kwargs): super(CustomerPaymentProfile, self).save(*args, **kwargs) def push_to_server(self): + if not self.customer_profile_id: + try: + self.customer_profile = CustomerProfile.objects.get( + user=self.user) + except CustomerProfile.DoesNotExist: + pass if self.payment_profile_id: response = update_payment_profile( self.customer_profile.profile_id, From 54c7bbc35213481d23406268f6a5bb8219064b25 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Sat, 15 Jun 2013 00:13:17 -0700 Subject: [PATCH 22/45] Allow customer models to reference non-User model --- authorizenet/forms.py | 4 ++-- authorizenet/managers.py | 4 ++-- authorizenet/models.py | 14 ++++++++------ authorizenet/views.py | 4 ++-- runtests.py | 1 + tests/tests/views.py | 8 ++++---- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 44c81c0..7d1eccc 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -102,12 +102,12 @@ class CustomerPaymentForm(forms.ModelForm): card_code = CreditCardCVV2Field(label="Card Security Code") def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user', None) + self.customer = kwargs.pop('customer', None) return super(CustomerPaymentForm, self).__init__(*args, **kwargs) def save(self): instance = super(CustomerPaymentForm, self).save(commit=False) - instance.user = self.user + instance.customer = self.customer instance.expiration_date = self.cleaned_data['expiration_date'] instance.card_code = self.cleaned_data['card_code'] instance.save() diff --git a/authorizenet/managers.py b/authorizenet/managers.py index e5d86a1..3f6f8f7 100644 --- a/authorizenet/managers.py +++ b/authorizenet/managers.py @@ -15,13 +15,13 @@ def create(self, **data): kwargs = data sync = kwargs.pop('sync', True) kwargs = { - 'user': kwargs.get('user', None), + 'customer': kwargs.get('customer', None), 'profile_id': kwargs.pop('profile_id', None), } # Create the customer profile with Authorize.NET CIM call if sync: - output = add_profile(kwargs['user'].pk, data, data) + output = add_profile(kwargs['customer'].pk, data, data) if not output['response'].success: raise BillingError("Error creating customer profile") kwargs['profile_id'] = output['profile_id'] diff --git a/authorizenet/models.py b/authorizenet/models.py index d16e0dc..f299e5f 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -1,5 +1,6 @@ from django.db import models from django.forms.models import model_to_dict +from django.conf import settings from .cim import add_profile, get_profile, update_payment_profile, \ create_payment_profile, delete_payment_profile @@ -208,8 +209,8 @@ class CustomerProfile(models.Model): """Authorize.NET customer profile""" - user = models.OneToOneField('auth.User', unique=True, - related_name='customer_profile') + customer = models.OneToOneField(settings.AUTHNET_CUSTOMER_MODEL, + related_name='customer_profile') profile_id = models.CharField(max_length=50) def sync(self): @@ -231,7 +232,8 @@ class CustomerPaymentProfile(models.Model): """Authorize.NET customer payment profile""" - user = models.ForeignKey('auth.User', related_name='payment_profiles') + customer = models.ForeignKey(settings.AUTHNET_CUSTOMER_MODEL, + related_name='payment_profiles') customer_profile = models.ForeignKey('CustomerProfile', related_name='payment_profiles') payment_profile_id = models.CharField(max_length=50) @@ -266,7 +268,7 @@ def push_to_server(self): if not self.customer_profile_id: try: self.customer_profile = CustomerProfile.objects.get( - user=self.user) + customer=self.customer) except CustomerProfile.DoesNotExist: pass if self.payment_profile_id: @@ -286,13 +288,13 @@ def push_to_server(self): self.payment_profile_id = output['payment_profile_id'] else: output = add_profile( - self.user.id, + self.customer.id, self.raw_data, self.raw_data, ) response = output['response'] self.customer_profile = CustomerProfile.objects.create( - user=self.user, + customer=self.customer, profile_id=output['profile_id'], sync=False, ) diff --git a/authorizenet/views.py b/authorizenet/views.py index f493da3..8eb61dc 100644 --- a/authorizenet/views.py +++ b/authorizenet/views.py @@ -131,7 +131,7 @@ class PaymentProfileCreateView(CreateView): def get_form_kwargs(self): kwargs = super(PaymentProfileCreateView, self).get_form_kwargs() - kwargs['user'] = self.request.user + kwargs['customer'] = self.request.user return kwargs @@ -141,5 +141,5 @@ class PaymentProfileUpdateView(UpdateView): def get_form_kwargs(self): kwargs = super(PaymentProfileUpdateView, self).get_form_kwargs() - kwargs['user'] = self.request.user + kwargs['customer'] = self.request.user return kwargs diff --git a/runtests.py b/runtests.py index 89cb44f..3775761 100644 --- a/runtests.py +++ b/runtests.py @@ -10,6 +10,7 @@ AUTHNET_DEBUG=False, AUTHNET_LOGIN_ID="loginid", AUTHNET_TRANSACTION_KEY="key", + AUTHNET_CUSTOMER_MODEL='auth.User', INSTALLED_APPS=( 'django.contrib.contenttypes', 'django.contrib.auth', diff --git a/tests/tests/views.py b/tests/tests/views.py index bdcb179..fc50886 100644 --- a/tests/tests/views.py +++ b/tests/tests/views.py @@ -58,7 +58,7 @@ def request_handler(url, request): self.assertEqual(xml_to_dict(request_xml), create_payment_profile_success) return payment_profile_success.format('createCustomerPaymentProfileResponse') - CustomerProfile.objects.create(user=self.user, profile_id='6666', sync=False) + CustomerProfile.objects.create(customer=self.user, profile_id='6666', sync=False) with HTTMock(request_handler): response = self.client.post('/customers/create', { 'card_number': "5586086832001747", @@ -80,10 +80,10 @@ class PaymentProfileUpdateTests(LiveServerTestCase): def setUp(self): self.user = create_user(id=42, username='billy', password='password') - profile = CustomerProfile(user=self.user, profile_id='6666') + profile = CustomerProfile(customer=self.user, profile_id='6666') profile.save() self.payment_profile = CustomerPaymentProfile( - user=self.user, + customer=self.user, customer_profile=profile, payment_profile_id='7777', ) @@ -128,7 +128,7 @@ def create_customer_success(url, request): self.assertEqual(payment_profile.raw_data, { 'id': payment_profile.id, 'customer_profile': self.user.customer_profile.id, - 'user': self.user.id, + 'customer': self.user.id, 'payment_profile_id': '7777', 'card_number': 'XXXX1747', 'expiration_date': None, From 9bb37130f89cf5650811c4f975ef5aa1fc957e5f Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Sat, 15 Jun 2013 00:13:57 -0700 Subject: [PATCH 23/45] Allow optional card_code field in forms --- authorizenet/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 7d1eccc..d2df946 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -109,7 +109,7 @@ def save(self): instance = super(CustomerPaymentForm, self).save(commit=False) instance.customer = self.customer instance.expiration_date = self.cleaned_data['expiration_date'] - instance.card_code = self.cleaned_data['card_code'] + instance.card_code = self.cleaned_data.get('card_code') instance.save() return instance From 54f7c4c911eda549eed22e4f69710dab5d06f505 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Mon, 24 Jun 2013 13:52:54 -0700 Subject: [PATCH 24/45] Fix CIM response parsing UTF-8 issues --- authorizenet/cim.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/authorizenet/cim.py b/authorizenet/cim.py index 77c74d4..8b07f25 100644 --- a/authorizenet/cim.py +++ b/authorizenet/cim.py @@ -302,7 +302,8 @@ def get_response(self): self.endpoint, data=self.document.toxml().encode('utf-8'), headers={'Content-Type': 'text/xml'}) - response_xml = xml.dom.minidom.parseString(response.text) + text = response.text.encode('utf-8') + response_xml = xml.dom.minidom.parseString(text) self.process_response(response_xml) return self.create_response_object() From 9c27db46ec6e1bf59ae7afd0a4b84b1167cecbe7 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Mon, 24 Jun 2013 13:53:05 -0700 Subject: [PATCH 25/45] Remove unused forms import --- authorizenet/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index d2df946..2c5a089 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -2,7 +2,7 @@ from django.conf import settings from authorizenet.fields import CreditCardField, CreditCardExpiryField, \ CreditCardCVV2Field, CountryField -from authorizenet.models import CustomerProfile, CustomerPaymentProfile +from authorizenet.models import CustomerPaymentProfile class SIMPaymentForm(forms.Form): From 969cfc4b1ff65d531bd7341932b70d40994f6342 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 10:17:49 -0700 Subject: [PATCH 26/45] Use self-documenting app-specific settings object --- authorizenet/cim.py | 10 +++++----- authorizenet/conf.py | 37 +++++++++++++++++++++++++++++++++++++ authorizenet/fields.py | 5 ++--- authorizenet/forms.py | 6 +++--- authorizenet/helpers.py | 4 ++-- authorizenet/models.py | 6 +++--- authorizenet/utils.py | 20 ++++++++++---------- authorizenet/views.py | 6 +++--- 8 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 authorizenet/conf.py diff --git a/authorizenet/cim.py b/authorizenet/cim.py index 8b07f25..8090f83 100644 --- a/authorizenet/cim.py +++ b/authorizenet/cim.py @@ -2,7 +2,7 @@ import xml.dom.minidom from django.utils.datastructures import SortedDict -from django.conf import settings +from authorizenet.conf import settings import requests from authorizenet import AUTHNET_CIM_URL, AUTHNET_TEST_CIM_URL @@ -264,7 +264,7 @@ class BaseRequest(object): def __init__(self, action): self.create_base_document(action) - if settings.AUTHNET_DEBUG: + if settings.DEBUG: self.endpoint = AUTHNET_TEST_CIM_URL else: self.endpoint = AUTHNET_CIM_URL @@ -284,9 +284,9 @@ def create_base_document(self, action): self.document = doc authentication = doc.createElement("merchantAuthentication") - name = self.get_text_node("name", settings.AUTHNET_LOGIN_ID) + name = self.get_text_node("name", settings.LOGIN_ID) key = self.get_text_node("transactionKey", - settings.AUTHNET_TRANSACTION_KEY) + settings.TRANSACTION_KEY) authentication.appendChild(name) authentication.appendChild(key) root.appendChild(authentication) @@ -695,7 +695,7 @@ def __init__(self, if delimiter: self.delimiter = delimiter else: - self.delimiter = getattr(settings, 'AUTHNET_DELIM_CHAR', "|") + self.delimiter = settings.DELIM_CHAR self.add_transaction_node() self.add_extra_options() if order_info: diff --git a/authorizenet/conf.py b/authorizenet/conf.py new file mode 100644 index 0000000..d3b75f5 --- /dev/null +++ b/authorizenet/conf.py @@ -0,0 +1,37 @@ +"""Application-specific settings for django-authorizenet""" + +from django.conf import settings as defined_settings + + +class Settings(object): + + """ + Retrieves django.conf settings, using defaults from Default subclass + + All usable settings are specified in settings attribute. Use an + ``AUTHNET_`` prefix when specifying settings in django.conf. + """ + + prefix = 'AUTHNET_' + settings = {'DEBUG', 'LOGIN_ID', 'TRANSACTION_KEY', 'CUSTOMER_MODEL', + 'DELIM_CHAR', 'FORCE_TEST_REQUEST', 'EMAIL_CUSTOMER', + 'MD5_HASH'} + + class Default: + DELIM_CHAR = "|" + FORCE_TEST_REQUEST = False + EMAIL_CUSTOMER = None + MD5_HASH = "" + + def __init__(self): + self.defaults = Settings.Default() + + def __getattr__(self, name): + if name not in self.settings: + raise AttributeError("Setting %s not understood" % name) + try: + return getattr(defined_settings, self.prefix + name) + except AttributeError: + return getattr(self.defaults, name) + +settings = Settings() diff --git a/authorizenet/fields.py b/authorizenet/fields.py index 2fd4a63..e58de0d 100644 --- a/authorizenet/fields.py +++ b/authorizenet/fields.py @@ -4,9 +4,9 @@ from calendar import monthrange from django import forms -from django.conf import settings from django.utils.translation import ugettext as _ +from authorizenet.conf import settings from authorizenet.creditcard import verify_credit_card @@ -23,8 +23,7 @@ def clean(self, value): Raises a ValidationError if the card is not valid and stashes card type. """ - self.card_type = verify_credit_card(value, - allow_test=settings.AUTHNET_DEBUG) + self.card_type = verify_credit_card(value, allow_test=settings.DEBUG) if self.card_type is None: raise forms.ValidationError("Invalid credit card number.") return value diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 2c5a089..841c0c3 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -1,5 +1,5 @@ from django import forms -from django.conf import settings +from authorizenet.conf import settings from authorizenet.fields import CreditCardField, CreditCardExpiryField, \ CreditCardCVV2Field, CountryField from authorizenet.models import CustomerPaymentProfile @@ -9,7 +9,7 @@ class SIMPaymentForm(forms.Form): x_login = forms.CharField(max_length=20, required=True, widget=forms.HiddenInput, - initial=settings.AUTHNET_LOGIN_ID) + initial=settings.LOGIN_ID) x_type = forms.CharField(max_length=20, widget=forms.HiddenInput, initial="AUTH_CAPTURE") @@ -125,7 +125,7 @@ class HostedCIMProfileForm(forms.Form): def __init__(self, token, *args, **kwargs): super(HostedCIMProfileForm, self).__init__(*args, **kwargs) self.fields['token'].initial = token - if settings.AUTHNET_DEBUG: + if settings.DEBUG: self.action = "https://test.authorize.net/profile/manage" else: self.action = "https://secure.authorize.net/profile/manage" diff --git a/authorizenet/helpers.py b/authorizenet/helpers.py index ae63897..835af71 100644 --- a/authorizenet/helpers.py +++ b/authorizenet/helpers.py @@ -1,15 +1,15 @@ import re -from django.conf import settings import requests +from authorizenet.conf import settings from authorizenet import AUTHNET_POST_URL, AUTHNET_TEST_POST_URL class AIMPaymentHelper(object): def __init__(self, defaults): self.defaults = defaults - if settings.AUTHNET_DEBUG: + if settings.DEBUG: self.endpoint = AUTHNET_TEST_POST_URL else: self.endpoint = AUTHNET_POST_URL diff --git a/authorizenet/models.py b/authorizenet/models.py index f299e5f..13e3e93 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -1,7 +1,7 @@ from django.db import models from django.forms.models import model_to_dict -from django.conf import settings +from .conf import settings from .cim import add_profile, get_profile, update_payment_profile, \ create_payment_profile, delete_payment_profile @@ -209,7 +209,7 @@ class CustomerProfile(models.Model): """Authorize.NET customer profile""" - customer = models.OneToOneField(settings.AUTHNET_CUSTOMER_MODEL, + customer = models.OneToOneField(settings.CUSTOMER_MODEL, related_name='customer_profile') profile_id = models.CharField(max_length=50) @@ -232,7 +232,7 @@ class CustomerPaymentProfile(models.Model): """Authorize.NET customer payment profile""" - customer = models.ForeignKey(settings.AUTHNET_CUSTOMER_MODEL, + customer = models.ForeignKey(settings.CUSTOMER_MODEL, related_name='payment_profiles') customer_profile = models.ForeignKey('CustomerProfile', related_name='payment_profiles') diff --git a/authorizenet/utils.py b/authorizenet/utils.py index bc475d9..6b636ad 100644 --- a/authorizenet/utils.py +++ b/authorizenet/utils.py @@ -1,21 +1,21 @@ import hmac -from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from authorizenet.conf import settings from authorizenet.helpers import AIMPaymentHelper from authorizenet.models import Response from authorizenet.signals import payment_was_successful, payment_was_flagged def get_fingerprint(x_fp_sequence, x_fp_timestamp, x_amount): - msg = '^'.join([settings.AUTHNET_LOGIN_ID, + msg = '^'.join([settings.LOGIN_ID, x_fp_sequence, x_fp_timestamp, x_amount ]) + '^' - return hmac.new(settings.AUTHNET_TRANSACTION_KEY, msg).hexdigest() + return hmac.new(settings.TRANSACTION_KEY, msg).hexdigest() def extract_form_data(form_data): @@ -23,10 +23,10 @@ def extract_form_data(form_data): form_data.items())) AIM_DEFAULT_DICT = { - 'x_login': settings.AUTHNET_LOGIN_ID, - 'x_tran_key': settings.AUTHNET_TRANSACTION_KEY, + 'x_login': settings.LOGIN_ID, + 'x_tran_key': settings.TRANSACTION_KEY, 'x_delim_data': "TRUE", - 'x_delim_char': getattr(settings, 'AUTHNET_DELIM_CHAR', "|"), + 'x_delim_char': settings.DELIM_CHAR, 'x_relay_response': "FALSE", 'x_type': "AUTH_CAPTURE", 'x_method': "CC" @@ -48,10 +48,10 @@ def process_payment(form_data, extra_data): data = extract_form_data(form_data) data.update(extract_form_data(extra_data)) data['x_exp_date'] = data['x_exp_date'].strftime('%m%y') - if getattr(settings, 'AUTHNET_FORCE_TEST_REQUEST', False): + if settings.FORCE_TEST_REQUEST: data['x_test_request'] = 'TRUE' - if hasattr(settings, 'AUTHNET_EMAIL_CUSTOMER'): - data['x_email_customer'] = settings.AUTHNET_EMAIL_CUSTOMER + if settings.EMAIL_CUSTOMER is not None: + data['x_email_customer'] = settings.EMAIL_CUSTOMER return create_response(data) @@ -74,6 +74,6 @@ def capture_transaction(response, extra_data=None): if not data.get('x_amount', None): data['x_amount'] = response.amount data['x_type'] = 'PRIOR_AUTH_CAPTURE' - if getattr(settings, 'AUTHNET_FORCE_TEST_REQUEST', False): + if settings.FORCE_TEST_REQUEST: data['x_test_request'] = 'TRUE' return create_response(data) diff --git a/authorizenet/views.py b/authorizenet/views.py index 8eb61dc..19d1d4a 100644 --- a/authorizenet/views.py +++ b/authorizenet/views.py @@ -3,7 +3,7 @@ except ImportError: import md5 as hashlib -from django.conf import settings +from authorizenet.conf import settings from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from django.views.generic.edit import CreateView, UpdateView @@ -18,14 +18,14 @@ @csrf_exempt def sim_payment(request): response = Response.objects.create_from_dict(request.POST) - MD5_HASH = getattr(settings, "AUTHNET_MD5_HASH", "") + MD5_HASH = settings.MD5_HASH hash_is_valid = True #if MD5-Hash value is provided, use it to validate response if MD5_HASH: hash_is_valid = False hash_value = hashlib.md5(''.join([MD5_HASH, - settings.AUTHNET_LOGIN_ID, + settings.LOGIN_ID, response.trans_id, response.amount])).hexdigest() From 43c2045c2338096e2ee81ba660185bba5e3d1ad3 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 10:20:04 -0700 Subject: [PATCH 27/45] Set default for CUSTOMER_MODEL setting --- authorizenet/conf.py | 6 ++++-- runtests.py | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/authorizenet/conf.py b/authorizenet/conf.py index d3b75f5..0185a3b 100644 --- a/authorizenet/conf.py +++ b/authorizenet/conf.py @@ -1,6 +1,6 @@ """Application-specific settings for django-authorizenet""" -from django.conf import settings as defined_settings +from django.conf import settings as django_settings class Settings(object): @@ -18,6 +18,8 @@ class Settings(object): 'MD5_HASH'} class Default: + CUSTOMER_MODEL = getattr( + django_settings, 'AUTH_USER_MODEL', "auth.User") DELIM_CHAR = "|" FORCE_TEST_REQUEST = False EMAIL_CUSTOMER = None @@ -30,7 +32,7 @@ def __getattr__(self, name): if name not in self.settings: raise AttributeError("Setting %s not understood" % name) try: - return getattr(defined_settings, self.prefix + name) + return getattr(django_settings, self.prefix + name) except AttributeError: return getattr(self.defaults, name) diff --git a/runtests.py b/runtests.py index 3775761..89cb44f 100644 --- a/runtests.py +++ b/runtests.py @@ -10,7 +10,6 @@ AUTHNET_DEBUG=False, AUTHNET_LOGIN_ID="loginid", AUTHNET_TRANSACTION_KEY="key", - AUTHNET_CUSTOMER_MODEL='auth.User', INSTALLED_APPS=( 'django.contrib.contenttypes', 'django.contrib.auth', From ddf75bdcd46cb48bc26765f18748bf0c1e407e23 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 13:06:25 -0700 Subject: [PATCH 28/45] Allow customer profile without payment profile --- authorizenet/cim.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/authorizenet/cim.py b/authorizenet/cim.py index 8090f83..9a78fbf 100644 --- a/authorizenet/cim.py +++ b/authorizenet/cim.py @@ -47,8 +47,9 @@ def extract_form_data(data): def extract_payment_form_data(data): payment_data = extract_form_data(data) - payment_data['expirationDate'] = \ - payment_data['expirationDate'].strftime('%Y-%m') + if 'expirationDate' in payment_data: + payment_data['expirationDate'] = \ + payment_data['expirationDate'].strftime('%Y-%m') return payment_data From 6c30427643610281cd5a8388b177c18cb0416cc9 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 13:06:45 -0700 Subject: [PATCH 29/45] Add CIM utility to delete customer profiles --- authorizenet/cim.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/authorizenet/cim.py b/authorizenet/cim.py index 9a78fbf..107f1f1 100644 --- a/authorizenet/cim.py +++ b/authorizenet/cim.py @@ -113,6 +113,18 @@ def add_profile(customer_id, payment_form_data, billing_form_data, 'shipping_profile_ids': shipping_profile_ids} +def delete_profile(profile_id): + """ + Delete a customer profile and return the CIMResponse. + + Arguments: + profile_id -- unique gateway-assigned profile identifier + """ + helper = DeleteProfileRequest(profile_id) + response = helper.get_response() + return response + + def update_payment_profile(profile_id, payment_profile_id, payment_form_data, From 069551784959e0278890a79480b80df50ea5146b Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 13:08:15 -0700 Subject: [PATCH 30/45] Fix profile and payment profile ORM mapping --- authorizenet/managers.py | 24 ++++++++---------------- authorizenet/models.py | 29 ++++++++++++++++++++++++++--- tests/tests/views.py | 2 +- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/authorizenet/managers.py b/authorizenet/managers.py index 3f6f8f7..fae1d75 100644 --- a/authorizenet/managers.py +++ b/authorizenet/managers.py @@ -1,15 +1,12 @@ from django.db import models -from authorizenet.cim import add_profile - -from .exceptions import BillingError - class CustomerProfileManager(models.Manager): def create(self, **data): """Create new Authorize.NET customer profile""" + from .models import CustomerPaymentProfile kwargs = data @@ -19,27 +16,22 @@ def create(self, **data): 'profile_id': kwargs.pop('profile_id', None), } - # Create the customer profile with Authorize.NET CIM call - if sync: - output = add_profile(kwargs['customer'].pk, data, data) - if not output['response'].success: - raise BillingError("Error creating customer profile") - kwargs['profile_id'] = output['profile_id'] - - # Store customer profile data locally - instance = super(CustomerProfileManager, self).create(**kwargs) + # Create customer profile + obj = self.model(**kwargs) + self._for_write = True + obj.save(force_insert=True, using=self.db, sync=sync, data=data) if sync: # Store customer payment profile data locally - for payment_profile_id in output['payment_profile_ids']: + for payment_profile_id in obj.payment_profile_ids: CustomerPaymentProfile.objects.create( - customer_profile=instance, + customer_profile=obj, payment_profile_id=payment_profile_id, sync=False, **data ) - return instance + return obj class CustomerPaymentProfileManager(models.Manager): diff --git a/authorizenet/models.py b/authorizenet/models.py index 13e3e93..d594515 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -3,7 +3,7 @@ from .conf import settings from .cim import add_profile, get_profile, update_payment_profile, \ - create_payment_profile, delete_payment_profile + create_payment_profile, delete_profile, delete_payment_profile from .managers import CustomerProfileManager, CustomerPaymentProfileManager from .exceptions import BillingError @@ -213,6 +213,26 @@ class CustomerProfile(models.Model): related_name='customer_profile') profile_id = models.CharField(max_length=50) + def save(self, *args, **kwargs): + data = kwargs.pop('data', {}) + if not self.id and kwargs.pop('sync', True): + self.push_to_server(data) + super(CustomerProfile, self).save(*args, **kwargs) + + def delete(self): + """Delete the customer profile remotely and locally""" + response = delete_profile(self.profile_id) + if not response.success: + raise BillingError("Error deleting customer profile") + super(CustomerProfile, self).delete() + + def push_to_server(self, data): + output = add_profile(self.customer.pk, data, data) + if not output['response'].success: + raise BillingError("Error creating customer profile") + self.profile_id = output['profile_id'] + self.payment_profile_ids = output['payment_profile_ids'] + def sync(self): """Overwrite local customer profile data with remote data""" response, payment_profiles = get_profile(self.profile_id) @@ -320,8 +340,11 @@ def sync(self, data): def delete(self): """Delete the customer payment profile remotely and locally""" - delete_payment_profile(self.customer_profile.profile_id, - self.payment_profile_id) + response = delete_payment_profile(self.customer_profile.profile_id, + self.payment_profile_id) + if not response.success: + raise BillingError("Error deleting customer payment profile") + return super(CustomerPaymentProfile, self).delete() def update(self, **data): """Update the customer payment profile remotely and locally""" diff --git a/tests/tests/views.py b/tests/tests/views.py index fc50886..6e76e74 100644 --- a/tests/tests/views.py +++ b/tests/tests/views.py @@ -81,7 +81,7 @@ class PaymentProfileUpdateTests(LiveServerTestCase): def setUp(self): self.user = create_user(id=42, username='billy', password='password') profile = CustomerProfile(customer=self.user, profile_id='6666') - profile.save() + profile.save(sync=False) self.payment_profile = CustomerPaymentProfile( customer=self.user, customer_profile=profile, From 1d09754629e039001ecf008a372178177bd01324 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 13:08:49 -0700 Subject: [PATCH 31/45] Add customer profile and payment profile to admin --- authorizenet/admin.py | 33 ++++++++++++++++++++++++++++++++- authorizenet/forms.py | 13 ++++++++++--- authorizenet/models.py | 5 ++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/authorizenet/admin.py b/authorizenet/admin.py index cb3c5fb..0518b5b 100644 --- a/authorizenet/admin.py +++ b/authorizenet/admin.py @@ -1,7 +1,9 @@ from django.contrib import admin from django.core.urlresolvers import reverse from django.utils.safestring import mark_safe -from authorizenet.models import Response, CIMResponse +from authorizenet.models import (Response, CIMResponse, CustomerProfile, + CustomerPaymentProfile) +from authorizenet.forms import CustomerPaymentForm, CustomerPaymentAdminForm class ResponseAdmin(admin.ModelAdmin): @@ -78,3 +80,32 @@ def response_link(self, obj): response_link.short_description = 'transaction response' admin.site.register(CIMResponse, CIMResponseAdmin) + + +class CustomerPaymentProfileInline(admin.StackedInline): + model = CustomerPaymentProfile + extra = 0 + max_num = 0 + form = CustomerPaymentForm + + +class CustomerProfileAdmin(admin.ModelAdmin): + list_display = ['profile_id', 'customer'] + readonly_fields = ['profile_id', 'customer'] + inlines = [CustomerPaymentProfileInline] + + def get_readonly_fields(self, request, obj=None): + return self.readonly_fields if obj is not None else [] + +admin.site.register(CustomerProfile, CustomerProfileAdmin) + + +class CustomerPaymentProfileAdmin(admin.ModelAdmin): + list_display = ['payment_profile_id', 'customer_profile', 'customer'] + readonly_fields = ['payment_profile_id', 'customer', 'customer_profile'] + form = CustomerPaymentAdminForm + + def get_readonly_fields(self, request, obj=None): + return self.readonly_fields if obj is not None else [] + +admin.site.register(CustomerPaymentProfile, CustomerPaymentProfileAdmin) diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 841c0c3..86d18a8 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -105,12 +105,14 @@ def __init__(self, *args, **kwargs): self.customer = kwargs.pop('customer', None) return super(CustomerPaymentForm, self).__init__(*args, **kwargs) - def save(self): + def save(self, commit=True): instance = super(CustomerPaymentForm, self).save(commit=False) - instance.customer = self.customer + if self.customer: + instance.customer = self.customer instance.expiration_date = self.cleaned_data['expiration_date'] instance.card_code = self.cleaned_data.get('card_code') - instance.save() + if commit: + instance.save() return instance class Meta: @@ -120,6 +122,11 @@ class Meta: 'expiration_date', 'card_code') +class CustomerPaymentAdminForm(CustomerPaymentForm): + class Meta(CustomerPaymentForm.Meta): + fields = ('customer',) + CustomerPaymentForm.Meta.fields + + class HostedCIMProfileForm(forms.Form): token = forms.CharField(widget=forms.HiddenInput) def __init__(self, token, *args, **kwargs): diff --git a/authorizenet/models.py b/authorizenet/models.py index d594515..3dd5736 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -247,6 +247,9 @@ def sync(self): objects = CustomerProfileManager() + def __unicode__(self): + return self.profile_id + class CustomerPaymentProfile(models.Model): @@ -354,6 +357,6 @@ def update(self, **data): return self def __unicode__(self): - return self.card_number + return self.payment_profile_id objects = CustomerPaymentProfileManager() From 29c7e05a02366362db1801636e2b71e042ea6461 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 13:52:34 -0700 Subject: [PATCH 32/45] Fix set notation for Python 2.6 --- authorizenet/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authorizenet/conf.py b/authorizenet/conf.py index 0185a3b..45d982a 100644 --- a/authorizenet/conf.py +++ b/authorizenet/conf.py @@ -13,9 +13,9 @@ class Settings(object): """ prefix = 'AUTHNET_' - settings = {'DEBUG', 'LOGIN_ID', 'TRANSACTION_KEY', 'CUSTOMER_MODEL', - 'DELIM_CHAR', 'FORCE_TEST_REQUEST', 'EMAIL_CUSTOMER', - 'MD5_HASH'} + settings = set(('DEBUG', 'LOGIN_ID', 'TRANSACTION_KEY', 'CUSTOMER_MODEL', + 'DELIM_CHAR', 'FORCE_TEST_REQUEST', 'EMAIL_CUSTOMER', + 'MD5_HASH')) class Default: CUSTOMER_MODEL = getattr( From 65905808f3acb3c62b9d066cef48c8e38e9cf1e2 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 14:13:52 -0700 Subject: [PATCH 33/45] Fix short-circuit evaluation in if statement --- authorizenet/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/authorizenet/models.py b/authorizenet/models.py index 3dd5736..1d9c251 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -215,7 +215,8 @@ class CustomerProfile(models.Model): def save(self, *args, **kwargs): data = kwargs.pop('data', {}) - if not self.id and kwargs.pop('sync', True): + sync = kwargs.pop('sync', True) + if not self.id and sync: self.push_to_server(data) super(CustomerProfile, self).save(*args, **kwargs) From 8d34e30ed0fe49772c1345442540399a47151da7 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Tue, 25 Jun 2013 15:03:57 -0700 Subject: [PATCH 34/45] Add some tests for CustomerProfile model --- tests/tests/__init__.py | 1 + tests/tests/mocks.py | 11 ++++++ tests/tests/models.py | 77 ++++++++++++++++++++++++++++++++++++++++ tests/tests/test_data.py | 26 ++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 tests/tests/models.py diff --git a/tests/tests/__init__.py b/tests/tests/__init__.py index 6ecfb67..75dda46 100644 --- a/tests/tests/__init__.py +++ b/tests/tests/__init__.py @@ -1,2 +1,3 @@ from .cim import * from .views import * +from .models import * diff --git a/tests/tests/mocks.py b/tests/tests/mocks.py index 632c1e7..db518a6 100644 --- a/tests/tests/mocks.py +++ b/tests/tests/mocks.py @@ -5,6 +5,17 @@ path=r'^/xml/v1/request\.api$') +delete_success = ( + '' + '<{0}>' + '' + 'Ok' + 'I00001Successful.' + '' + '' +) + + customer_profile_success = ( '' '<{0}>' diff --git a/tests/tests/models.py b/tests/tests/models.py new file mode 100644 index 0000000..32aa548 --- /dev/null +++ b/tests/tests/models.py @@ -0,0 +1,77 @@ +from httmock import HTTMock, with_httmock +from xml.dom.minidom import parseString +from django.test import TestCase +from authorizenet.models import CustomerProfile + +from .utils import create_user, xml_to_dict +from .mocks import cim_url_match, customer_profile_success, delete_success +from .test_data import create_empty_profile_success, delete_profile_success + + +class RequestError(Exception): + pass + + +def error_on_request(url, request): + raise RequestError("CIM Request") + + +class CustomerProfileModelTests(TestCase): + + """Tests for CustomerProfile model""" + + def setUp(self): + self.user = create_user(id=42, username='billy', password='password') + + def create_profile(self): + return CustomerProfile.objects.create( + customer=self.user, profile_id='6666', sync=False) + + def test_create_sync_no_data(self): + @cim_url_match + def request_handler(url, request): + request_xml = parseString(request.body) + self.assertEqual(xml_to_dict(request_xml), + create_empty_profile_success) + return customer_profile_success.format( + 'createCustomerProfileResponse') + profile = CustomerProfile(customer=self.user) + with HTTMock(error_on_request): + self.assertRaises(RequestError, profile.save) + self.assertEqual(profile.profile_id, '') + with HTTMock(request_handler): + profile.save(sync=True) + self.assertEqual(profile.profile_id, '6666') + + @with_httmock(error_on_request) + def test_create_no_sync(self): + profile = CustomerProfile(customer=self.user) + profile.save(sync=False) + self.assertEqual(profile.profile_id, '') + + @with_httmock(error_on_request) + def test_edit(self): + profile = self.create_profile() + self.assertEqual(profile.profile_id, '6666') + profile.profile_id = '7777' + profile.save() + self.assertEqual(profile.profile_id, '7777') + profile.profile_id = '8888' + profile.save(sync=True) + self.assertEqual(profile.profile_id, '8888') + profile.profile_id = '9999' + profile.save(sync=False) + self.assertEqual(profile.profile_id, '9999') + + def test_delete(self): + @cim_url_match + def request_handler(url, request): + request_xml = parseString(request.body) + self.assertEqual(xml_to_dict(request_xml), + delete_profile_success) + return delete_success.format( + 'deleteCustomerProfileResponse') + profile = self.create_profile() + with HTTMock(request_handler): + profile.delete() + self.assertEqual(profile.__class__.objects.count(), 0) diff --git a/tests/tests/test_data.py b/tests/tests/test_data.py index b301b0d..bec7753 100644 --- a/tests/tests/test_data.py +++ b/tests/tests/test_data.py @@ -1,3 +1,17 @@ +create_empty_profile_success = { + 'createCustomerProfileRequest': { + 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', + 'profile': { + 'merchantCustomerId': '42', + }, + 'merchantAuthentication': { + 'transactionKey': 'key', + 'name': 'loginid', + }, + } +} + + create_profile_success = { 'createCustomerProfileRequest': { 'xmlns': 'AnetApi/xml/v1/schema/AnetApiSchema.xsd', @@ -98,3 +112,15 @@ }, } } + + +delete_profile_success = { + 'deleteCustomerProfileRequest': { + 'xmlns': u'AnetApi/xml/v1/schema/AnetApiSchema.xsd', + 'customerProfileId': '6666', + 'merchantAuthentication': { + 'transactionKey': 'key', + 'name': 'loginid' + }, + }, +} From 3d00d25a24c4b4feb3bf4db6c9a58fb65dad778f Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 26 Jun 2013 10:02:26 -0700 Subject: [PATCH 35/45] Don't allow setting profile ID in admin --- authorizenet/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizenet/admin.py b/authorizenet/admin.py index 0518b5b..2b327b3 100644 --- a/authorizenet/admin.py +++ b/authorizenet/admin.py @@ -95,7 +95,7 @@ class CustomerProfileAdmin(admin.ModelAdmin): inlines = [CustomerPaymentProfileInline] def get_readonly_fields(self, request, obj=None): - return self.readonly_fields if obj is not None else [] + return self.readonly_fields if obj is not None else ['profile_id'] admin.site.register(CustomerProfile, CustomerProfileAdmin) From c8a520078d87874323a74f3e45b43018bd77700b Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 26 Jun 2013 10:03:14 -0700 Subject: [PATCH 36/45] Allow null expiration date in cim data --- authorizenet/cim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizenet/cim.py b/authorizenet/cim.py index 107f1f1..07ecae6 100644 --- a/authorizenet/cim.py +++ b/authorizenet/cim.py @@ -47,7 +47,7 @@ def extract_form_data(data): def extract_payment_form_data(data): payment_data = extract_form_data(data) - if 'expirationDate' in payment_data: + if payment_data.get('expirationDate') is not None: payment_data['expirationDate'] = \ payment_data['expirationDate'].strftime('%Y-%m') return payment_data From 81d3d8b5c6f0c052d9a4474cc1c12e0316955bd5 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 26 Jun 2013 10:08:23 -0700 Subject: [PATCH 37/45] Simplify CIM exception raising --- authorizenet/exceptions.py | 1 - authorizenet/models.py | 19 +++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/authorizenet/exceptions.py b/authorizenet/exceptions.py index 26ecafe..a572ba7 100644 --- a/authorizenet/exceptions.py +++ b/authorizenet/exceptions.py @@ -1,3 +1,2 @@ class BillingError(Exception): - """Error due to Authorize.NET request""" diff --git a/authorizenet/models.py b/authorizenet/models.py index 1d9c251..d8be5ea 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -204,6 +204,10 @@ class CIMResponse(models.Model): def success(self): return self.result == 'Ok' + def raise_if_error(self): + if not self.success: + raise BillingError(self.result_text) + class CustomerProfile(models.Model): @@ -223,22 +227,19 @@ def save(self, *args, **kwargs): def delete(self): """Delete the customer profile remotely and locally""" response = delete_profile(self.profile_id) - if not response.success: - raise BillingError("Error deleting customer profile") + response.raise_if_error() super(CustomerProfile, self).delete() def push_to_server(self, data): output = add_profile(self.customer.pk, data, data) - if not output['response'].success: - raise BillingError("Error creating customer profile") + output['response'].raise_if_error() self.profile_id = output['profile_id'] self.payment_profile_ids = output['payment_profile_ids'] def sync(self): """Overwrite local customer profile data with remote data""" response, payment_profiles = get_profile(self.profile_id) - if not response.success: - raise BillingError("Error syncing remote customer profile") + response.raise_if_error() for payment_profile in payment_profiles: instance, created = CustomerPaymentProfile.objects.get_or_create( customer_profile=self, @@ -323,8 +324,7 @@ def push_to_server(self): sync=False, ) self.payment_profile_id = output['payment_profile_ids'][0] - if not response.success: - raise BillingError() + response.raise_if_error() @property def raw_data(self): @@ -346,8 +346,7 @@ def delete(self): """Delete the customer payment profile remotely and locally""" response = delete_payment_profile(self.customer_profile.profile_id, self.payment_profile_id) - if not response.success: - raise BillingError("Error deleting customer payment profile") + response.raise_if_error() return super(CustomerPaymentProfile, self).delete() def update(self, **data): From b22fa5ef342257235f73dc391bc1cc74ebb3f99c Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 26 Jun 2013 10:09:26 -0700 Subject: [PATCH 38/45] Fix min_length for CVV field --- authorizenet/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/authorizenet/fields.py b/authorizenet/fields.py index e58de0d..d0b5f7b 100644 --- a/authorizenet/fields.py +++ b/authorizenet/fields.py @@ -97,6 +97,7 @@ def compress(self, data_list): class CreditCardCVV2Field(forms.CharField): def __init__(self, *args, **kwargs): + kwargs.setdefault('min_length', 3) kwargs.setdefault('max_length', 4) super(CreditCardCVV2Field, self).__init__(*args, **kwargs) From 5d554e695ca08022bcaa0de362f1dd8788a31d55 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 26 Jun 2013 11:09:17 -0700 Subject: [PATCH 39/45] Fix sync methods --- authorizenet/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/authorizenet/models.py b/authorizenet/models.py index d8be5ea..7e8941e 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -238,9 +238,9 @@ def push_to_server(self, data): def sync(self): """Overwrite local customer profile data with remote data""" - response, payment_profiles = get_profile(self.profile_id) - response.raise_if_error() - for payment_profile in payment_profiles: + output = get_profile(self.profile_id) + output['response'].raise_if_error() + for payment_profile in output['payment_profiles']: instance, created = CustomerPaymentProfile.objects.get_or_create( customer_profile=self, payment_profile_id=payment_profile['payment_profile_id'] @@ -340,7 +340,7 @@ def sync(self, data): setattr(self, k, v) self.card_number = data.get('credit_card', {}).get('card_number', self.card_number) - self.save() + self.save(sync=False) def delete(self): """Delete the customer payment profile remotely and locally""" From d52a3ec25c1c69f58e8202a0185ba703538d89d0 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 26 Jun 2013 11:10:16 -0700 Subject: [PATCH 40/45] Store expiration date in database --- authorizenet/forms.py | 1 - ...e__add_customerprofile__chg_field_cimre.py | 220 ++++++++++++++++++ authorizenet/models.py | 7 +- tests/tests/views.py | 3 +- 4 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 authorizenet/migrations/0005_auto__add_customerpaymentprofile__add_customerprofile__chg_field_cimre.py diff --git a/authorizenet/forms.py b/authorizenet/forms.py index 86d18a8..61c347d 100644 --- a/authorizenet/forms.py +++ b/authorizenet/forms.py @@ -109,7 +109,6 @@ def save(self, commit=True): instance = super(CustomerPaymentForm, self).save(commit=False) if self.customer: instance.customer = self.customer - instance.expiration_date = self.cleaned_data['expiration_date'] instance.card_code = self.cleaned_data.get('card_code') if commit: instance.save() diff --git a/authorizenet/migrations/0005_auto__add_customerpaymentprofile__add_customerprofile__chg_field_cimre.py b/authorizenet/migrations/0005_auto__add_customerpaymentprofile__add_customerprofile__chg_field_cimre.py new file mode 100644 index 0000000..1bfae2d --- /dev/null +++ b/authorizenet/migrations/0005_auto__add_customerpaymentprofile__add_customerprofile__chg_field_cimre.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CustomerPaymentProfile' + db.create_table(u'authorizenet_customerpaymentprofile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('customer', self.gf('django.db.models.fields.related.ForeignKey')(related_name='payment_profiles', to=orm['doctors.Practice'])), + ('customer_profile', self.gf('django.db.models.fields.related.ForeignKey')(related_name='payment_profiles', to=orm['authorizenet.CustomerProfile'])), + ('payment_profile_id', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('first_name', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)), + ('last_name', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)), + ('company', self.gf('django.db.models.fields.CharField')(max_length=60, blank=True)), + ('phone_number', self.gf('django.db.models.fields.CharField')(max_length=25, blank=True)), + ('fax_number', self.gf('django.db.models.fields.CharField')(max_length=25, blank=True)), + ('address', self.gf('django.db.models.fields.CharField')(max_length=60, blank=True)), + ('city', self.gf('django.db.models.fields.CharField')(max_length=40, blank=True)), + ('state', self.gf('django.db.models.fields.CharField')(max_length=40, blank=True)), + ('zip', self.gf('django.db.models.fields.CharField')(max_length=20, blank=True)), + ('country', self.gf('django.db.models.fields.CharField')(max_length=60, blank=True)), + ('card_number', self.gf('django.db.models.fields.CharField')(max_length=16, blank=True)), + ('expiration_date', self.gf('django.db.models.fields.DateField')(null=True, blank=True)), + )) + db.send_create_signal(u'authorizenet', ['CustomerPaymentProfile']) + + # Adding model 'CustomerProfile' + db.create_table(u'authorizenet_customerprofile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('customer', self.gf('django.db.models.fields.related.OneToOneField')(related_name='customer_profile', unique=True, to=orm['doctors.Practice'])), + ('profile_id', self.gf('django.db.models.fields.CharField')(max_length=50)), + )) + db.send_create_signal(u'authorizenet', ['CustomerProfile']) + + + # Changing field 'CIMResponse.result_text' + db.alter_column(u'authorizenet_cimresponse', 'result_text', self.gf('django.db.models.fields.TextField')()) + + def backwards(self, orm): + # Deleting model 'CustomerPaymentProfile' + db.delete_table(u'authorizenet_customerpaymentprofile') + + # Deleting model 'CustomerProfile' + db.delete_table(u'authorizenet_customerprofile') + + + # Changing field 'CIMResponse.result_text' + db.alter_column(u'authorizenet_cimresponse', 'result_text', self.gf('django.db.models.fields.TextField')(max_length=1023)) + + models = { + u'authorizenet.cimresponse': { + 'Meta': {'object_name': 'CIMResponse'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'result': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'result_code': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'result_text': ('django.db.models.fields.TextField', [], {}), + 'transaction_response': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['authorizenet.Response']", 'null': 'True', 'blank': 'True'}) + }, + u'authorizenet.customerpaymentprofile': { + 'Meta': {'object_name': 'CustomerPaymentProfile'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'card_number': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'company': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'customer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payment_profiles'", 'to': u"orm['doctors.Practice']"}), + 'customer_profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'payment_profiles'", 'to': u"orm['authorizenet.CustomerProfile']"}), + 'expiration_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'fax_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'payment_profile_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'phone_number': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'zip': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}) + }, + u'authorizenet.customerprofile': { + 'Meta': {'object_name': 'CustomerProfile'}, + 'customer': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'customer_profile'", 'unique': 'True', 'to': u"orm['doctors.Practice']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'profile_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'authorizenet.response': { + 'MD5_Hash': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'Meta': {'object_name': 'Response'}, + 'account_number': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '60'}), + 'amount': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'auth_code': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'avs_code': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'card_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}), + 'cavv_response': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'company': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '60'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}), + 'cust_id': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'cvv2_resp_code': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'duty': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '25'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'freight': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'invoice_num': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'method': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '25'}), + 'po_num': ('django.db.models.fields.CharField', [], {'max_length': '25', 'blank': 'True'}), + 'response_code': ('django.db.models.fields.CharField', [], {'max_length': '2'}), + 'response_reason_code': ('django.db.models.fields.CharField', [], {'max_length': '15'}), + 'response_reason_text': ('django.db.models.fields.TextField', [], {}), + 'response_subcode': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'ship_to_address': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'ship_to_city': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'ship_to_company': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'ship_to_country': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'ship_to_first_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'ship_to_last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'ship_to_state': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'ship_to_zip': ('django.db.models.fields.CharField', [], {'max_length': '20', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'tax': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'tax_exempt': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'test_request': ('django.db.models.fields.CharField', [], {'default': "'FALSE'", 'max_length': '10', 'blank': 'True'}), + 'trans_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '20', 'db_index': 'True'}), + 'zip': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + u'base.address': { + 'Meta': {'object_name': 'Address'}, + 'address1': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'address2': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'state': ('django_localflavor_us.models.USStateField', [], {'max_length': '2', 'blank': 'True'}), + 'zip': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}) + }, + u'doctors.employeetype': { + 'Meta': {'object_name': 'EmployeeType'}, + 'has_profile': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_doctor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_schedulable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}) + }, + u'doctors.practice': { + 'Meta': {'object_name': 'Practice'}, + 'accepted_insurance': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['medical.InsurancePlan']", 'symmetrical': 'False', 'blank': 'True'}), + 'addresses': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['base.Address']", 'through': u"orm['doctors.PracticeAddress']", 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'phone': ('base.fields.PhoneNumberField', [], {'max_length': '20'}), + 'practice_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doctors.PracticeType']"}), + 'statement': ('django.db.models.fields.TextField', [], {'max_length': '5000', 'blank': 'True'}), + 'timezone': ('timezone_field.fields.TimeZoneField', [], {}) + }, + u'doctors.practiceaddress': { + 'Meta': {'object_name': 'PracticeAddress'}, + 'address': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['base.Address']", 'unique': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'practice': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doctors.Practice']"}) + }, + u'doctors.practiceemployeetype': { + 'Meta': {'ordering': "['order']", 'object_name': 'PracticeEmployeeType', 'db_table': "'doctors_practicetype_employee_types'"}, + 'employeetype': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doctors.EmployeeType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'practicetype': ('adminsortable.fields.SortableForeignKey', [], {'to': u"orm['doctors.PracticeType']"}) + }, + u'doctors.practicetype': { + 'Meta': {'ordering': "['order']", 'object_name': 'PracticeType'}, + 'employee_types': ('sortedm2m.fields.SortedManyToManyField', [], {'to': u"orm['doctors.EmployeeType']", 'through': u"orm['doctors.PracticeEmployeeType']", 'symmetrical': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'specialties': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['medical.DoctorSpecialty']", 'symmetrical': 'False'}) + }, + u'medical.appointmenttype': { + 'Meta': {'ordering': "['order']", 'object_name': 'AppointmentType'}, + 'category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['medical.BillingCategory']", 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}), + 'specialty': ('adminsortable.fields.SortableForeignKey', [], {'related_name': "'appointment_types'", 'to': u"orm['medical.DoctorSpecialty']"}) + }, + u'medical.billingcategory': { + 'Meta': {'object_name': 'BillingCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'medical.doctorspecialty': { + 'Meta': {'ordering': "['order']", 'object_name': 'DoctorSpecialty'}, + 'default_appointment_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['medical.AppointmentType']", 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'}) + }, + u'medical.insuranceplan': { + 'Meta': {'ordering': "['provider__name', 'name']", 'object_name': 'InsurancePlan'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'provider': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'plans'", 'to': u"orm['medical.InsuranceProvider']"}) + }, + u'medical.insuranceprovider': { + 'Meta': {'ordering': "['name']", 'object_name': 'InsuranceProvider'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['authorizenet'] \ No newline at end of file diff --git a/authorizenet/models.py b/authorizenet/models.py index 7e8941e..2a37e3f 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -273,18 +273,16 @@ class CustomerPaymentProfile(models.Model): zip = models.CharField(max_length=20, blank=True, verbose_name="ZIP") country = models.CharField(max_length=60, blank=True) card_number = models.CharField(max_length=16, blank=True) - expiration_date = None + expiration_date = models.DateField(blank=True, null=True) card_code = None def __init__(self, *args, **kwargs): self.card_code = kwargs.pop('card_code', None) - self.expiration_date = kwargs.pop('expiration_date', None) return super(CustomerPaymentProfile, self).__init__(*args, **kwargs) def save(self, *args, **kwargs): if kwargs.pop('sync', True): self.push_to_server() - self.expiration_date = None self.card_code = None self.card_number = "XXXX%s" % self.card_number[-4:] super(CustomerPaymentProfile, self).save(*args, **kwargs) @@ -330,8 +328,7 @@ def push_to_server(self): def raw_data(self): """Return data suitable for use in payment and billing forms""" data = model_to_dict(self) - data.update(dict((k, getattr(self, k)) - for k in ('expiration_date', 'card_code'))) + data['card_code'] = getattr(self, 'card_code') return data def sync(self, data): diff --git a/tests/tests/views.py b/tests/tests/views.py index 6e76e74..a3ebdfe 100644 --- a/tests/tests/views.py +++ b/tests/tests/views.py @@ -1,3 +1,4 @@ +from datetime import date from django.test import LiveServerTestCase from xml.dom.minidom import parseString from httmock import HTTMock @@ -131,7 +132,7 @@ def create_customer_success(url, request): 'customer': self.user.id, 'payment_profile_id': '7777', 'card_number': 'XXXX1747', - 'expiration_date': None, + 'expiration_date': date(2020, 5, 31), 'card_code': None, 'first_name': 'Danielle', 'last_name': 'Thompson', From 610d70d51fdbdb5fc992e448cd841fa3beb018be Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 26 Jun 2013 11:47:59 -0700 Subject: [PATCH 41/45] Remove unecessary model manager --- authorizenet/managers.py | 11 ----------- authorizenet/models.py | 4 +--- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/authorizenet/managers.py b/authorizenet/managers.py index fae1d75..384d92d 100644 --- a/authorizenet/managers.py +++ b/authorizenet/managers.py @@ -32,14 +32,3 @@ def create(self, **data): ) return obj - - -class CustomerPaymentProfileManager(models.Manager): - - def create(self, **kwargs): - """Create new Authorize.NET customer payment profile""" - sync = kwargs.pop('sync', True) - obj = self.model(**kwargs) - self._for_write = True - obj.save(force_insert=True, using=self.db, sync=sync) - return obj diff --git a/authorizenet/models.py b/authorizenet/models.py index 2a37e3f..5f92979 100644 --- a/authorizenet/models.py +++ b/authorizenet/models.py @@ -5,7 +5,7 @@ from .cim import add_profile, get_profile, update_payment_profile, \ create_payment_profile, delete_profile, delete_payment_profile -from .managers import CustomerProfileManager, CustomerPaymentProfileManager +from .managers import CustomerProfileManager from .exceptions import BillingError @@ -355,5 +355,3 @@ def update(self, **data): def __unicode__(self): return self.payment_profile_id - - objects = CustomerPaymentProfileManager() From 744c9970bf6470f6540eb1a7d00b8e6b79a21c5e Mon Sep 17 00:00:00 2001 From: Andrii Kurinnyi Date: Wed, 26 Jun 2013 16:49:28 -0700 Subject: [PATCH 42/45] Updated sample project to work with Django 1.5 --- sample_project/requirements.txt | 1 + sample_project/settings.py | 4 +++- sample_project/urls.py | 7 +++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sample_project/requirements.txt b/sample_project/requirements.txt index f61efdb..0cfbb20 100644 --- a/sample_project/requirements.txt +++ b/sample_project/requirements.txt @@ -1 +1,2 @@ django>=1.5 +requests>=1.2.3 diff --git a/sample_project/settings.py b/sample_project/settings.py index f29affd..75ef751 100644 --- a/sample_project/settings.py +++ b/sample_project/settings.py @@ -60,7 +60,7 @@ # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/admin-media/' +STATIC_URL = '/static/' # Make this unique, and don't share it with anybody. SECRET_KEY = '=21(@m#)-$5r(cc110zpy$v4od_45r!k1nz!uq@v$w17&!i8=%' @@ -83,6 +83,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', @@ -104,6 +105,7 @@ 'django.contrib.sites', 'django.contrib.admin', 'django.contrib.redirects', + 'django.contrib.staticfiles', 'authorizenet', 'samplestore', ) diff --git a/sample_project/urls.py b/sample_project/urls.py index 5a47df5..30b1476 100644 --- a/sample_project/urls.py +++ b/sample_project/urls.py @@ -1,6 +1,7 @@ -from django.conf.urls.defaults import url, include, patterns +from django.conf.urls import url, include, patterns from django.conf import settings from django.views.generic.base import RedirectView +from django.contrib.staticfiles.urls import staticfiles_urlpatterns # Uncomment the next two lines to enable the admin: from django.contrib import admin @@ -16,6 +17,8 @@ url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', include(admin.site.urls)), - + url(r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}), ) + +urlpatterns += staticfiles_urlpatterns() From e24ab03fa3cfb9c76ad30208a73e9bbafef9377c Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 27 Jun 2013 00:45:34 -0700 Subject: [PATCH 43/45] Use django-relatives to link between admin pages --- authorizenet/admin.py | 9 +++++++-- .../templates/admin/authorizenet/change_form.html | 7 +++++++ setup.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 authorizenet/templates/admin/authorizenet/change_form.html diff --git a/authorizenet/admin.py b/authorizenet/admin.py index 2b327b3..f1d6e0a 100644 --- a/authorizenet/admin.py +++ b/authorizenet/admin.py @@ -4,6 +4,7 @@ from authorizenet.models import (Response, CIMResponse, CustomerProfile, CustomerPaymentProfile) from authorizenet.forms import CustomerPaymentForm, CustomerPaymentAdminForm +from relatives.utils import object_edit_link class ResponseAdmin(admin.ModelAdmin): @@ -82,11 +83,15 @@ def response_link(self, obj): admin.site.register(CIMResponse, CIMResponseAdmin) -class CustomerPaymentProfileInline(admin.StackedInline): +class CustomerPaymentProfileInline(admin.TabularInline): model = CustomerPaymentProfile + form = CustomerPaymentForm + fields = [object_edit_link("Edit"), 'first_name', 'last_name', + 'card_number', 'expiration_date'] + readonly_fields = fields extra = 0 max_num = 0 - form = CustomerPaymentForm + can_delete = False class CustomerProfileAdmin(admin.ModelAdmin): diff --git a/authorizenet/templates/admin/authorizenet/change_form.html b/authorizenet/templates/admin/authorizenet/change_form.html new file mode 100644 index 0000000..3914eb4 --- /dev/null +++ b/authorizenet/templates/admin/authorizenet/change_form.html @@ -0,0 +1,7 @@ +{% extends "admin/change_form.html" %} + +{% block field_sets %} +{% for fieldset in adminform %} + {% include "relatives/includes/fieldset.html" %} +{% endfor %} +{% endblock %} diff --git a/setup.py b/setup.py index b9a5200..12b6c00 100644 --- a/setup.py +++ b/setup.py @@ -25,4 +25,4 @@ ).read().strip(), test_suite='runtests.runtests', tests_require=['httmock'], - install_requires=['requests', 'django>=1.4']) + install_requires=['requests', 'django>=1.4', 'django-relatives>=0.2.0']) From 5ef94fa572856671e395cba650dff637911ca182 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 27 Jun 2013 01:06:53 -0700 Subject: [PATCH 44/45] Hide save buttons from customer profile page --- .../admin/authorizenet/customerprofile/change_form.html | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 authorizenet/templates/admin/authorizenet/customerprofile/change_form.html diff --git a/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html b/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html new file mode 100644 index 0000000..f0b8ce5 --- /dev/null +++ b/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html @@ -0,0 +1,4 @@ +{% extends "admin/authorizenet/change_form.html" %} + +{% block submit_buttons_bottom %}{% if not change %}{{ block.super }}{% endif %}{% endblock %} +{% block submit_buttons_top %}{% if not change %}{{ block.super }}{% endif %}{% endblock %} From 51c614fb3c4e4ac2b0de76a137cd9e4ba9d1261a Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Thu, 27 Jun 2013 01:07:50 -0700 Subject: [PATCH 45/45] Add "add payment profile" link to customer profile --- .../admin/authorizenet/customerprofile/change_form.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html b/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html index f0b8ce5..8790e8d 100644 --- a/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html +++ b/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html @@ -1,4 +1,13 @@ {% extends "admin/authorizenet/change_form.html" %} +{% load url from future %} {% block submit_buttons_bottom %}{% if not change %}{{ block.super }}{% endif %}{% endblock %} {% block submit_buttons_top %}{% if not change %}{{ block.super }}{% endif %}{% endblock %} + +{% block inline_field_sets %} +{{ block.super }} +{% if change %} + + Add payment profile
+{% endif %} +{% endblock %}