[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
This commit is contained in:
@@ -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', '/'))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import payment
|
||||
|
||||
@@ -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
|
||||
@@ -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.')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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="•••• •••• •••• •••• "/>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user