summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamien Bouvy <dbo@odoo.com>2016-08-29 15:50:53 +0200
committerDamien Bouvy <dbo@odoo.com>2016-09-01 15:58:41 +0200
commita2b9b3a6bb39aded694c30620820034f74cd0f77 (patch)
tree56ced8e7fa4882a9377880346f1c9b1b0551da9c
parentba87ff438d1cdf5e1f5c57563a16a17f6eea6529 (diff)
[IMP] payment_authorize: add server2server support
This commit add s2s communication to the Authorize.net payment provider, allowing the user to: - Create a payment token - Charge a payment token - Authorize/capture transactions
-rw-r--r--addons/payment_authorize/controllers/main.py14
-rw-r--r--addons/payment_authorize/data/payment_acquirer_data.xml1
-rw-r--r--addons/payment_authorize/models/__init__.py1
-rw-r--r--addons/payment_authorize/models/authorize_request.py247
-rw-r--r--addons/payment_authorize/models/payment.py146
-rw-r--r--addons/payment_authorize/tests/test_authorize.py81
-rw-r--r--addons/payment_authorize/views/payment_authorize_templates.xml35
7 files changed, 518 insertions, 7 deletions
diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py
index aec2aa1f777..5fc8ba87a69 100644
--- a/addons/payment_authorize/controllers/main.py
+++ b/addons/payment_authorize/controllers/main.py
@@ -2,6 +2,7 @@
import pprint
import logging
import urlparse
+import werkzeug
from odoo import http
from odoo.http import request
@@ -30,3 +31,16 @@ class AuthorizeController(http.Controller):
return request.render('payment_authorize.payment_authorize_redirect', {
'return_url': '%s' % urlparse.urljoin(base_url, return_url)
})
+
+ @http.route(['/payment/authorize/s2s/create_json'], type='json', auth='public')
+ def authorize_s2s_create_json(self, **kwargs):
+ acquirer_id = int(kwargs.get('acquirer_id'))
+ acquirer = request.env['payment.acquirer'].browse(acquirer_id)
+ return acquirer.s2s_process(kwargs)
+
+ @http.route(['/payment/authorize/s2s/create'], type='http', auth='public')
+ def authorize_s2s_create(self, **post):
+ acquirer_id = int(post.get('acquirer_id'))
+ acquirer = request.env['payment.acquirer'].browse(acquirer_id)
+ acquirer.s2s_process(post)
+ return werkzeug.utils.redirect(post.get('return_url', '/'))
diff --git a/addons/payment_authorize/data/payment_acquirer_data.xml b/addons/payment_authorize/data/payment_acquirer_data.xml
index 8748535c4b9..de2c3a4eb45 100644
--- a/addons/payment_authorize/data/payment_acquirer_data.xml
+++ b/addons/payment_authorize/data/payment_acquirer_data.xml
@@ -8,6 +8,7 @@
<field name="provider">authorize</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="authorize_acquirer_button"/>
+ <field name="registration_view_template_id" ref="authorize_s2s_form"/>
<field name="environment">test</field>
<field name="pre_msg"><![CDATA[
<p>You will be redirected to the Authorize website after clicking on the payment button.</p>]]></field>
diff --git a/addons/payment_authorize/models/__init__.py b/addons/payment_authorize/models/__init__.py
index 10b988e0c28..6d0ae2698db 100644
--- a/addons/payment_authorize/models/__init__.py
+++ b/addons/payment_authorize/models/__init__.py
@@ -1,3 +1,2 @@
# -*- coding: utf-8 -*-
-
import payment
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
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index b7488937e61..af57569eb5f 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -1,5 +1,7 @@
# coding: utf-8
+from authorize_request import AuthorizeAPI
+from datetime import datetime
import hashlib
import hmac
import logging
@@ -10,6 +12,7 @@ from odoo import api, fields, models
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
from odoo.tools.float_utils import float_compare
+from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
@@ -47,7 +50,7 @@ class PaymentAcquirerAuthorize(models.Model):
'x_trans_key': self.authorize_transaction_key,
'x_amount': str(values['amount']),
'x_show_form': 'PAYMENT_FORM',
- 'x_type': 'AUTH_CAPTURE',
+ 'x_type': 'AUTH_CAPTURE' if self.auto_confirm != 'authorize' else 'AUTH_ONLY',
'x_method': 'CC',
'x_fp_sequence': '%s%s' % (self.id, int(time.time())),
'x_version': '3.1',
@@ -85,6 +88,37 @@ class PaymentAcquirerAuthorize(models.Model):
self.ensure_one()
return self._get_authorize_urls(self.environment)['authorize_form_url']
+ @api.model
+ def authorize_s2s_form_process(self, data):
+ values = {
+ 'cc_number': data.get('cc_number'),
+ 'cc_holder_name': data.get('cc_holder_name'),
+ 'cc_expiry': data.get('cc_expiry'),
+ 'cc_cvc': data.get('cc_cvc'),
+ 'cc_brand': data.get('cc_brand'),
+ 'acquirer_id': int(data.get('acquirer_id')),
+ 'partner_id': int(data.get('partner_id'))
+ }
+ PaymentMethod = self.env['payment.token'].sudo().create(values)
+ return PaymentMethod.id
+
+ @api.multi
+ def authorize_s2s_form_validate(self, data):
+ error = dict()
+ mandatory_fields = ["cc_number", "cc_cvc", "cc_holder_name", "cc_expiry", "cc_brand"]
+ # Validation
+ for field_name in mandatory_fields:
+ if not data.get(field_name):
+ error[field_name] = 'missing'
+ if data['cc_expiry'] and datetime.now().strftime('%y%M') > datetime.strptime(data['cc_expiry'], '%M / %y').strftime('%y%M'):
+ return False
+ return False if error else True
+
+ @api.multi
+ def authorize_test_credentials(self):
+ self.ensure_one()
+ transaction = AuthorizeAPI(self.acquirer_id)
+ return transaction.test_authenticate()
class TxAuthorize(models.Model):
_inherit = 'payment.transaction'
@@ -104,7 +138,7 @@ class TxAuthorize(models.Model):
reference, trans_id, fingerprint = data.get('x_invoice_num'), data.get('x_trans_id'), data.get('x_MD5_Hash')
if not reference or not trans_id or not fingerprint:
error_msg = 'Authorize: received data with missing reference (%s) or trans_id (%s) or fingerprint (%s)' % (reference, trans_id, fingerprint)
- _logger.error(error_msg)
+ _logger.info(error_msg)
raise ValidationError(error_msg)
tx = self.search([('reference', '=', reference)])
if not tx or len(tx) > 1:
@@ -113,7 +147,7 @@ class TxAuthorize(models.Model):
error_msg += '; no order found'
else:
error_msg += '; multiple order found'
- _logger.error(error_msg)
+ _logger.info(error_msg)
raise ValidationError(error_msg)
return tx[0]
@@ -136,8 +170,9 @@ class TxAuthorize(models.Model):
status_code = int(data.get('x_response_code', '0'))
if status_code == self._authorize_valid_tx_status:
self.write({
- 'state': 'done',
+ 'state': 'done' if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture'] else 'authorized',
'acquirer_reference': data.get('x_trans_id'),
+ 'date_validate': fields.Datetime.now() if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture'] else False,
})
return True
elif status_code == self._authorize_pending_tx_status:
@@ -161,3 +196,106 @@ class TxAuthorize(models.Model):
'acquirer_reference': data.get('x_trans_id'),
})
return False
+
+ @api.multi
+ def authorize_s2s_do_transaction(self, **data):
+ self.ensure_one()
+ transaction = AuthorizeAPI(self.acquirer_id)
+ if self.acquirer_id.auto_confirm != "authorize":
+ res = transaction.auth_and_capture(self.payment_token_id, self.amount, self.reference)
+ else:
+ res = transaction.authorize(self.payment_token_id, self.amount, self.reference)
+ return self._authorize_s2s_validate_tree(res)
+
+ @api.multi
+ def authorize_s2s_capture_transaction(self):
+ self.ensure_one()
+ transaction = AuthorizeAPI(self.acquirer_id)
+ tree = transaction.capture(self.acquirer_reference, self.amount)
+ return self._authorize_s2s_validate_tree(tree)
+
+ @api.multi
+ def authorize_s2s_void_transaction(self):
+ self.ensure_one()
+ transaction = AuthorizeAPI(self.acquirer_id)
+ tree = transaction.void(self.acquirer_reference)
+ return self._authorize_s2s_validate_tree(tree)
+
+ @api.multi
+ def _authorize_s2s_validate_tree(self, tree):
+ return self._authorize_s2s_validate(tree)
+
+ @api.multi
+ def _authorize_s2s_validate(self, tree):
+ self.ensure_one()
+ if self.state == 'done':
+ _logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
+ return True
+ status_code = int(tree.get('x_response_code', '0'))
+ if status_code == self._authorize_valid_tx_status:
+ if tree.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
+ if self.callback_eval and self.state != 'authorized':
+ safe_eval(self.callback_eval, {'self': self})
+ self.write({
+ 'state': 'done',
+ 'acquirer_reference': tree.get('x_trans_id'),
+ 'date_validate': fields.Datetime.now(),
+ })
+ if tree.get('x_type').lower() == 'auth_only':
+ if self.callback_eval:
+ safe_eval(self.callback_eval, {'self': self})
+ self.write({
+ 'state': 'authorized',
+ 'acquirer_reference': tree.get('x_trans_id'),
+ })
+ if tree.get('x_type').lower() == 'void':
+ self.write({
+ 'state': 'cancel',
+ })
+ return True
+ elif status_code == self._authorize_pending_tx_status:
+ self.write({
+ 'state': 'pending',
+ 'acquirer_reference': tree.get('x_trans_id'),
+ })
+ return True
+ elif status_code == self._authorize_cancel_tx_status:
+ self.write({
+ 'state': 'cancel',
+ 'acquirer_reference': tree.get('x_trans_id'),
+ })
+ return True
+ else:
+ error = tree.get('x_response_reason_text')
+ _logger.info(error)
+ self.write({
+ 'state': 'error',
+ 'state_message': error,
+ 'acquirer_reference': tree.get('x_trans_id'),
+ })
+ return False
+
+
+class PaymentToken(models.Model):
+ _inherit = 'payment.token'
+
+ authorize_profile = fields.Char(string='Authorize.net Profile ID', help='This contains the unique reference '
+ 'for this partner/payment token combination in the Authorize.net backend')
+
+ @api.model
+ def authorize_create(self, values):
+ if values.get('cc_number'):
+ values['cc_number'] = values['cc_number'].replace(' ', '')
+ acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
+ expiry = str(values['cc_expiry'][:2]) + str(values['cc_expiry'][-2:])
+ partner = self.env['res.partner'].browse(values['partner_id'])
+ transaction = AuthorizeAPI(acquirer)
+ res = transaction.create_customer_profile(partner, values['cc_number'], expiry, values['cc_cvc'])
+ if res.get('profile_id') and res.get('payment_profile_id'):
+ return {
+ 'authorize_profile': res.get('profile_id'),
+ 'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']),
+ 'acquirer_ref': res.get('payment_profile_id'),
+ }
+ else:
+ raise ValidationError('The Customer Profile creation in Authorize.NET failed.')
diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py
index 464a794d481..50e4e95fdc4 100644
--- a/addons/payment_authorize/tests/test_authorize.py
+++ b/addons/payment_authorize/tests/test_authorize.py
@@ -4,6 +4,7 @@ import hashlib
import hmac
import time
import urlparse
+import unittest
from lxml import objectify
import odoo
@@ -23,6 +24,8 @@ class AuthorizeCommon(PaymentAcquirerCommon):
self.currency_usd = self.env['res.currency'].search([('name', '=', 'USD')], limit=1)[0]
# get the authorize account
self.authorize = self.env.ref('payment.payment_acquirer_authorize')
+ # Be sure to be in 'capture' mode
+ self.authorize.auto_confirm = 'confirm_so'
@odoo.tests.common.at_install(True)
@@ -97,7 +100,7 @@ class AuthorizeForm(AuthorizeCommon):
'Authorize: wrong value for input %s: received %s instead of %s' % (values[1], values[2], form_values[values[1]])
)
- @mute_logger('openerp.addons.payment_authorize.models.payment', 'ValidationError')
+ @mute_logger('odoo.addons.payment_authorize.models.payment', 'ValidationError')
def test_20_authorize_form_management(self):
# be sure not to do stupid thing
self.assertEqual(self.authorize.environment, 'test', 'test without test environment')
@@ -120,7 +123,7 @@ class AuthorizeForm(AuthorizeCommon):
'x_cvv2_resp_code': u'',
'x_description': u'',
'x_duty': u'0.00',
- 'x_email': u'norbert.buyer@exampl',
+ 'x_email': u'norbert.buyer@example.com',
'x_fax': u'',
'x_first_name': u'Norbert',
'x_freight': u'0.00',
@@ -174,3 +177,77 @@ class AuthorizeForm(AuthorizeCommon):
self.env['payment.transaction'].form_feedback(authorize_post_data, 'authorize')
# check state
self.assertEqual(tx.state, 'error', 'Authorize: erroneous validation did not put tx into error state')
+
+ @unittest.skip("Authorize s2s test disabled: We do not want to overload Authorize.net with runbot's requests")
+ def test_30_authorize_s2s(self):
+ # be sure not to do stupid thing
+ authorize = self.authorize
+ self.assertEqual(authorize.environment, 'test', 'test without test environment')
+
+ # add credential
+ # FIXME: put this test in master-nightly on odoo/odoo + create sandbox account
+ authorize.write({
+ 'authorize_transaction_key': '',
+ 'authorize_login': '',
+ })
+ self.assertTrue(authorize.authorize_test_credentials, 'Authorize.net: s2s authentication failed')
+
+ # create payment meethod
+ payment_token = self.env['payment.token'].create({
+ 'acquirer_id': authorize.id,
+ 'partner_id': self.buyer_id,
+ 'cc_number': '4111 1111 1111 1111',
+ 'cc_expiry': '02 / 26',
+ 'cc_brand': 'visa',
+ 'cc_cvc': '111',
+ 'cc_holder_name': 'test',
+ })
+
+ # create normal s2s transaction
+ transaction = self.env['payment.transaction'].create({
+ 'amount': 500,
+ 'acquirer_id': authorize.id,
+ 'type': 'server2server',
+ 'currency_id': self.currency_usd.id,
+ 'reference': 'test_ref_%s' % odoo.fields.Date.today(),
+ 'payment_token_id': payment_token.id,
+ 'partner_id': self.buyer_id,
+
+ })
+ transaction.authorize_s2s_do_transaction()
+ self.assertEqual(transaction.state, 'done',)
+
+ # switch to 'authorize only'
+ # create authorize only s2s transaction & capture it
+ self.authorize.auto_confirm = 'authorize'
+ transaction = self.env['payment.transaction'].create({
+ 'amount': 500,
+ 'acquirer_id': authorize.id,
+ 'type': 'server2server',
+ 'currency_id': self.currency_usd.id,
+ 'reference': 'test_%s' % int(time.time()),
+ 'payment_token_id': payment_token.id,
+ 'partner_id': self.buyer_id,
+
+ })
+ transaction.authorize_s2s_do_transaction()
+ self.assertEqual(transaction.state, 'authorized')
+ transaction.action_capture()
+ self.assertEqual(transaction.state, 'done')
+
+ # create authorize only s2s transaction & void it
+ self.authorize.auto_confirm = 'authorize'
+ transaction = self.env['payment.transaction'].create({
+ 'amount': 500,
+ 'acquirer_id': authorize.id,
+ 'type': 'server2server',
+ 'currency_id': self.currency_usd.id,
+ 'reference': 'test_%s' % int(time.time()),
+ 'payment_token_id': payment_token.id,
+ 'partner_id': self.buyer_id,
+
+ })
+ transaction.authorize_s2s_do_transaction()
+ self.assertEqual(transaction.state, 'authorized')
+ transaction.action_void()
+ self.assertEqual(transaction.state, 'cancel')
diff --git a/addons/payment_authorize/views/payment_authorize_templates.xml b/addons/payment_authorize/views/payment_authorize_templates.xml
index 9c39fcd1c8e..abbbc80b93a 100644
--- a/addons/payment_authorize/views/payment_authorize_templates.xml
+++ b/addons/payment_authorize/views/payment_authorize_templates.xml
@@ -52,5 +52,40 @@
window.location.href = '<t t-esc="return_url"/>';
</script>
</template>
+
+ <template id="authorize_s2s_form">
+ <form method="post" t-att-action="'/payment/authorize/s2s/create' if not json else '/payment/authorize/s2s/create_json'">
+ <div t-att-class="'row' if bootstrap_formatting else ''">
+ <div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cc_number">Card number</label>
+ <input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;"/>
+ <div class="card_placeholder"></div>
+ <div class="visa"></div>
+ <input type="hidden" name="cc_brand" value=""/>
+ </div>
+ <div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cc_cvc">CVC</label>
+ <input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
+ </div>
+ <div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cc_holder_name">Name</label>
+ <input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
+ </div>
+ <div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
+ <label class="control-label" for="cc_expires_mm">Expires</label>
+ <input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
+ </div>
+ <input type="hidden" name="acquirer_id" t-att-value="id"/>
+ <input class="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
+ <input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
+ <input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
+ <div class="clearfix"/>
+ </div>
+ <button t-att-class="submit_class" class="btn btn-primary" type="submit">
+ <span t-if="not submit_txt">Confirm <span class="fa fa-long-arrow-right"></span></span>
+ <span t-if="submit_txt"><t t-esc="submit_txt"/></span>
+ </button>
+ </form>
+ </template>
</data>
</odoo>