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..2b327b3 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 ['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/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}>'
- 'I00001
I00001
I00001
I00001