diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py index f93c913ed1d..42947d7a4b9 100644 --- a/addons/payment_ogone/controllers/main.py +++ b/addons/payment_ogone/controllers/main.py @@ -5,6 +5,7 @@ import werkzeug from openerp import http, SUPERUSER_ID from openerp.http import request +from openerp.addons.payment.models.payment_acquirer import ValidationError _logger = logging.getLogger(__name__) @@ -27,3 +28,30 @@ class OgoneController(http.Controller): cr, uid, context = request.cr, SUPERUSER_ID, request.context request.registry['payment.transaction'].form_feedback(cr, uid, post, 'ogone', context=context) return werkzeug.utils.redirect(post.pop('return_url', '/')) + + @http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public') + def ogone_s2s_create_json(self, **kwargs): + data = kwargs['params'] + acquirer_id = int(data.get('acquirer_id')) + acquirer = request.env['payment.acquirer'].browse(acquirer_id) + new_id = acquirer.s2s_process(data) + return new_id + + @http.route(['/payment/ogone/s2s/create'], type='http', auth='public') + def ogone_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', '/')) + + @http.route(['/payment/ogone/s2s/feedback']) + def feedback(self, **kwargs): + cr, uid, context = request.cr, SUPERUSER_ID, request.context + payment = request.registry.get('payment.transaction') + try: + tx = payment._ogone_form_get_tx_from_data(cr, uid, kwargs, context=context) + payment._ogone_s2s_validate(tx) + except ValidationError: + return 'ko' + + return 'ok' diff --git a/addons/payment_ogone/data/ogone.xml b/addons/payment_ogone/data/ogone.xml index bb9c9a59eea..3f0f0844640 100644 --- a/addons/payment_ogone/data/ogone.xml +++ b/addons/payment_ogone/data/ogone.xml @@ -3,10 +3,11 @@ - Credit Card + Ogone ogone + test You will be redirected to the Ogone website after clicking on the payment button.

]]>
diff --git a/addons/payment_ogone/models/ogone.py b/addons/payment_ogone/models/ogone.py index 4276d8d3dcd..f6610544113 100644 --- a/addons/payment_ogone/models/ogone.py +++ b/addons/payment_ogone/models/ogone.py @@ -1,5 +1,6 @@ # -*- coding: utf-'8' "-*-" +import datetime from hashlib import sha1 import logging from lxml import etree, objectify @@ -10,12 +11,14 @@ from urllib import urlencode import urllib2 import urlparse +from openerp import SUPERUSER_ID from openerp.addons.payment.models.payment_acquirer import ValidationError from openerp.addons.payment_ogone.controllers.main import OgoneController from openerp.addons.payment_ogone.data import ogone from openerp.osv import osv, fields -from openerp.tools import float_round +from openerp.tools import float_round, DEFAULT_SERVER_DATE_FORMAT from openerp.tools.float_utils import float_compare +from openerp.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -48,6 +51,10 @@ class PaymentAcquirerOgone(osv.Model): 'ogone_password': fields.char('API User Password', required_if_provider='ogone'), 'ogone_shakey_in': fields.char('SHA Key IN', size=32, required_if_provider='ogone'), 'ogone_shakey_out': fields.char('SHA Key OUT', size=32, required_if_provider='ogone'), + 'ogone_alias_usage': fields.char('Alias Usage', help="""If you want to use Ogone Aliases, + this default Alias Usage will be presented to + the customer as the reason you want to + keep his payment data""") } def _ogone_generate_shasign(self, acquirer, inout, values): @@ -161,6 +168,11 @@ class PaymentAcquirerOgone(osv.Model): 'EXCEPTIONURL': '%s' % urlparse.urljoin(base_url, OgoneController._exception_url), 'CANCELURL': '%s' % urlparse.urljoin(base_url, OgoneController._cancel_url), } + if tx_values.get('type') == 'form_save': + temp_ogone_tx_values.update({ + 'ALIAS': 'ODOO-NEW-ALIAS-%s' % time.time(), # something unique, + 'ALIASUSAGE': tx_values.get('alias_usage') or acquirer.ogone_alias_usage, + }) if ogone_tx_values.get('return_url'): temp_ogone_tx_values['PARAMPLUS'] = 'return_url=%s' % ogone_tx_values.pop('return_url') shasign = self._ogone_generate_shasign(acquirer, 'in', temp_ogone_tx_values) @@ -172,6 +184,31 @@ class PaymentAcquirerOgone(osv.Model): acquirer = self.browse(cr, uid, id, context=context) return self._get_ogone_urls(cr, uid, acquirer.environment, context=context)['ogone_standard_order_url'] + def ogone_s2s_form_validate(self, cr, uid, id, data, context=None): + error = dict() + error_message = [] + + 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' + + return False if error else True + + def ogone_s2s_form_process(self, cr, uid, data, context=None): + values = { + 'cc_number': data.get('cc_number'), + 'cc_cvc': int(data.get('cc_cvc')), + 'cc_holder_name': data.get('cc_holder_name'), + 'cc_expiry': data.get('cc_expiry'), + 'cc_brand': data.get('cc_brand'), + 'acquirer_id': int(data.get('acquirer_id')), + 'partner_id': int(data.get('partner_id')) + } + pm_id = self.pool['payment.method'].create(cr, SUPERUSER_ID, values, context=context) + return pm_id + class PaymentTxOgone(osv.Model): _inherit = 'payment.transaction' @@ -181,21 +218,14 @@ class PaymentTxOgone(osv.Model): _ogone_pending_tx_status = [46] # 3DS HTML response _ogone_cancel_tx_status = [1] - _columns = { - 'ogone_3ds': fields.boolean('3DS Activated'), - 'ogone_3ds_html': fields.html('3DS HTML'), - 'ogone_complus': fields.char('Complus'), - 'ogone_payid': fields.char('PayID', help='Payment ID, generated by Ogone') - } - # -------------------------------------------------- # FORM RELATED METHODS # -------------------------------------------------- def _ogone_form_get_tx_from_data(self, cr, uid, data, context=None): """ Given a data dict coming from ogone, verify it and find the related - transaction record. """ - reference, pay_id, shasign = data.get('orderID'), data.get('PAYID'), data.get('SHASIGN') + transaction record. Create a payment method if an alias is returned.""" + reference, pay_id, shasign, alias = data.get('orderID'), data.get('PAYID'), data.get('SHASIGN'), data.get('ALIAS') if not reference or not pay_id or not shasign: error_msg = _('Ogone: received data with missing reference (%s) or pay_id (%s) or shashign (%s)') % (reference, pay_id, shasign) _logger.info(error_msg) @@ -220,6 +250,20 @@ class PaymentTxOgone(osv.Model): _logger.info(error_msg) raise ValidationError(error_msg) + # alias was created on ogone server, store it + if alias: + method_obj = self.pool['payment.method'] + domain = [('acquirer_ref', '=', alias)] + cardholder = data.get('CN') + if not method_obj.search_count(cr, uid, domain, context=context): + _logger.info('Ogone: saving alias %s for partner %s' % (data.get('CARDNO'), tx.partner_id)) + ref = method_obj.create(cr, uid, {'name': data.get('CARDNO') + (' - ' + cardholder if cardholder else ''), + 'partner_id': tx.partner_id.id, + 'acquirer_id': tx.acquirer_id.id, + 'acquirer_ref': alias + }) + tx.partner_reference = alias + return tx def _ogone_form_get_invalid_parameters(self, cr, uid, tx, data, context=None): @@ -276,195 +320,189 @@ class PaymentTxOgone(osv.Model): # -------------------------------------------------- # S2S RELATED METHODS # -------------------------------------------------- - - def ogone_s2s_create_alias(self, cr, uid, id, values, context=None): - """ Create an alias at Ogone via batch. - - .. versionadded:: pre-v8 saas-3 - .. warning:: - - Experimental code. You should not use it before OpenERP v8 official - release. - """ + def ogone_s2s_do_transaction(self, cr, uid, id, context=None, **kwargs): + # TODO: create tx with s2s type tx = self.browse(cr, uid, id, context=context) - assert tx.type == 'server2server', 'Calling s2s dedicated method for a %s acquirer' % tx.type - alias = 'OPENERP-%d-%d' % (tx.partner_id.id, tx.id) + account = tx.acquirer_id + reference = tx.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), tx.partner_id.id) - expiry_date = '%s%s' % (values['expiry_date_mm'], values['expiry_date_yy'][2:]) - line = 'ADDALIAS;%(alias)s;%(holder_name)s;%(number)s;%(expiry_date)s;%(brand)s;%(pspid)s' - line = line % dict(values, alias=alias, expiry_date=expiry_date, pspid=tx.acquirer_id.ogone_pspid) - - tx_data = { - 'FILE_REFERENCE': 'OPENERP-NEW-ALIAS-%s' % time.time(), # something unique, - 'TRANSACTION_CODE': 'ATR', - 'OPERATION': 'SAL', - 'NB_PAYMENTS': 1, # even if we do not actually have any payment, ogone want it to not be 0 - 'FILE': line, - 'REPLY_TYPE': 'XML', - 'PSPID': tx.acquirer_id.ogone_pspid, - 'USERID': tx.acquirer_id.ogone_userid, - 'PSWD': tx.acquirer_id.ogone_password, - 'PROCESS_MODE': 'CHECKANDPROCESS', - } - - # TODO: fix URL computation - request = urllib2.Request(tx.acquirer_id.ogone_afu_agree_url, urlencode(tx_data)) - result = urllib2.urlopen(request).read() - - try: - tree = objectify.fromstring(result) - except etree.XMLSyntaxError: - _logger.exception('Invalid xml response from ogone') - return None - - error_code = error_str = None - if hasattr(tree, 'PARAMS_ERROR'): - error_code = tree.NCERROR.text - error_str = 'PARAMS ERROR: %s' % (tree.PARAMS_ERROR.text or '',) - else: - node = tree.FORMAT_CHECK - error_node = getattr(node, 'FORMAT_CHECK_ERROR', None) - if error_node is not None: - error_code = error_node.NCERROR.text - error_str = 'CHECK ERROR: %s' % (error_node.ERROR.text or '',) - - if error_code: - error_msg = ogone.OGONE_ERROR_MAP.get(error_code) - error = '%s\n\n%s: %s' % (error_str, error_code, error_msg) - _logger.info(error) - raise Exception(error) # TODO specific exception - - tx.write({'partner_reference': alias}) - return True - - def ogone_s2s_generate_values(self, cr, uid, id, custom_values, context=None): - """ Generate valid Ogone values for a s2s tx. - - .. versionadded:: pre-v8 saas-3 - .. warning:: - - Experimental code. You should not use it before OpenERP v8 official - release. - """ - tx = self.browse(cr, uid, id, context=context) - tx_data = { - 'PSPID': tx.acquirer_id.ogone_pspid, - 'USERID': tx.acquirer_id.ogone_userid, - 'PSWD': tx.acquirer_id.ogone_password, - 'OrderID': tx.reference, - 'amount': '%d' % int(float_round(tx.amount, 2) * 100), # tde check amount or str * 100 ? + data = { + 'PSPID': account.ogone_pspid, + 'USERID': account.ogone_userid, + 'PSWD': account.ogone_password, + 'ORDERID': reference, + 'AMOUNT': long(tx.amount * 100), 'CURRENCY': tx.currency_id.name, - 'LANGUAGE': tx.partner_lang, 'OPERATION': 'SAL', 'ECI': 2, # Recurring (from MOTO) 'ALIAS': tx.partner_reference, 'RTIMEOUT': 30, } - if custom_values.get('ogone_cvc'): - tx_data['CVC'] = custom_values.get('ogone_cvc') - if custom_values.pop('ogone_3ds', None): - tx_data.update({ - 'FLAG3D': 'Y', # YEAH!! + + if kwargs.get('3d_secure'): + data.update({ + 'FLAG3D': 'Y', + 'LANGUAGE': tx.partner_id.lang or 'en_US', }) - if custom_values.get('ogone_complus'): - tx_data['COMPLUS'] = custom_values.get('ogone_complus') - if custom_values.get('ogone_accept_url'): - pass - shasign = self.pool['payment.acquirer']._ogone_generate_shasign(tx.acquirer_id, 'in', tx_data) - tx_data['SHASIGN'] = shasign - return tx_data + for url in 'accept decline exception'.split(): + key = '{0}_url'.format(url) + val = kwargs.pop(key, None) + if val: + key = '{0}URL'.format(url).upper() + data[key] = val - def ogone_s2s_feedback(self, cr, uid, data, context=None): - """ - .. versionadded:: pre-v8 saas-3 - .. warning:: + data['SHASIGN'] = self.pool['payment.acquirer']._ogone_generate_shasign(tx.acquirer_id, 'in', data) - Experimental code. You should not use it before OpenERP v8 official - release. - """ - pass + direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % (tx.acquirer_id.environment) - def ogone_s2s_execute(self, cr, uid, id, values, context=None): - """ - .. versionadded:: pre-v8 saas-3 - .. warning:: - - Experimental code. You should not use it before OpenERP v8 official - release. - """ - tx = self.browse(cr, uid, id, context=context) - - tx_data = self.ogone_s2s_generate_values(cr, uid, id, values, context=context) - _logger.info('Generated Ogone s2s data %s', pformat(tx_data)) # debug - - request = urllib2.Request(tx.acquirer_id.ogone_direct_order_url, urlencode(tx_data)) + _logger.debug("Ogone data %s", pformat(data)) + request = urllib2.Request(direct_order_url, urlencode(data)) result = urllib2.urlopen(request).read() - _logger.info('Contacted Ogone direct order; result %s', result) # debug + _logger.debug('Ogone response = %s', result) - tree = objectify.fromstring(result) - payid = tree.get('PAYID') + try: + tree = objectify.fromstring(result) + except etree.XMLSyntaxError: + # invalid response from ogone + _logger.exception('Invalid xml response from ogone') + raise - query_direct_data = dict( - PSPID=tx.acquirer_id.ogone_pspid, - USERID=tx.acquirer_id.ogone_userid, - PSWD=tx.acquirer_id.ogone_password, - ID=payid, - ) - query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (tx.acquirer_id.environment,) + return self._ogone_s2s_validate_tree(tx, tree) + + def _ogone_s2s_validate(self, tx): + tree = self._ogone_s2s_get_tx_status(tx) + return self.ogone_s2s_validate(tx, tree) + + def _ogone_s2s_validate_tree(self, tx, tree, tries=2): + if tx.state not in ('draft', 'pending'): + return True # already validated + + status = int(tree.get('STATUS', '0')) + if status in self._ogone_valid_tx_status: + tx.write({ + 'state': 'done', + 'date_validate': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT), + 'acquirer_reference': tree.get('PAYID'), + }) + if tx.s2s_cb_eval: + safe_eval(tx.s2s_cb_eval, {'self': tx}) + return True + elif status in self._ogone_cancel_tx_status: + tx.write({ + 'state': 'cancel', + 'acquirer_reference': tree.get('PAYID'), + }) + elif status in self._ogone_pending_tx_status: + tx.write({ + 'state': 'pending', + 'acquirer_reference': tree.get('PAYID'), + 'html_3ds': str(tree.HTML_ANSWER).decode('base64') + }) + elif status in self._ogone_wait_tx_status and tries > 0: + time.sleep(1500) + tx.write({'acquirer_reference': tree.get('PAYID')}) + tree = self._ogone_s2s_get_tx_status(tx) + return self.ogone_s2s_validate(tx, tree, tries - 1) + else: + error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % { + 'error_str': tree.get('NCERROR'), + 'error_code': tree.get('NCERRORPLUS'), + 'error_msg': ogone.OGONE_ERROR_MAP.get(tree.get('NCERRORPLUS')), + } + _logger.info(error) + tx.write({ + 'state': 'error', + 'state_message': error, + 'acquirer_reference': tree.get('PAYID'), + }) + return False + + def _ogone_s2s_get_tx_status(self, tx): + account = tx.acquirer_id + #reference = tx.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%Y%m%d_%H%M%S'), tx.partner_id.id) + + data = { + 'PAYID': tx.acquirer_reference, + 'PSPID': account.ogone_pspid, + 'USERID': account.ogone_userid, + 'PSWD': account.ogone_password, + } + + query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (tx.acquirer_id.environment) + + _logger.debug("Ogone data %s", pformat(data)) + request = urllib2.Request(query_direct_url, urlencode(data)) + result = urllib2.urlopen(request).read() + _logger.debug('Ogone response = %s', result) + + try: + tree = objectify.fromstring(result) + except etree.XMLSyntaxError: + # invalid response from ogone + _logger.exception('Invalid xml response from ogone') + raise + + return tree + + +class PaymentMethod(osv.Model): + _inherit = 'payment.method' + + def ogone_create(self, cr, uid, values, context=None): + if values.get('cc_number'): + # create a alias via batch + values['cc_number'] = values['cc_number'].replace(' ', '') + acquirer = self.pool['payment.acquirer'].browse(cr, uid, values['acquirer_id']) + alias = 'ODOO-NEW-ALIAS-%s' % time.time() + + expiry = str(values['cc_expiry'][:2]) + str(values['cc_expiry'][-2:]) + line = 'ADDALIAS;%(alias)s;%(cc_holder_name)s;%(cc_number)s;%(expiry)s;%(cc_brand)s;%(pspid)s' + line = line % dict(values, alias=alias, expiry=expiry, pspid=acquirer.ogone_pspid) + + data = { + 'FILE_REFERENCE': alias, + 'TRANSACTION_CODE': 'ATR', + 'OPERATION': 'SAL', + 'NB_PAYMENTS': 1, # even if we do not actually have any payment, ogone want it to not be 0 + 'FILE': line, + 'REPLY_TYPE': 'XML', + 'PSPID': acquirer.ogone_pspid, + 'USERID': acquirer.ogone_userid, + 'PSWD': acquirer.ogone_password, + 'PROCESS_MODE': 'CHECKANDPROCESS', + } + + url = 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (acquirer.environment,) + request = urllib2.Request(url, urlencode(data)) + + result = urllib2.urlopen(request).read() - tries = 2 - tx_done = False - tx_status = False - while not tx_done or tries > 0: try: tree = objectify.fromstring(result) except etree.XMLSyntaxError: - # invalid response from ogone _logger.exception('Invalid xml response from ogone') - raise + return None - # see https://secure.ogone.com/ncol/paymentinfos1.asp - VALID_TX = [5, 9] - WAIT_TX = [41, 50, 51, 52, 55, 56, 91, 92, 99] - PENDING_TX = [46] # 3DS HTML response - # other status are errors... - - status = tree.get('STATUS') - if status == '': - status = None + error_code = error_str = None + if hasattr(tree, 'PARAMS_ERROR'): + error_code = tree.NCERROR.text + error_str = 'PARAMS ERROR: %s' % (tree.PARAMS_ERROR.text or '',) else: - status = int(status) + node = tree.FORMAT_CHECK + error_node = getattr(node, 'FORMAT_CHECK_ERROR', None) + if error_node is not None: + error_code = error_node.NCERROR.text + error_str = 'CHECK ERROR: %s' % (error_node.ERROR.text or '',) - if status in VALID_TX: - tx_status = True - tx_done = True + if error_code: + error_msg = tree.get(error_code) + error = '%s\n\n%s: %s' % (error_str, error_code, error_msg) + _logger.error(error) + raise Exception(error) - elif status in PENDING_TX: - html = str(tree.HTML_ANSWER) - tx_data.update(ogone_3ds_html=html.decode('base64')) - tx_status = False - tx_done = True - - elif status in WAIT_TX: - time.sleep(1500) - - request = urllib2.Request(query_direct_url, urlencode(query_direct_data)) - result = urllib2.urlopen(request).read() - _logger.debug('Contacted Ogone query direct; result %s', result) - - else: - error_code = tree.get('NCERROR') - if not ogone.retryable(error_code): - error_str = tree.get('NCERRORPLUS') - error_msg = ogone.OGONE_ERROR_MAP.get(error_code) - error = 'ERROR: %s\n\n%s: %s' % (error_str, error_code, error_msg) - _logger.info(error) - raise Exception(error) - - tries = tries - 1 - - if not tx_done and tries == 0: - raise Exception('Cannot get transaction status...') - - return tx_status + return { + 'acquirer_ref': alias, + 'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']) + } + return {} diff --git a/addons/payment_ogone/views/ogone.xml b/addons/payment_ogone/views/ogone.xml index 48480da23eb..a6dcd4f5624 100644 --- a/addons/payment_ogone/views/ogone.xml +++ b/addons/payment_ogone/views/ogone.xml @@ -1,6 +1,6 @@ - + + + diff --git a/addons/payment_ogone/views/payment_acquirer.xml b/addons/payment_ogone/views/payment_acquirer.xml index f297eb9431a..83eebb91a01 100644 --- a/addons/payment_ogone/views/payment_acquirer.xml +++ b/addons/payment_ogone/views/payment_acquirer.xml @@ -14,29 +14,11 @@ +
- - acquirer.transaction.form.ogone - payment.transaction - - - - - - - - - - - - - - - -