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/authorizenet/admin.py b/authorizenet/admin.py
index cb3c5fb..f1d6e0a 100644
--- a/authorizenet/admin.py
+++ b/authorizenet/admin.py
@@ -1,7 +1,10 @@
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
+from relatives.utils import object_edit_link
class ResponseAdmin(admin.ModelAdmin):
@@ -78,3 +81,36 @@ def response_link(self, obj):
response_link.short_description = 'transaction response'
admin.site.register(CIMResponse, CIMResponseAdmin)
+
+
+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
+ can_delete = False
+
+
+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 ['profile_id']
+
+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/cim.py b/authorizenet/cim.py
index 0da2a65..07ecae6 100644
--- a/authorizenet/cim.py
+++ b/authorizenet/cim.py
@@ -2,11 +2,10 @@
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
-from authorizenet.models import CIMResponse, Response
from authorizenet.signals import customer_was_created, customer_was_flagged, \
payment_was_successful, payment_was_flagged
@@ -48,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 payment_data.get('expirationDate') is not None:
+ payment_data['expirationDate'] = \
+ payment_data['expirationDate'].strftime('%Y-%m')
return payment_data
@@ -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,
@@ -265,7 +277,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
@@ -285,9 +297,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)
@@ -303,7 +315,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()
@@ -317,6 +330,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)
@@ -694,7 +708,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:
@@ -755,6 +769,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/conf.py b/authorizenet/conf.py
new file mode 100644
index 0000000..45d982a
--- /dev/null
+++ b/authorizenet/conf.py
@@ -0,0 +1,39 @@
+"""Application-specific settings for django-authorizenet"""
+
+from django.conf import settings as django_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 = set(('DEBUG', 'LOGIN_ID', 'TRANSACTION_KEY', 'CUSTOMER_MODEL',
+ 'DELIM_CHAR', 'FORCE_TEST_REQUEST', 'EMAIL_CUSTOMER',
+ 'MD5_HASH'))
+
+ class Default:
+ CUSTOMER_MODEL = getattr(
+ django_settings, 'AUTH_USER_MODEL', "auth.User")
+ 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(django_settings, self.prefix + name)
+ except AttributeError:
+ return getattr(self.defaults, name)
+
+settings = Settings()
diff --git a/authorizenet/exceptions.py b/authorizenet/exceptions.py
new file mode 100644
index 0000000..a572ba7
--- /dev/null
+++ b/authorizenet/exceptions.py
@@ -0,0 +1,2 @@
+class BillingError(Exception):
+ """Error due to Authorize.NET request"""
diff --git a/authorizenet/fields.py b/authorizenet/fields.py
index 2fd4a63..d0b5f7b 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
@@ -98,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)
diff --git a/authorizenet/forms.py b/authorizenet/forms.py
index 648bb82..61c347d 100644
--- a/authorizenet/forms.py
+++ b/authorizenet/forms.py
@@ -1,14 +1,15 @@
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
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")
@@ -91,12 +92,46 @@ class CIMPaymentForm(forms.Form):
card_code = CreditCardCVV2Field(label="Card Security Code")
+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.customer = kwargs.pop('customer', None)
+ return super(CustomerPaymentForm, self).__init__(*args, **kwargs)
+
+ def save(self, commit=True):
+ instance = super(CustomerPaymentForm, self).save(commit=False)
+ if self.customer:
+ instance.customer = self.customer
+ instance.card_code = self.cleaned_data.get('card_code')
+ if commit:
+ 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 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):
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/managers.py b/authorizenet/managers.py
new file mode 100644
index 0000000..384d92d
--- /dev/null
+++ b/authorizenet/managers.py
@@ -0,0 +1,34 @@
+from django.db import models
+
+
+class CustomerProfileManager(models.Manager):
+
+ def create(self, **data):
+
+ """Create new Authorize.NET customer profile"""
+
+ from .models import CustomerPaymentProfile
+
+ kwargs = data
+ sync = kwargs.pop('sync', True)
+ kwargs = {
+ 'customer': kwargs.get('customer', None),
+ 'profile_id': kwargs.pop('profile_id', None),
+ }
+
+ # 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 obj.payment_profile_ids:
+ CustomerPaymentProfile.objects.create(
+ customer_profile=obj,
+ payment_profile_id=payment_profile_id,
+ sync=False,
+ **data
+ )
+
+ return obj
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 c1cfe0b..5f92979 100644
--- a/authorizenet/models.py
+++ b/authorizenet/models.py
@@ -1,4 +1,13 @@
from django.db import models
+from django.forms.models import model_to_dict
+
+from .conf import settings
+from .cim import add_profile, get_profile, update_payment_profile, \
+ create_payment_profile, delete_profile, delete_payment_profile
+
+from .managers import CustomerProfileManager
+from .exceptions import BillingError
+
RESPONSE_CHOICES = (
('1', 'Approved'),
@@ -194,3 +203,155 @@ class CIMResponse(models.Model):
@property
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):
+
+ """Authorize.NET customer profile"""
+
+ customer = models.OneToOneField(settings.CUSTOMER_MODEL,
+ related_name='customer_profile')
+ profile_id = models.CharField(max_length=50)
+
+ def save(self, *args, **kwargs):
+ data = kwargs.pop('data', {})
+ sync = kwargs.pop('sync', True)
+ if not self.id and sync:
+ 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)
+ response.raise_if_error()
+ super(CustomerProfile, self).delete()
+
+ def push_to_server(self, data):
+ output = add_profile(self.customer.pk, data, data)
+ 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"""
+ 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']
+ )
+ instance.sync(payment_profile)
+
+ objects = CustomerProfileManager()
+
+ def __unicode__(self):
+ return self.profile_id
+
+
+class CustomerPaymentProfile(models.Model):
+
+ """Authorize.NET customer payment profile"""
+
+ customer = models.ForeignKey(settings.CUSTOMER_MODEL,
+ 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)
+ card_number = models.CharField(max_length=16, blank=True)
+ expiration_date = models.DateField(blank=True, null=True)
+ card_code = None
+
+ def __init__(self, *args, **kwargs):
+ self.card_code = kwargs.pop('card_code', None)
+ return super(CustomerPaymentProfile, self).__init__(*args, **kwargs)
+
+ def save(self, *args, **kwargs):
+ if kwargs.pop('sync', True):
+ self.push_to_server()
+ 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 not self.customer_profile_id:
+ try:
+ self.customer_profile = CustomerProfile.objects.get(
+ customer=self.customer)
+ except CustomerProfile.DoesNotExist:
+ pass
+ if self.payment_profile_id:
+ response = update_payment_profile(
+ self.customer_profile.profile_id,
+ self.payment_profile_id,
+ self.raw_data,
+ self.raw_data,
+ )
+ elif self.customer_profile_id:
+ output = create_payment_profile(
+ self.customer_profile.profile_id,
+ self.raw_data,
+ self.raw_data,
+ )
+ response = output['response']
+ self.payment_profile_id = output['payment_profile_id']
+ else:
+ output = add_profile(
+ self.customer.id,
+ self.raw_data,
+ self.raw_data,
+ )
+ response = output['response']
+ self.customer_profile = CustomerProfile.objects.create(
+ customer=self.customer,
+ profile_id=output['profile_id'],
+ sync=False,
+ )
+ self.payment_profile_id = output['payment_profile_ids'][0]
+ response.raise_if_error()
+
+ @property
+ def raw_data(self):
+ """Return data suitable for use in payment and billing forms"""
+ data = model_to_dict(self)
+ data['card_code'] = getattr(self, 'card_code')
+ return data
+
+ 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(sync=False)
+
+ def delete(self):
+ """Delete the customer payment profile remotely and locally"""
+ response = delete_payment_profile(self.customer_profile.profile_id,
+ self.payment_profile_id)
+ response.raise_if_error()
+ return super(CustomerPaymentProfile, self).delete()
+
+ def update(self, **data):
+ """Update the customer payment profile remotely and locally"""
+ for key, value in data.items():
+ setattr(self, key, value)
+ self.save()
+ return self
+
+ def __unicode__(self):
+ return self.payment_profile_id
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/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html b/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html
new file mode 100644
index 0000000..8790e8d
--- /dev/null
+++ b/authorizenet/templates/admin/authorizenet/customerprofile/change_form.html
@@ -0,0 +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 %}
diff --git a/authorizenet/tests/tests.py b/authorizenet/tests/tests.py
deleted file mode 100644
index 1706576..0000000
--- a/authorizenet/tests/tests.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from datetime import datetime
-from django.test import TestCase
-from xml.dom.minidom import parseString
-from httmock import urlmatch, 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'
- 'I00001
Successful.'
- ''
- '6666'
- ''
- '7777'
- ''
- ''
- ''
- '{0}>'
-)
-
-
-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
-
-
-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': "1111222233334444",
- '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 = {
- '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'
- },
- }
- }
-
- 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 success_response.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/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 f030578..19d1d4a 100644
--- a/authorizenet/views.py
+++ b/authorizenet/views.py
@@ -3,11 +3,13 @@
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
-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
@@ -16,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()
@@ -121,3 +123,23 @@ def validate_payment_form(self):
self.payment_template,
self.context
)
+
+
+class PaymentProfileCreateView(CreateView):
+ template_name = 'authorizenet/create_payment_profile.html'
+ form_class = CustomerPaymentForm
+
+ def get_form_kwargs(self):
+ kwargs = super(PaymentProfileCreateView, self).get_form_kwargs()
+ kwargs['customer'] = 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['customer'] = self.request.user
+ return kwargs
diff --git a/runtests.py b/runtests.py
index d02ca06..89cb44f 100644
--- a/runtests.py
+++ b/runtests.py
@@ -11,9 +11,14 @@
AUTHNET_LOGIN_ID="loginid",
AUTHNET_TRANSACTION_KEY="key",
INSTALLED_APPS=(
- 'authorizenet.tests',
+ 'django.contrib.contenttypes',
+ 'django.contrib.auth',
+ 'django.contrib.sessions',
+ 'tests',
'authorizenet',
),
+ ROOT_URLCONF='tests.urls',
+ STATIC_URL='/static/',
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}},
)
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()
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'])
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/models.py b/tests/models.py
similarity index 100%
rename from authorizenet/tests/models.py
rename to tests/models.py
diff --git a/tests/templates/authorizenet/create_payment_profile.html b/tests/templates/authorizenet/create_payment_profile.html
new file mode 100644
index 0000000..c592814
--- /dev/null
+++ b/tests/templates/authorizenet/create_payment_profile.html
@@ -0,0 +1,4 @@
+
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 @@
+
diff --git a/tests/tests/__init__.py b/tests/tests/__init__.py
new file mode 100644
index 0000000..75dda46
--- /dev/null
+++ b/tests/tests/__init__.py
@@ -0,0 +1,3 @@
+from .cim import *
+from .views import *
+from .models import *
diff --git a/tests/tests/cim.py b/tests/tests/cim.py
new file mode 100644
index 0000000..dfbb27f
--- /dev/null
+++ b/tests/tests/cim.py
@@ -0,0 +1,80 @@
+from copy import deepcopy
+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 = 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"""
+ @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/tests/mocks.py b/tests/tests/mocks.py
new file mode 100644
index 0000000..db518a6
--- /dev/null
+++ b/tests/tests/mocks.py
@@ -0,0 +1,46 @@
+from httmock import urlmatch
+
+
+cim_url_match = urlmatch(scheme='https', netloc=r'^api\.authorize\.net$',
+ path=r'^/xml/v1/request\.api$')
+
+
+delete_success = (
+ ''
+ '<{0}>'
+ ''
+ 'Ok'
+ 'I00001
Successful.'
+ ''
+ '{0}>'
+)
+
+
+customer_profile_success = (
+ ''
+ '<{0}>'
+ ''
+ 'Ok'
+ 'I00001
Successful.'
+ ''
+ '6666'
+ ''
+ '7777'
+ ''
+ ''
+ ''
+ '{0}>'
+)
+
+
+payment_profile_success = (
+ ''
+ '<{0}>'
+ ''
+ 'Ok'
+ 'I00001
Successful.'
+ ''
+ '6666'
+ '7777'
+ '{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
new file mode 100644
index 0000000..bec7753
--- /dev/null
+++ b/tests/tests/test_data.py
@@ -0,0 +1,126 @@
+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',
+ 'profile': {
+ 'merchantCustomerId': '42',
+ 'paymentProfiles': {
+ 'billTo': {
+ 'firstName': 'Danielle',
+ 'lastName': 'Thompson',
+ 'company': '',
+ 'phoneNumber': '',
+ 'faxNumber': '',
+ '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': '',
+ 'phoneNumber': '',
+ 'faxNumber': '',
+ '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',
+ },
+ }
+}
+
+
+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',
+ },
+ }
+}
+
+
+delete_profile_success = {
+ 'deleteCustomerProfileRequest': {
+ 'xmlns': u'AnetApi/xml/v1/schema/AnetApiSchema.xsd',
+ 'customerProfileId': '6666',
+ 'merchantAuthentication': {
+ 'transactionKey': 'key',
+ 'name': 'loginid'
+ },
+ },
+}
diff --git a/tests/tests/utils.py b/tests/tests/utils.py
new file mode 100644
index 0000000..e4079eb
--- /dev/null
+++ b/tests/tests/utils.py
@@ -0,0 +1,35 @@
+from django.contrib.auth.models import User
+
+
+def create_user(id=None, username='', password=''):
+ user = User(username=username)
+ user.id = id
+ 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/tests/tests/views.py b/tests/tests/views.py
new file mode 100644
index 0000000..a3ebdfe
--- /dev/null
+++ b/tests/tests/views.py
@@ -0,0 +1,148 @@
+from datetime import date
+from django.test import LiveServerTestCase
+from xml.dom.minidom import parseString
+from httmock import HTTMock
+
+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
+
+
+class PaymentProfileCreationTests(LiveServerTestCase):
+
+ 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):
+ 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):
+ @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 customer_profile_success.format('createCustomerProfileResponse')
+ 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)
+
+ 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(customer=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):
+
+ def setUp(self):
+ self.user = create_user(id=42, username='billy', password='password')
+ profile = CustomerProfile(customer=self.user, profile_id='6666')
+ profile.save(sync=False)
+ self.payment_profile = CustomerPaymentProfile(
+ customer=self.user,
+ customer_profile=profile,
+ payment_profile_id='7777',
+ )
+ self.payment_profile.save(sync=False)
+ 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 customer_profile_success.format('updateCustomerProfileResponse')
+ 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)
+ 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,
+ 'customer': self.user.id,
+ 'payment_profile_id': '7777',
+ 'card_number': 'XXXX1747',
+ 'expiration_date': date(2020, 5, 31),
+ 'card_code': None,
+ 'first_name': 'Danielle',
+ 'last_name': 'Thompson',
+ 'company': '',
+ 'fax_number': '',
+ 'phone_number': '',
+ 'address': '101 Broadway Avenue',
+ 'city': 'San Diego',
+ 'state': 'CA',
+ 'country': 'US',
+ 'zip': '92101',
+ })
+
diff --git a/tests/urls.py b/tests/urls.py
new file mode 100644
index 0000000..8db5d51
--- /dev/null
+++ b/tests/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls import url, patterns
+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
new file mode 100644
index 0000000..64b861d
--- /dev/null
+++ b/tests/views.py
@@ -0,0 +1,20 @@
+from django.http import HttpResponse
+from authorizenet.views import PaymentProfileCreateView, PaymentProfileUpdateView
+
+
+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'
+
+
+def success_view(request):
+ return HttpResponse("success")