diff options
| author | Damien Bouvy <dbo@odoo.com> | 2016-08-29 15:50:53 +0200 |
|---|---|---|
| committer | Damien Bouvy <dbo@odoo.com> | 2016-09-01 15:58:41 +0200 |
| commit | a2b9b3a6bb39aded694c30620820034f74cd0f77 (patch) | |
| tree | 56ced8e7fa4882a9377880346f1c9b1b0551da9c | |
| parent | ba87ff438d1cdf5e1f5c57563a16a17f6eea6529 (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.py | 14 | ||||
| -rw-r--r-- | addons/payment_authorize/data/payment_acquirer_data.xml | 1 | ||||
| -rw-r--r-- | addons/payment_authorize/models/__init__.py | 1 | ||||
| -rw-r--r-- | addons/payment_authorize/models/authorize_request.py | 247 | ||||
| -rw-r--r-- | addons/payment_authorize/models/payment.py | 146 | ||||
| -rw-r--r-- | addons/payment_authorize/tests/test_authorize.py | 81 | ||||
| -rw-r--r-- | addons/payment_authorize/views/payment_authorize_templates.xml | 35 |
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="•••• •••• •••• •••• "/> + <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> |
