1 # -*- coding: utf-'8' "-*-"
3 from hashlib import sha1
5 from lxml import etree, objectify
6 from pprint import pformat
8 from urllib import urlencode
12 from openerp.addons.payment.models.payment_acquirer import ValidationError
13 from openerp.addons.payment_ogone.controllers.main import OgoneController
14 from openerp.addons.payment_ogone.data import ogone
15 from openerp.osv import osv, fields
16 from openerp.tools import float_round
17 from openerp.tools.float_utils import float_compare
19 _logger = logging.getLogger(__name__)
22 class PaymentAcquirerOgone(osv.Model):
23 _inherit = 'payment.acquirer'
25 def _get_ogone_urls(self, cr, uid, environment, context=None):
28 - standard order: POST address for form-based
33 'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard_utf8.asp' % (environment,),
34 'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect_utf8.asp' % (environment,),
35 'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect_utf8.asp' % (environment,),
36 'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (environment,),
39 def _get_providers(self, cr, uid, context=None):
40 providers = super(PaymentAcquirerOgone, self)._get_providers(cr, uid, context=context)
41 providers.append(['ogone', 'Ogone'])
45 'ogone_pspid': fields.char('PSPID', required_if_provider='ogone'),
46 'ogone_userid': fields.char('API User ID', required_if_provider='ogone'),
47 'ogone_password': fields.char('API User Password', required_if_provider='ogone'),
48 'ogone_shakey_in': fields.char('SHA Key IN', size=32, required_if_provider='ogone'),
49 'ogone_shakey_out': fields.char('SHA Key OUT', size=32, required_if_provider='ogone'),
52 def _ogone_generate_shasign(self, acquirer, inout, values):
53 """ Generate the shasign for incoming or outgoing communications.
55 :param browse acquirer: the payment.acquirer browse record. It should
56 have a shakey in shaky out
57 :param string inout: 'in' (openerp contacting ogone) or 'out' (ogone
58 contacting openerp). In this last case only some
59 fields should be contained (see e-Commerce basic)
60 :param dict values: transaction values
62 :return string: shasign
64 assert inout in ('in', 'out')
65 assert acquirer.provider == 'ogone'
66 key = getattr(acquirer, 'ogone_shakey_' + inout)
73 # source https://viveum.v-psp.com/Ncol/Viveum_e-Com-BAS_EN.pdf
101 'DCC_MARGINPERCENTAGE',
132 return key.upper() in keys
134 items = sorted((k.upper(), v) for k, v in values.items())
135 sign = ''.join('%s=%s%s' % (k, v, key) for k, v in items if v and filter_key(k))
136 sign = sign.encode("utf-8")
137 shasign = sha1(sign).hexdigest()
140 def ogone_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
141 base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
142 acquirer = self.browse(cr, uid, id, context=context)
144 ogone_tx_values = dict(tx_values)
145 temp_ogone_tx_values = {
146 'PSPID': acquirer.ogone_pspid,
147 'ORDERID': tx_values['reference'],
148 'AMOUNT': '%d' % int(float_round(tx_values['amount'], 2) * 100),
149 'CURRENCY': tx_values['currency'] and tx_values['currency'].name or '',
150 'LANGUAGE': partner_values['lang'],
151 'CN': partner_values['name'],
152 'EMAIL': partner_values['email'],
153 'OWNERZIP': partner_values['zip'],
154 'OWNERADDRESS': partner_values['address'],
155 'OWNERTOWN': partner_values['city'],
156 'OWNERCTY': partner_values['country'] and partner_values['country'].name or '',
157 'OWNERTELNO': partner_values['phone'],
158 'ACCEPTURL': '%s' % urlparse.urljoin(base_url, OgoneController._accept_url),
159 'DECLINEURL': '%s' % urlparse.urljoin(base_url, OgoneController._decline_url),
160 'EXCEPTIONURL': '%s' % urlparse.urljoin(base_url, OgoneController._exception_url),
161 'CANCELURL': '%s' % urlparse.urljoin(base_url, OgoneController._cancel_url),
163 if ogone_tx_values.get('return_url'):
164 temp_ogone_tx_values['PARAMPLUS'] = 'return_url=%s' % ogone_tx_values.pop('return_url')
165 shasign = self._ogone_generate_shasign(acquirer, 'in', temp_ogone_tx_values)
166 temp_ogone_tx_values['SHASIGN'] = shasign
167 ogone_tx_values.update(temp_ogone_tx_values)
168 return partner_values, ogone_tx_values
170 def ogone_get_form_action_url(self, cr, uid, id, context=None):
171 acquirer = self.browse(cr, uid, id, context=context)
172 return self._get_ogone_urls(cr, uid, acquirer.environment, context=context)['ogone_standard_order_url']
175 class PaymentTxOgone(osv.Model):
176 _inherit = 'payment.transaction'
178 _ogone_valid_tx_status = [5, 9]
179 _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99]
180 _ogone_pending_tx_status = [46] # 3DS HTML response
181 _ogone_cancel_tx_status = [1]
184 'ogone_3ds': fields.boolean('3DS Activated'),
185 'ogone_3ds_html': fields.html('3DS HTML'),
186 'ogone_complus': fields.char('Complus'),
187 'ogone_payid': fields.char('PayID', help='Payment ID, generated by Ogone')
190 # --------------------------------------------------
191 # FORM RELATED METHODS
192 # --------------------------------------------------
194 def _ogone_form_get_tx_from_data(self, cr, uid, data, context=None):
195 """ Given a data dict coming from ogone, verify it and find the related
196 transaction record. """
197 reference, pay_id, shasign = data.get('orderID'), data.get('PAYID'), data.get('SHASIGN')
198 if not reference or not pay_id or not shasign:
199 error_msg = 'Ogone: received data with missing reference (%s) or pay_id (%s) or shashign (%s)' % (reference, pay_id, shasign)
200 _logger.error(error_msg)
201 raise ValidationError(error_msg)
203 # find tx -> @TDENOTE use paytid ?
204 tx_ids = self.search(cr, uid, [('reference', '=', reference)], context=context)
205 if not tx_ids or len(tx_ids) > 1:
206 error_msg = 'Ogone: received data for reference %s' % (reference)
208 error_msg += '; no order found'
210 error_msg += '; multiple order found'
211 _logger.error(error_msg)
212 raise ValidationError(error_msg)
213 tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
216 shasign_check = self.pool['payment.acquirer']._ogone_generate_shasign(tx.acquirer_id, 'out', data)
217 if shasign_check.upper() != shasign.upper():
218 error_msg = 'Ogone: invalid shasign, received %s, computed %s, for data %s' % (shasign, shasign_check, data)
219 _logger.error(error_msg)
220 raise ValidationError(error_msg)
224 def _ogone_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
225 invalid_parameters = []
227 # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
228 if tx.acquirer_reference and data.get('PAYID') != tx.acquirer_reference:
229 invalid_parameters.append(('PAYID', data.get('PAYID'), tx.acquirer_reference))
230 # check what is buyed
231 if float_compare(float(data.get('amount', '0.0')), tx.amount, 2) != 0:
232 invalid_parameters.append(('amount', data.get('amount'), '%.2f' % tx.amount))
233 if data.get('currency') != tx.currency_id.name:
234 invalid_parameters.append(('currency', data.get('currency'), tx.currency_id.name))
236 return invalid_parameters
238 def _ogone_form_validate(self, cr, uid, tx, data, context=None):
239 if tx.state == 'done':
240 _logger.warning('Ogone: trying to validate an already validated tx (ref %s)' % tx.reference)
243 status = int(data.get('STATUS', '0'))
244 if status in self._ogone_valid_tx_status:
247 'date_validate': data['TRXDATE'],
248 'acquirer_reference': data['PAYID'],
251 elif status in self._ogone_cancel_tx_status:
254 'acquirer_reference': data.get('PAYID'),
256 elif status in self._ogone_pending_tx_status:
259 'acquirer_reference': data.get('PAYID'),
262 error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % {
263 'error_str': data.get('NCERROR'),
264 'error_code': data.get('NCERRORPLUS'),
265 'error_msg': ogone.OGONE_ERROR_MAP.get(data.get('NCERRORPLUS')),
270 'state_message': error,
271 'acquirer_reference': data.get('PAYID'),
275 # --------------------------------------------------
276 # S2S RELATED METHODS
277 # --------------------------------------------------
279 def ogone_s2s_create_alias(self, cr, uid, id, values, context=None):
280 """ Create an alias at Ogone via batch.
282 .. versionadded:: pre-v8 saas-3
285 Experimental code. You should not use it before OpenERP v8 official
288 tx = self.browse(cr, uid, id, context=context)
289 assert tx.type == 'server2server', 'Calling s2s dedicated method for a %s acquirer' % tx.type
290 alias = 'OPENERP-%d-%d' % (tx.partner_id.id, tx.id)
292 expiry_date = '%s%s' % (values['expiry_date_mm'], values['expiry_date_yy'][2:])
293 line = 'ADDALIAS;%(alias)s;%(holder_name)s;%(number)s;%(expiry_date)s;%(brand)s;%(pspid)s'
294 line = line % dict(values, alias=alias, expiry_date=expiry_date, pspid=tx.acquirer_id.ogone_pspid)
297 'FILE_REFERENCE': 'OPENERP-NEW-ALIAS-%s' % time.time(), # something unique,
298 'TRANSACTION_CODE': 'ATR',
300 'NB_PAYMENTS': 1, # even if we do not actually have any payment, ogone want it to not be 0
303 'PSPID': tx.acquirer_id.ogone_pspid,
304 'USERID': tx.acquirer_id.ogone_userid,
305 'PSWD': tx.acquirer_id.ogone_password,
306 'PROCESS_MODE': 'CHECKANDPROCESS',
309 # TODO: fix URL computation
310 request = urllib2.Request(tx.acquirer_id.ogone_afu_agree_url, urlencode(tx_data))
311 result = urllib2.urlopen(request).read()
314 tree = objectify.fromstring(result)
315 except etree.XMLSyntaxError:
316 _logger.exception('Invalid xml response from ogone')
319 error_code = error_str = None
320 if hasattr(tree, 'PARAMS_ERROR'):
321 error_code = tree.NCERROR.text
322 error_str = 'PARAMS ERROR: %s' % (tree.PARAMS_ERROR.text or '',)
324 node = tree.FORMAT_CHECK
325 error_node = getattr(node, 'FORMAT_CHECK_ERROR', None)
326 if error_node is not None:
327 error_code = error_node.NCERROR.text
328 error_str = 'CHECK ERROR: %s' % (error_node.ERROR.text or '',)
331 error_msg = ogone.OGONE_ERROR_MAP.get(error_code)
332 error = '%s\n\n%s: %s' % (error_str, error_code, error_msg)
334 raise Exception(error) # TODO specific exception
336 tx.write({'partner_reference': alias})
339 def ogone_s2s_generate_values(self, cr, uid, id, custom_values, context=None):
340 """ Generate valid Ogone values for a s2s tx.
342 .. versionadded:: pre-v8 saas-3
345 Experimental code. You should not use it before OpenERP v8 official
348 tx = self.browse(cr, uid, id, context=context)
350 'PSPID': tx.acquirer_id.ogone_pspid,
351 'USERID': tx.acquirer_id.ogone_userid,
352 'PSWD': tx.acquirer_id.ogone_password,
353 'OrderID': tx.reference,
354 'amount': '%d' % int(float_round(tx.amount, 2) * 100), # tde check amount or str * 100 ?
355 'CURRENCY': tx.currency_id.name,
356 'LANGUAGE': tx.partner_lang,
358 'ECI': 2, # Recurring (from MOTO)
359 'ALIAS': tx.partner_reference,
362 if custom_values.get('ogone_cvc'):
363 tx_data['CVC'] = custom_values.get('ogone_cvc')
364 if custom_values.pop('ogone_3ds', None):
366 'FLAG3D': 'Y', # YEAH!!
368 if custom_values.get('ogone_complus'):
369 tx_data['COMPLUS'] = custom_values.get('ogone_complus')
370 if custom_values.get('ogone_accept_url'):
373 shasign = self.pool['payment.acquirer']._ogone_generate_shasign(tx.acquirer_id, 'in', tx_data)
374 tx_data['SHASIGN'] = shasign
377 def ogone_s2s_feedback(self, cr, uid, data, context=None):
379 .. versionadded:: pre-v8 saas-3
382 Experimental code. You should not use it before OpenERP v8 official
387 def ogone_s2s_execute(self, cr, uid, id, values, context=None):
389 .. versionadded:: pre-v8 saas-3
392 Experimental code. You should not use it before OpenERP v8 official
395 tx = self.browse(cr, uid, id, context=context)
397 tx_data = self.ogone_s2s_generate_values(cr, uid, id, values, context=context)
398 _logger.info('Generated Ogone s2s data %s', pformat(tx_data)) # debug
400 request = urllib2.Request(tx.acquirer_id.ogone_direct_order_url, urlencode(tx_data))
401 result = urllib2.urlopen(request).read()
402 _logger.info('Contacted Ogone direct order; result %s', result) # debug
404 tree = objectify.fromstring(result)
405 payid = tree.get('PAYID')
407 query_direct_data = dict(
408 PSPID=tx.acquirer_id.ogone_pspid,
409 USERID=tx.acquirer_id.ogone_userid,
410 PSWD=tx.acquirer_id.ogone_password,
413 query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (tx.acquirer_id.environment,)
418 while not tx_done or tries > 0:
420 tree = objectify.fromstring(result)
421 except etree.XMLSyntaxError:
422 # invalid response from ogone
423 _logger.exception('Invalid xml response from ogone')
426 # see https://secure.ogone.com/ncol/paymentinfos1.asp
428 WAIT_TX = [41, 50, 51, 52, 55, 56, 91, 92, 99]
429 PENDING_TX = [46] # 3DS HTML response
430 # other status are errors...
432 status = tree.get('STATUS')
438 if status in VALID_TX:
442 elif status in PENDING_TX:
443 html = str(tree.HTML_ANSWER)
444 tx_data.update(ogone_3ds_html=html.decode('base64'))
448 elif status in WAIT_TX:
451 request = urllib2.Request(query_direct_url, urlencode(query_direct_data))
452 result = urllib2.urlopen(request).read()
453 _logger.debug('Contacted Ogone query direct; result %s', result)
456 error_code = tree.get('NCERROR')
457 if not ogone.retryable(error_code):
458 error_str = tree.get('NCERRORPLUS')
459 error_msg = ogone.OGONE_ERROR_MAP.get(error_code)
460 error = 'ERROR: %s\n\n%s: %s' % (error_str, error_code, error_msg)
462 raise Exception(error)
466 if not tx_done and tries == 0:
467 raise Exception('Cannot get transaction status...')