summaryrefslogtreecommitdiff
path: root/addons/payment_authorize/models/authorize_request.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/payment_authorize/models/authorize_request.py')
-rw-r--r--addons/payment_authorize/models/authorize_request.py247
1 files changed, 247 insertions, 0 deletions
diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py
new file mode 100644
index 00000000000..e94c4e1c484
--- /dev/null
+++ b/addons/payment_authorize/models/authorize_request.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+from lxml import etree, objectify
+from urllib2 import urlopen, Request
+from StringIO import StringIO
+import xml.etree.ElementTree as ET
+from uuid import uuid4
+
+from odoo.exceptions import ValidationError, UserError
+
+XMLNS = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'
+
+
+def strip_ns(xml, ns):
+ """Strip the provided name from tag names.
+
+ :param str xml: xml document
+ :param str ns: namespace to strip
+
+ :rtype: etree._Element
+ :return: the parsed xml string with the namespace prefix removed
+ """
+ it = ET.iterparse(StringIO(xml))
+ ns_prefix = '{%s}' % XMLNS
+ for _, el in it:
+ if el.tag.startswith(ns_prefix):
+ el.tag = el.tag[len(ns_prefix):] # strip all Auth.net namespaces
+ return it.root
+
+
+class AuthorizeAPI():
+ """Authorize.net Gateway API integration.
+
+ This class allows contacting the Authorize.net API with simple operation
+ requests. It implements a *very limited* subset of the complete API
+ (http://developer.authorize.net/api/reference); namely:
+ - Customer Profile/Payment Profile creation
+ - Transaction authorization/capture/voiding
+ """
+ def __init__(self, acquirer):
+ """Initiate the environment with the acquirer data.
+
+ :param record acquirer: payment.acquirer account that will be contacted
+ """
+ if acquirer.environment == 'test':
+ self.url = 'https://apitest.authorize.net/xml/v1/request.api'
+ else:
+ self.url = 'https://api.authorize.net/xml/v1/request.api'
+ self.name = acquirer.authorize_login
+ self.transaction_key = acquirer.authorize_transaction_key
+
+ def _authorize_request(self, data):
+ """Encode, send and process the request to the Authorize.net API.
+
+ Encodes the xml data and process the response. Note that only a basic
+ processing is done at this level (namespace cleanup, basic error management).
+
+ :param etree._Element data: etree data to process
+ """
+ data = etree.tostring(data, xml_declaration=True, encoding='utf-8')
+ request = Request(self.url, data)
+ request.add_header('Content-Type', 'text/xml')
+ response = urlopen(request).read()
+ response = strip_ns(response, XMLNS)
+ if response.find('messages/resultCode').text == 'Error':
+ messages = map(lambda m: m.text, response.findall('messages/message/text'))
+ raise ValidationError('Authorize.net Error Message(s):\n %s' % '\n'.join(messages))
+ return response
+
+ def _base_tree(self, requestType):
+ """Create a basic tree containing authentication information.
+
+ Create a etree Element of type requestType and appends the Authorize.net
+ credentials (they are always required).
+ :param str requestType: the type of request to send to Authorize.net
+ See http://developer.authorize.net/api/reference
+ for available types.
+ :return: basic etree Element of the requested type
+ containing credentials information
+ :rtype: etree._Element
+ """
+ root = etree.Element(requestType, xmlns=XMLNS)
+ auth = etree.SubElement(root, "merchantAuthentication")
+ etree.SubElement(auth, "name").text = self.name
+ etree.SubElement(auth, "transactionKey").text = self.transaction_key
+ return root
+
+ # Customer profiles
+ def create_customer_profile(self, partner, cardnumber, expiration_date, card_code):
+ """Create a payment and customer profile in the Authorize.net backend.
+
+ Creates a customer profile for the partner/credit card combination and links
+ a corresponding payment profile to it. Note that a single partner in the Odoo
+ database can have multiple customer profiles in Authorize.net (i.e. a customer
+ profile is created for every res.partner/payment.token couple).
+
+ :param record partner: the res.partner record of the customer
+ :param str cardnumber: cardnumber in string format (numbers only, no separator)
+ :param str expiration_date: expiration date in 'YYYY-MM' string format
+ :param str card_code: three- or four-digit verification number
+
+ :return: a dict containing the profile_id and payment_profile_id of the
+ newly created customer profile and payment profile
+ :rtype: dict
+ """
+ root = self._base_tree('createCustomerProfileRequest')
+ profile = etree.SubElement(root, "profile")
+ etree.SubElement(profile, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8])
+ etree.SubElement(profile, "email").text = partner.email
+ payment_profile = etree.SubElement(profile, "paymentProfiles")
+ etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual'
+ billTo = etree.SubElement(payment_profile, "billTo")
+ etree.SubElement(billTo, "address").text = (partner.street + (partner.street2 if partner.street2 else '')) or None
+ etree.SubElement(billTo, "city").text = partner.city
+ etree.SubElement(billTo, "state").text = partner.state_id.name or None
+ etree.SubElement(billTo, "zip").text = partner.zip
+ etree.SubElement(billTo, "country").text = partner.country_id.name or None
+ payment = etree.SubElement(payment_profile, "payment")
+ creditCard = etree.SubElement(payment, "creditCard")
+ etree.SubElement(creditCard, "cardNumber").text = cardnumber
+ etree.SubElement(creditCard, "expirationDate").text = expiration_date
+ etree.SubElement(creditCard, "cardCode").text = card_code
+ etree.SubElement(root, "validationMode").text = 'liveMode'
+ response = self._authorize_request(root)
+ res = dict()
+ res['profile_id'] = response.find('customerProfileId').text
+ res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
+ return res
+
+ # Transaction management
+ def auth_and_capture(self, token, amount, reference):
+ """Authorize and capture a payment for the given amount.
+
+ Authorize and immediately capture a payment for the given payment.token
+ record for the specified amount with reference as communication.
+
+ :param record token: the payment.token record that must be charged
+ :param str amount: transaction amount (up to 15 digits with decimal point)
+ :param str reference: used as "invoiceNumber" in the Authorize.net backend
+
+ :return: a dict containing the response code, transaction id and transaction type
+ :rtype: dict
+ """
+ root = self._base_tree('createTransactionRequest')
+ tx = etree.SubElement(root, "transactionRequest")
+ etree.SubElement(tx, "transactionType").text = "authCaptureTransaction"
+ etree.SubElement(tx, "amount").text = str(amount)
+ profile = etree.SubElement(tx, "profile")
+ etree.SubElement(profile, "customerProfileId").text = token.authorize_profile
+ payment_profile = etree.SubElement(profile, "paymentProfile")
+ etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
+ order = etree.SubElement(tx, "order")
+ etree.SubElement(order, "invoiceNumber").text = reference
+ response = self._authorize_request(root)
+ res = dict()
+ res['x_response_code'] = response.find('transactionResponse/responseCode').text
+ res['x_trans_id'] = response.find('transactionResponse/transId').text
+ res['x_type'] = 'auth_capture'
+ return res
+
+ def authorize(self, token, amount, reference):
+ """Authorize a payment for the given amount.
+
+ Authorize (without capture) a payment for the given payment.token
+ record for the specified amount with reference as communication.
+
+ :param record token: the payment.token record that must be charged
+ :param str amount: transaction amount (up to 15 digits with decimal point)
+ :param str reference: used as "invoiceNumber" in the Authorize.net backend
+
+ :return: a dict containing the response code, transaction id and transaction type
+ :rtype: dict
+ """
+ root = self._base_tree('createTransactionRequest')
+ tx = etree.SubElement(root, "transactionRequest")
+ etree.SubElement(tx, "transactionType").text = "authOnlyTransaction"
+ etree.SubElement(tx, "amount").text = str(amount)
+ profile = etree.SubElement(tx, "profile")
+ etree.SubElement(profile, "customerProfileId").text = token.authorize_profile
+ payment_profile = etree.SubElement(profile, "paymentProfile")
+ etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
+ order = etree.SubElement(tx, "order")
+ etree.SubElement(order, "invoiceNumber").text = reference
+ response = self._authorize_request(root)
+ res = dict()
+ res['x_response_code'] = response.find('transactionResponse/responseCode').text
+ res['x_trans_id'] = response.find('transactionResponse/transId').text
+ res['x_type'] = 'auth_only'
+ return res
+
+ def capture(self, transaction_id, amount):
+ """Capture a previously authorized payment for the given amount.
+
+ Capture a previsouly authorized payment. Note that the amount is required
+ even though we do not support partial capture.
+
+ :param str transaction_id: the id of the authorized transaction in the
+ Authorize.net backend
+ :param str amount: transaction amount (up to 15 digits with decimal point)
+
+ :return: a dict containing the response code, transaction id and transaction type
+ :rtype: dict
+ """
+ root = self._base_tree('createTransactionRequest')
+ tx = etree.SubElement(root, "transactionRequest")
+ etree.SubElement(tx, "transactionType").text = "priorAuthCaptureTransaction"
+ etree.SubElement(tx, "amount").text = str(amount)
+ etree.SubElement(tx, "refTransId").text = transaction_id
+ response = self._authorize_request(root)
+ res = dict()
+ res['x_response_code'] = response.find('transactionResponse/responseCode').text
+ res['x_trans_id'] = response.find('transactionResponse/transId').text
+ res['x_type'] = 'prior_auth_capture'
+ return res
+
+ def void(self, transaction_id):
+ """Void a previously authorized payment.
+
+ :param str transaction_id: the id of the authorized transaction in the
+ Authorize.net backend
+
+ :return: a dict containing the response code, transaction id and transaction type
+ :rtype: dict
+ """
+ root = self._base_tree('createTransactionRequest')
+ tx = etree.SubElement(root, "transactionRequest")
+ etree.SubElement(tx, "transactionType").text = "voidTransaction"
+ etree.SubElement(tx, "refTransId").text = transaction_id
+ response = self._authorize_request(root)
+ res = dict()
+ res['x_response_code'] = response.find('transactionResponse/responseCode').text
+ res['x_trans_id'] = response.find('transactionResponse/transId').text
+ res['x_type'] = 'void'
+ return res
+
+ # Test
+ def test_authenticate(self):
+ """Test Authorize.net communication with a simple credentials check.
+
+ :return: True if authentication was successful, else False (or throws an error)
+ :rtype: bool
+ """
+ test_auth = self._base_tree('authenticateTestRequest')
+ response = self._authorize_request(test_auth)
+ root = objectify.fromstring(response)
+ if root.find('{ns}messages/{ns}resultCode'.format(ns='{%s}' % XMLNS)) == 'Ok':
+ return True
+ return False