diff --git a/addons/payment_ingenico/__manifest__.py b/addons/payment_ingenico/__manifest__.py deleted file mode 100644 index fb625787ab7..00000000000 --- a/addons/payment_ingenico/__manifest__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -{ - 'name': 'Ingenico Payment Acquirer', - 'category': 'Accounting/Payment Acquirers', - 'sequence': 360, - 'summary': 'Payment Acquirer: Ingenico Implementation', - 'version': '1.0', - 'description': """Ingenico Payment Acquirer""", - 'depends': ['payment'], - 'data': [ - 'views/payment_views.xml', - 'views/payment_ingenico_templates.xml', - 'data/payment_acquirer_data.xml', - ], - 'installable': True, - 'application': True, - 'post_init_hook': 'create_missing_journal_for_acquirers', - 'uninstall_hook': 'uninstall_hook', -} diff --git a/addons/payment_ingenico/controllers/__init__.py b/addons/payment_ingenico/controllers/__init__.py deleted file mode 100644 index 65a8c12013d..00000000000 --- a/addons/payment_ingenico/controllers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import main diff --git a/addons/payment_ingenico/controllers/main.py b/addons/payment_ingenico/controllers/main.py deleted file mode 100644 index a2bf858f7c6..00000000000 --- a/addons/payment_ingenico/controllers/main.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -import logging -import pprint -import werkzeug -from werkzeug.urls import url_unquote_plus - -from odoo import http -from odoo.http import request -from odoo.addons.payment.models.payment_acquirer import ValidationError -from odoo.addons.payment.controllers.portal import PaymentProcessing - -_logger = logging.getLogger(__name__) - - -class OgoneController(http.Controller): - _accept_url = '/payment/ogone/test/accept' - _decline_url = '/payment/ogone/test/decline' - _exception_url = '/payment/ogone/test/exception' - _cancel_url = '/payment/ogone/test/cancel' - - @http.route([ - '/payment/ogone/accept', '/payment/ogone/test/accept', - '/payment/ogone/decline', '/payment/ogone/test/decline', - '/payment/ogone/exception', '/payment/ogone/test/exception', - '/payment/ogone/cancel', '/payment/ogone/test/cancel', - ], type='http', auth='public', csrf=False, method=['GET', 'POST']) - def ogone_form_feedback(self, **post): - """ Handle both redirection from Ingenico (GET) and s2s notification (POST/GET) """ - _logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug - request.env['payment.transaction'].sudo().form_feedback(post, 'ogone') - return werkzeug.utils.redirect("/payment/process") - - @http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public', csrf=False) - def ogone_s2s_create_json(self, **kwargs): - if not kwargs.get('partner_id'): - kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id) - new_id = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs) - return new_id.id - - @http.route(['/payment/ogone/s2s/create_json_3ds'], type='json', auth='public', csrf=False) - def ogone_s2s_create_json_3ds(self, verify_validity=False, **kwargs): - if not kwargs.get('partner_id'): - kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id) - token = False - error = None - - try: - token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs) - except Exception as e: - error = str(e) - - if not token: - res = { - 'result': False, - 'error': error, - } - return res - - res = { - 'result': True, - 'id': token.id, - 'short_name': token.short_name, - '3d_secure': False, - 'verified': False, - } - - if verify_validity != False: - baseurl = request.env['ir.config_parameter'].sudo().get_param('web.base.url') - params = { - 'accept_url': baseurl + '/payment/ogone/validate/accept', - 'decline_url': baseurl + '/payment/ogone/validate/decline', - 'exception_url': baseurl + '/payment/ogone/validate/exception', - 'return_url': kwargs.get('return_url', baseurl) - } - tx = token.validate(**params) - res['verified'] = token.verified - - if tx and tx.html_3ds: - res['3d_secure'] = tx.html_3ds - - return res - - @http.route(['/payment/ogone/s2s/create'], type='http', auth='public', methods=["POST"], csrf=False) - def ogone_s2s_create(self, **post): - error = '' - acq = request.env['payment.acquirer'].browse(int(post.get('acquirer_id'))) - try: - token = acq.s2s_process(post) - except Exception as e: - # synthax error: 'CHECK ERROR: |Not a valid date\n\n50001111: None' - token = False - error = str(e).splitlines()[0].split('|')[-1] or '' - - if token and post.get('verify_validity'): - baseurl = request.env['ir.config_parameter'].sudo().get_param('web.base.url') - params = { - 'accept_url': baseurl + '/payment/ogone/validate/accept', - 'decline_url': baseurl + '/payment/ogone/validate/decline', - 'exception_url': baseurl + '/payment/ogone/validate/exception', - 'return_url': post.get('return_url', baseurl) - } - tx = token.validate(**params) - if tx and tx.html_3ds: - return tx.html_3ds - # add the payment transaction into the session to let the page /payment/process to handle it - PaymentProcessing.add_payment_transaction(tx) - return werkzeug.utils.redirect("/payment/process") - - @http.route([ - '/payment/ogone/validate/accept', - '/payment/ogone/validate/decline', - '/payment/ogone/validate/exception', - ], type='http', auth='public') - def ogone_validation_form_feedback(self, **post): - """ Feedback from 3d secure for a bank card validation """ - request.env['payment.transaction'].sudo().form_feedback(post, 'ogone') - return werkzeug.utils.redirect("/payment/process") - - @http.route(['/payment/ogone/s2s/feedback'], auth='public', csrf=False) - def feedback(self, **kwargs): - try: - tx = request.env['payment.transaction'].sudo()._ogone_form_get_tx_from_data(kwargs) - tx._ogone_s2s_validate_tree(kwargs) - except ValidationError: - return 'ko' - return 'ok' diff --git a/addons/payment_ingenico/data/__init__.py b/addons/payment_ingenico/data/__init__.py deleted file mode 100644 index f6f4ded2fb1..00000000000 --- a/addons/payment_ingenico/data/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import ogone diff --git a/addons/payment_ingenico/data/ogone.py b/addons/payment_ingenico/data/ogone.py deleted file mode 100644 index 8b3369d9721..00000000000 --- a/addons/payment_ingenico/data/ogone.py +++ /dev/null @@ -1,497 +0,0 @@ -# -*- coding: utf-8 -*- - -OGONE_ERROR_MAP = { - '0020001001': "Authorization failed, please retry", - '0020001002': "Authorization failed, please retry", - '0020001003': "Authorization failed, please retry", - '0020001004': "Authorization failed, please retry", - '0020001005': "Authorization failed, please retry", - '0020001006': "Authorization failed, please retry", - '0020001007': "Authorization failed, please retry", - '0020001008': "Authorization failed, please retry", - '0020001009': "Authorization failed, please retry", - '0020001010': "Authorization failed, please retry", - '0030001999': "Our payment system is currently under maintenance, please try later", - '0050001005': "Expiration Date error", - '0050001007': "Requested Operation code not allowed", - '0050001008': "Invalid delay value", - '0050001010': "Input date in invalid format", - '0050001013': "Unable to parse socket input stream", - '0050001014': "Error in parsing stream content", - '0050001015': "Currency error", - '0050001016': "Transaction still posted at end of wait", - '0050001017': "Sync value not compatible with delay value", - '0050001019': "Transaction duplicate of a pre-existing transaction", - '0050001020': "Acceptation code empty while required for the transaction", - '0050001024': "Maintenance acquirer differs from original transaction acquirer", - '0050001025': "Maintenance merchant differs from original transaction merchant", - '0050001028': "Maintenance operation not accurate for the original transaction", - '0050001031': "Host application unknown for the transaction", - '0050001032': "Unable to perform requested operation with requested currency", - '0050001033': "Maintenance card number differs from original transaction card number", - '0050001034': "Operation code not allowed", - '0050001035': "Exception occurred in socket input stream treatment", - '0050001036': "Card length does not correspond to an acceptable value for the brand", - '0050001036': "Card length does not correspond to an acceptable value for the brand", - '0050001068': "A technical problem occurred, please contact helpdesk", - '0050001069': "Invalid check for CardID and Brand", - '0050001070': "A technical problem occurred, please contact helpdesk", - '0050001116': "Unknown origin IP", - '0050001117': "No origin IP detected", - '0050001118': "Merchant configuration problem, please contact support", - '10001001': "Communication failure", - '10001002': "Communication failure", - '10001003': "Communication failure", - '10001004': "Communication failure", - '10001005': "Communication failure", - '20001001': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001002': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001003': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001004': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001005': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001006': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001007': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001008': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001009': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001010': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001101': "A technical problem occurred, please contact helpdesk", - '20001105': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.", - '20001111': "A technical problem occurred, please contact helpdesk", - '20002001': "Origin for the response of the bank can not be checked", - '20002002': "Beneficiary account number has been modified during processing", - '20002003': "Amount has been modified during processing", - '20002004': "Currency has been modified during processing", - '20002005': "No feedback from the bank server has been detected", - '30001001': "Payment refused by the acquirer", - '30001002': "Duplicate request", - '30001010': "A technical problem occurred, please contact helpdesk", - '30001011': "A technical problem occurred, please contact helpdesk", - '30001012': "Card black listed - Contact acquirer", - '30001015': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.", - '30001051': "A technical problem occurred, please contact helpdesk", - '30001054': "A technical problem occurred, please contact helpdesk", - '30001057': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.", - '30001058': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.", - '30001060': "Aquirer indicates that a failure occured during payment processing", - '30001070': "RATEPAY Invalid Response Type (Failure)", - '30001071': "RATEPAY Missing Mandatory status code field (failure)", - '30001072': "RATEPAY Missing Mandatory Result code field (failure)", - '30001073': "RATEPAY Response parsing Failed", - '30001090': "CVC check required by front end and returned invalid by acquirer", - '30001091': "ZIP check required by front end and returned invalid by acquirer", - '30001092': "Address check required by front end and returned as invalid by acquirer.", - '30001100': "Unauthorized buyer's country", - '30001101': "IP country <> card country", - '30001102': "Number of different countries too high", - '30001103': "unauthorized card country", - '30001104': "unauthorized ip address country", - '30001105': "Anonymous proxy", - '30001110': "If the problem persists, please contact Support, or go to paysafecard's card balance page (https://customer.cc.at.paysafecard.com/psccustomer/GetWelcomePanelServlet?language=en) to see when the amount reserved on your card will be available again.", - '30001120': "IP address in merchant's black list", - '30001130': "BIN in merchant's black list", - '30001131': "Wrong BIN for 3xCB", - '30001140': "Card in merchant's card blacklist", - '30001141': "Email in blacklist", - '30001142': "Passenger name in blacklist", - '30001143': "Card holder name in blacklist", - '30001144': "Passenger name different from owner name", - '30001145': "Time to departure too short", - '30001149': "Card Configured in Card Supplier Limit for another relation (CSL)", - '30001150': "Card not configured in the system for this customer (CSL)", - '30001151': "REF1 not allowed for this relationship (Contract number", - '30001152': "Card/Supplier Amount limit reached (CSL)", - '30001153': "Card not allowed for this supplier (Date out of contract bounds)", - '30001154': "You have reached the usage limit allowed", - '30001155': "You have reached the usage limit allowed", - '30001156': "You have reached the usage limit allowed", - '30001157': "Unauthorized IP country for itinerary", - '30001158': "email usage limit reached", - '30001159': "Unauthorized card country/IP country combination", - '30001160': "Postcode in highrisk group", - '30001161': "generic blacklist match", - '30001162': "Billing Address is a PO Box", - '30001180': "maximum scoring reached", - '30001997': "Authorization canceled by simulation", - '30001998': "A technical problem occurred, please try again.", - '30001999': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.", - '30002001': "Payment refused by the financial institution", - '30002001': "Payment refused by the financial institution", - '30021001': "Call acquirer support call number.", - '30022001': "Payment must be approved by the acquirer before execution.", - '30031001': "Invalid merchant number.", - '30041001': "Retain card.", - '30051001': "Authorization declined", - '30071001': "Retain card - special conditions.", - '30121001': "Invalid transaction", - '30131001': "Invalid amount", - '30131002': "You have reached the total amount allowed", - '30141001': "Invalid card number", - '30151001': "Unknown acquiring institution.", - '30171001': "Payment method cancelled by the buyer", - '30171002': "The maximum time allowed is elapsed.", - '30191001': "Try again later.", - '30201001': "A technical problem occurred, please contact helpdesk", - '30301001': "Invalid format", - '30311001': "Unknown acquirer ID.", - '30331001': "Card expired.", - '30341001': "Suspicion of fraud.", - '30341002': "Suspicion of fraud (3rdMan)", - '30341003': "Suspicion of fraud (Perseuss)", - '30341004': "Suspicion of fraud (ETHOCA)", - '30381001': "A technical problem occurred, please contact helpdesk", - '30401001': "Invalid function.", - '30411001': "Lost card.", - '30431001': "Stolen card, pick up", - '30511001': "Insufficient funds.", - '30521001': "No Authorization. Contact the issuer of your card.", - '30541001': "Card expired.", - '30551001': "Invalid PIN.", - '30561001': "Card not in authorizer's database.", - '30571001': "Transaction not permitted on card.", - '30581001': "Transaction not allowed on this terminal", - '30591001': "Suspicion of fraud.", - '30601001': "The merchant must contact the acquirer.", - '30611001': "Amount exceeds card ceiling.", - '30621001': "Restricted card.", - '30631001': "Security policy not respected.", - '30641001': "Amount changed from ref. trn.", - '30681001': "Tardy response.", - '30751001': "PIN entered incorrectly too often", - '30761001': "Card holder already contesting.", - '30771001': "PIN entry required.", - '30811001': "Message flow error.", - '30821001': "Authorization center unavailable", - '30831001': "Authorization center unavailable", - '30901001': "Temporary system shutdown.", - '30911001': "Acquirer unavailable.", - '30921001': "Invalid card type for acquirer.", - '30941001': "Duplicate transaction", - '30961001': "Processing temporarily not possible", - '30971001': "A technical problem occurred, please contact helpdesk", - '30981001': "A technical problem occurred, please contact helpdesk", - '31011001': "Unknown acceptance code", - '31021001': "Invalid currency", - '31031001': "Acceptance code missing", - '31041001': "Inactive card", - '31051001': "Merchant not active", - '31061001': "Invalid expiration date", - '31071001': "Interrupted host communication", - '31081001': "Card refused", - '31091001': "Invalid password", - '31101001': "Plafond transaction (majoré du bonus) dépassé", - '31111001': "Plafond mensuel (majoré du bonus) dépassé", - '31121001': "Plafond centre de facturation dépassé", - '31131001': "Plafond entreprise dépassé", - '31141001': "Code MCC du fournisseur non autorisé pour la carte", - '31151001': "Numéro SIRET du fournisseur non autorisé pour la carte", - '31161001': "This is not a valid online banking account", - '32001004': "A technical problem occurred, please try again.", - '34011001': "Bezahlung mit RatePAY nicht möglich.", - '39991001': "A technical problem occurred, please contact the helpdesk of your acquirer", - '40001001': "A technical problem occurred, please try again.", - '40001002': "A technical problem occurred, please try again.", - '40001003': "A technical problem occurred, please try again.", - '40001004': "A technical problem occurred, please try again.", - '40001005': "A technical problem occurred, please try again.", - '40001006': "A technical problem occurred, please try again.", - '40001007': "A technical problem occurred, please try again.", - '40001008': "A technical problem occurred, please try again.", - '40001009': "A technical problem occurred, please try again.", - '40001010': "A technical problem occurred, please try again.", - '40001011': "A technical problem occurred, please contact helpdesk", - '40001012': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.", - '40001013': "A technical problem occurred, please contact helpdesk", - '40001016': "A technical problem occurred, please contact helpdesk", - '40001018': "A technical problem occurred, please try again.", - '40001019': "Sorry, an error occurred during processing. Please retry the operation (use back button of the browser). If problem persists, contact your merchant's helpdesk.", - '40001020': "Sorry, an error occurred during processing. Please retry the operation (use back button of the browser). If problem persists, contact your merchant's helpdesk.", - '40001050': "A technical problem occurred, please contact helpdesk", - '40001133': "Authentication failed, the signature of your bank access control server is incorrect", - '40001134': "Authentication failed, please retry or cancel.", - '40001135': "Authentication temporary unavailable, please retry or cancel.", - '40001136': "Technical problem with your browser, please retry or cancel", - '40001137': "Your bank access control server is temporary unavailable, please retry or cancel", - '40001998': "Temporary technical problem. Please retry a little bit later.", - '50001001': "Unknown card type", - '50001002': "Card number format check failed for given card number.", - '50001003': "Merchant data error", - '50001004': "Merchant identification missing", - '50001005': "Expiration Date error", - '50001006': "Amount is not a number", - '50001007': "A technical problem occurred, please contact helpdesk", - '50001008': "A technical problem occurred, please contact helpdesk", - '50001009': "A technical problem occurred, please contact helpdesk", - '50001010': "A technical problem occurred, please contact helpdesk", - '50001011': "Brand not supported for that merchant", - '50001012': "A technical problem occurred, please contact helpdesk", - '50001013': "A technical problem occurred, please contact helpdesk", - '50001014': "A technical problem occurred, please contact helpdesk", - '50001015': "Invalid currency code", - '50001016': "A technical problem occurred, please contact helpdesk", - '50001017': "A technical problem occurred, please contact helpdesk", - '50001018': "A technical problem occurred, please contact helpdesk", - '50001019': "A technical problem occurred, please contact helpdesk", - '50001020': "A technical problem occurred, please contact helpdesk", - '50001021': "A technical problem occurred, please contact helpdesk", - '50001022': "A technical problem occurred, please contact helpdesk", - '50001023': "A technical problem occurred, please contact helpdesk", - '50001024': "A technical problem occurred, please contact helpdesk", - '50001025': "A technical problem occurred, please contact helpdesk", - '50001026': "A technical problem occurred, please contact helpdesk", - '50001027': "A technical problem occurred, please contact helpdesk", - '50001028': "A technical problem occurred, please contact helpdesk", - '50001029': "A technical problem occurred, please contact helpdesk", - '50001030': "A technical problem occurred, please contact helpdesk", - '50001031': "A technical problem occurred, please contact helpdesk", - '50001032': "A technical problem occurred, please contact helpdesk", - '50001033': "A technical problem occurred, please contact helpdesk", - '50001034': "A technical problem occurred, please contact helpdesk", - '50001035': "A technical problem occurred, please contact helpdesk", - '50001036': "Card length does not correspond to an acceptable value for the brand", - '50001037': "Purchasing card number for a regular merchant", - '50001038': "Non Purchasing card for a Purchasing card merchant", - '50001039': "Details sent for a non-Purchasing card merchant, please contact helpdesk", - '50001040': "Details not sent for a Purchasing card transaction, please contact helpdesk", - '50001041': "Payment detail validation failed", - '50001042': "Given transactions amounts (tax,discount,shipping,net,etc…) do not compute correctly together", - '50001043': "A technical problem occurred, please contact helpdesk", - '50001044': "No acquirer configured for this operation", - '50001045': "No UID configured for this operation", - '50001046': "Operation not allowed for the merchant", - '50001047': "A technical problem occurred, please contact helpdesk", - '50001048': "A technical problem occurred, please contact helpdesk", - '50001049': "A technical problem occurred, please contact helpdesk", - '50001050': "A technical problem occurred, please contact helpdesk", - '50001051': "A technical problem occurred, please contact helpdesk", - '50001052': "A technical problem occurred, please contact helpdesk", - '50001053': "A technical problem occurred, please contact helpdesk", - '50001054': "Card number incorrect or incompatible", - '50001055': "A technical problem occurred, please contact helpdesk", - '50001056': "A technical problem occurred, please contact helpdesk", - '50001057': "A technical problem occurred, please contact helpdesk", - '50001058': "A technical problem occurred, please contact helpdesk", - '50001059': "A technical problem occurred, please contact helpdesk", - '50001060': "A technical problem occurred, please contact helpdesk", - '50001061': "A technical problem occurred, please contact helpdesk", - '50001062': "A technical problem occurred, please contact helpdesk", - '50001063': "Card Issue Number does not correspond to range or not present", - '50001064': "Start Date not valid or not present", - '50001066': "Format of CVC code invalid", - '50001067': "The merchant is not enrolled for 3D-Secure", - '50001068': "The card number or account number (PAN) is invalid", - '50001069': "Invalid check for CardID and Brand", - '50001070': "The ECI value given is either not supported, or in conflict with other data in the transaction", - '50001071': "Incomplete TRN demat", - '50001072': "Incomplete PAY demat", - '50001073': "No demat APP", - '50001074': "Authorisation too old", - '50001075': "VERRes was an error message", - '50001076': "DCP amount greater than authorisation amount", - '50001077': "Details negative amount", - '50001078': "Details negative quantity", - '50001079': "Could not decode/decompress received PARes (3D-Secure)", - '50001080': "Received PARes was an erereor message from ACS (3D-Secure)", - '50001081': "Received PARes format was invalid according to the 3DS specifications (3D-Secure)", - '50001082': "PAReq/PARes reconciliation failure (3D-Secure)", - '50001084': "Maximum amount reached", - '50001087': "The transaction type requires authentication, please check with your bank.", - '50001090': "CVC missing at input, but CVC check asked", - '50001091': "ZIP missing at input, but ZIP check asked", - '50001092': "Address missing at input, but Address check asked", - '50001095': "Invalid date of birth", - '50001096': "Invalid commodity code", - '50001097': "The requested currency and brand are incompatible.", - '50001111': "Data validation error", - '50001113': "This order has already been processed", - '50001114': "Error pre-payment check page access", - '50001115': "Request not received in secure mode", - '50001116': "Unknown IP address origin", - '50001117': "NO IP address origin", - '50001118': "Pspid not found or not correct", - '50001119': "Password incorrect or disabled due to numbers of errors", - '50001120': "Invalid currency", - '50001121': "Invalid number of decimals for the currency", - '50001122': "Currency not accepted by the merchant", - '50001123': "Card type not active", - '50001124': "Number of lines don't match with number of payments", - '50001125': "Format validation error", - '50001126': "Overflow in data capture requests for the original order", - '50001127': "The original order is not in a correct status", - '50001128': "missing authorization code for unauthorized order", - '50001129': "Overflow in refunds requests", - '50001130': "Error access to original order", - '50001131': "Error access to original history item", - '50001132': "The Selected Catalog is empty", - '50001133': "Duplicate request", - '50001134': "Authentication failed, please retry or cancel.", - '50001135': "Authentication temporary unavailable, please retry or cancel.", - '50001136': "Technical problem with your browser, please retry or cancel", - '50001137': "Your bank access control server is temporary unavailable, please retry or cancel", - '50001150': "Fraud Detection, Technical error (IP not valid)", - '50001151': "Fraud detection : technical error (IPCTY unknown or error)", - '50001152': "Fraud detection : technical error (CCCTY unknown or error)", - '50001153': "Overflow in redo-authorisation requests", - '50001170': "Dynamic BIN check failed", - '50001171': "Dynamic country check failed", - '50001172': "Error in Amadeus signature", - '50001174': "Card Holder Name is too long", - '50001175': "Name contains invalid characters", - '50001176': "Card number is too long", - '50001177': "Card number contains non-numeric info", - '50001178': "Card Number Empty", - '50001179': "CVC too long", - '50001180': "CVC contains non-numeric info", - '50001181': "Expiration date contains non-numeric info", - '50001182': "Invalid expiration month", - '50001183': "Expiration date must be in the future", - '50001184': "SHA Mismatch", - '50001205': "Missing mandatory fields for billing address.", - '50001206': "Missing mandatory field date of birth.", - '50001207': "Missing required shopping basket details.", - '50001208': "Missing social security number", - '50001209': "Invalid country code", - '50001210': "Missing yearly salary", - '50001211': "Missing gender", - '50001212': "Missing email", - '50001213': "Missing IP address", - '50001214': "Missing part payment campaign ID", - '50001215': "Missing invoice number", - '50001216': "The alias must be different than the card number", - '60000001': "account number unknown", - '60000003': "not credited dd-mm-yy", - '60000005': "name/number do not correspond", - '60000007': "account number blocked", - '60000008': "specific direct debit block", - '60000009': "account number WKA", - '60000010': "administrative reason", - '60000011': "account number expired", - '60000012': "no direct debit authorisation given", - '60000013': "debit not approved", - '60000014': "double payment", - '60000018': "name/address/city not entered", - '60001001': "no original direct debit for revocation", - '60001002': "payer’s account number format error", - '60001004': "payer’s account at different bank", - '60001005': "payee’s account at different bank", - '60001006': "payee’s account number format error", - '60001007': "payer’s account number blocked", - '60001008': "payer’s account number expired", - '60001009': "payee’s account number expired", - '60001010': "direct debit not possible", - '60001011': "creditor payment not possible", - '60001012': "payer’s account number unknown WKA-number", - '60001013': "payee’s account number unknown WKA-number", - '60001014': "impermissible WKA transaction", - '60001015': "period for revocation expired", - '60001017': "reason for revocation not correct", - '60001018': "original run number not numeric", - '60001019': "payment ID incorrect", - '60001020': "amount not numeric", - '60001021': "amount zero not permitted", - '60001022': "negative amount not permitted", - '60001023': "payer and payee giro account number", - '60001025': "processing code (verwerkingscode) incorrect", - '60001028': "revocation not permitted", - '60001029': "guaranteed direct debit on giro account number", - '60001030': "NBC transaction type incorrect", - '60001031': "description too large", - '60001032': "book account number not issued", - '60001034': "book account number incorrect", - '60001035': "payer’s account number not numeric", - '60001036': "payer’s account number not eleven-proof", - '60001037': "payer’s account number not issued", - '60001039': "payer’s account number of DNB/BGC/BLA", - '60001040': "payee’s account number not numeric", - '60001041': "payee’s account number not eleven-proof", - '60001042': "payee’s account number not issued", - '60001044': "payee’s account number unknown", - '60001050': "payee’s name missing", - '60001051': "indicate payee’s bank account number instead of 3102", - '60001052': "no direct debit contract", - '60001053': "amount beyond bounds", - '60001054': "selective direct debit block", - '60001055': "original run number unknown", - '60001057': "payer’s name missing", - '60001058': "payee’s account number missing", - '60001059': "restore not permitted", - '60001060': "bank’s reference (navraaggegeven) missing", - '60001061': "BEC/GBK number incorrect", - '60001062': "BEC/GBK code incorrect", - '60001087': "book account number not numeric", - '60001090': "cancelled on request", - '60001091': "cancellation order executed", - '60001092': "cancelled instead of bended", - '60001093': "book account number is a shortened account number", - '60001094': "instructing party account number not identical with payer", - '60001095': "payee unknown GBK acceptor", - '60001097': "instructing party account number not identical with payee", - '60001099': "clearing not permitted", - '60001101': "payer’s account number not spaces", - '60001102': "PAN length not numeric", - '60001103': "PAN length outside limits", - '60001104': "track number not numeric", - '60001105': "track number not valid", - '60001106': "PAN sequence number not numeric", - '60001107': "domestic PAN not numeric", - '60001108': "domestic PAN not eleven-proof", - '60001109': "domestic PAN not issued", - '60001110': "foreign PAN not numeric", - '60001111': "card valid date not numeric", - '60001112': "book period number (boekperiodenr) not numeric", - '60001113': "transaction number not numeric", - '60001114': "transaction time not numeric", - '60001115': "transaction no valid time", - '60001116': "transaction date not numeric", - '60001117': "transaction no valid date", - '60001118': "STAN not numeric", - '60001119': "instructing party’s name missing", - '60001120': "foreign amount (bedrag-vv) not numeric", - '60001122': "rate (verrekenkoers) not numeric", - '60001125': "number of decimals (aantaldecimalen) incorrect", - '60001126': "tariff (tarifering) not B/O/S", - '60001127': "domestic costs (kostenbinnenland) not numeric", - '60001128': "domestic costs (kostenbinnenland) not higher than zero", - '60001129': "foreign costs (kostenbuitenland) not numeric", - '60001130': "foreign costs (kostenbuitenland) not higher than zero", - '60001131': "domestic costs (kostenbinnenland) not zero", - '60001132': "foreign costs (kostenbuitenland) not zero", - '60001134': "Euro record not fully filled in", - '60001135': "Client currency incorrect", - '60001136': "Amount NLG not numeric", - '60001137': "Amount NLG not higher than zero", - '60001138': "Amount NLG not equal to Amount", - '60001139': "Amount NLG incorrectly converted", - '60001140': "Amount EUR not numeric", - '60001141': "Amount EUR not greater than zero", - '60001142': "Amount EUR not equal to Amount", - '60001143': "Amount EUR incorrectly converted", - '60001144': "Client currency not NLG", - '60001145': "rate euro-vv (Koerseuro-vv) not numeric", - '60001146': "comma rate euro-vv (Kommakoerseuro-vv) incorrect", - '60001147': "acceptgiro distributor not valid", - '60001148': "Original run number and/or BRN are missing", - '60001149': "Amount/Account number/ BRN different", - '60001150': "Direct debit already revoked/restored", - '60001151': "Direct debit already reversed/revoked/restored", - '60001153': "Payer’s account number not known", -} - -DATA_VALIDATION_ERROR = '50001111' - - -def retryable(error): - return error in [ - '0020001001', '0020001002', '0020001003', '0020001004', '0020001005', - '0020001006', '0020001007', '0020001008', '0020001009', '0020001010', - '30001010', '30001011', '30001015', - '30001057', '30001058', - '30001998', '30001999', - #'30611001', # amount exceeds card limit - '30961001', - '40001001', '40001002', '40001003', '40001004', '40001005', - '40001006', '40001007', '40001008', '40001009', '40001010', - '40001012', - '40001018', '40001019', '40001020', - '40001134', '40001135', '40001136', '40001137', - #'50001174', # cardholder name too long - ] diff --git a/addons/payment_ingenico/data/payment_acquirer_data.xml b/addons/payment_ingenico/data/payment_acquirer_data.xml deleted file mode 100644 index b1dc3843130..00000000000 --- a/addons/payment_ingenico/data/payment_acquirer_data.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Ingenico - - ogone - - - - - - - diff --git a/addons/payment_ingenico/models/__init__.py b/addons/payment_ingenico/models/__init__.py deleted file mode 100644 index ef12533682f..00000000000 --- a/addons/payment_ingenico/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import payment diff --git a/addons/payment_ingenico/models/payment.py b/addons/payment_ingenico/models/payment.py deleted file mode 100644 index 6076088aec1..00000000000 --- a/addons/payment_ingenico/models/payment.py +++ /dev/null @@ -1,597 +0,0 @@ -# coding: utf-8 -import base64 -import datetime -import logging -import time -from hashlib import sha1 -from pprint import pformat -from unicodedata import normalize - -import requests -from lxml import etree, objectify -from werkzeug import urls - -from odoo import api, fields, models, _ -from odoo.addons.payment.models.payment_acquirer import ValidationError -from odoo.addons.payment_ingenico.controllers.main import OgoneController -from odoo.addons.payment_ingenico.data import ogone -from odoo.http import request -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, ustr -from odoo.tools.float_utils import float_compare, float_repr, float_round - -_logger = logging.getLogger(__name__) - - -class PaymentAcquirerOgone(models.Model): - _inherit = 'payment.acquirer' - - provider = fields.Selection(selection_add=[ - ('ogone', 'Ingenico') - ], ondelete={'ogone': 'set default'}) - ogone_pspid = fields.Char('PSPID', required_if_provider='ogone', groups='base.group_user') - ogone_userid = fields.Char('API User ID', required_if_provider='ogone', groups='base.group_user') - ogone_password = fields.Char('API User Password', required_if_provider='ogone', groups='base.group_user') - ogone_shakey_in = fields.Char('SHA Key IN', size=32, required_if_provider='ogone', groups='base.group_user') - ogone_shakey_out = fields.Char('SHA Key OUT', size=32, required_if_provider='ogone', groups='base.group_user') - ogone_alias_usage = fields.Char('Alias Usage', default="Allow saving my payment data", - 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 _get_feature_support(self): - """Get advanced feature support by provider. - - Each provider should add its technical in the corresponding - key for the following features: - * fees: support payment fees computations - * authorize: support authorizing payment (separates - authorization and capture) - * tokenize: support saving payment data in a payment.tokenize - object - """ - res = super(PaymentAcquirerOgone, self)._get_feature_support() - res['tokenize'].append('ogone') - return res - - def _get_ogone_urls(self, environment): - """ Ogone URLS: - - standard order: POST address for form-based """ - return { - 'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard_utf8.asp' % (environment,), - 'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect_utf8.asp' % (environment,), - 'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect_utf8.asp' % (environment,), - 'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (environment,), - } - - def _ogone_generate_shasign(self, inout, values): - """ Generate the shasign for incoming or outgoing communications. - - :param string inout: 'in' (odoo contacting ogone) or 'out' (ogone - contacting odoo). In this last case only some - fields should be contained (see e-Commerce basic) - :param dict values: transaction values - - :return string: shasign - """ - assert inout in ('in', 'out') - assert self.provider == 'ogone' - key = getattr(self, 'ogone_shakey_' + inout) - - def filter_key(key): - if inout == 'in': - return True - else: - # SHA-OUT keys - # source https://payment-services.ingenico.com/int/en/ogone/support/guides/integration guides/e-commerce/transaction-feedback - keys = [ - 'AAVADDRESS', - 'AAVCHECK', - 'AAVMAIL', - 'AAVNAME', - 'AAVPHONE', - 'AAVZIP', - 'ACCEPTANCE', - 'ALIAS', - 'AMOUNT', - 'BIC', - 'BIN', - 'BRAND', - 'CARDNO', - 'CCCTY', - 'CN', - 'COLLECTOR_BIC', - 'COLLECTOR_IBAN', - 'COMPLUS', - 'CREATION_STATUS', - 'CREDITDEBIT', - 'CURRENCY', - 'CVCCHECK', - 'DCC_COMMPERCENTAGE', - 'DCC_CONVAMOUNT', - 'DCC_CONVCCY', - 'DCC_EXCHRATE', - 'DCC_EXCHRATESOURCE', - 'DCC_EXCHRATETS', - 'DCC_INDICATOR', - 'DCC_MARGINPERCENTAGE', - 'DCC_VALIDHOURS', - 'DEVICEID', - 'DIGESTCARDNO', - 'ECI', - 'ED', - 'EMAIL', - 'ENCCARDNO', - 'FXAMOUNT', - 'FXCURRENCY', - 'IP', - 'IPCTY', - 'MANDATEID', - 'MOBILEMODE', - 'NBREMAILUSAGE', - 'NBRIPUSAGE', - 'NBRIPUSAGE_ALLTX', - 'NBRUSAGE', - 'NCERROR', - 'ORDERID', - 'PAYID', - 'PAYIDSUB', - 'PAYMENT_REFERENCE', - 'PM', - 'SCO_CATEGORY', - 'SCORING', - 'SEQUENCETYPE', - 'SIGNDATE', - 'STATUS', - 'SUBBRAND', - 'SUBSCRIPTION_ID', - 'TICKET', - 'TRXDATE', - 'VC', - ] - return key.upper() in keys - - items = sorted((k.upper(), v) for k, v in values.items()) - sign = ''.join('%s=%s%s' % (k, v, key) for k, v in items if v and filter_key(k)) - sign = sign.encode("utf-8") - shasign = sha1(sign).hexdigest() - return shasign - - def ogone_form_generate_values(self, values): - base_url = self.get_base_url() - ogone_tx_values = dict(values) - param_plus = { - 'return_url': ogone_tx_values.pop('return_url', False) - } - temp_ogone_tx_values = { - 'PSPID': self.ogone_pspid, - 'ORDERID': values['reference'], - 'AMOUNT': float_repr(float_round(values['amount'], 2) * 100, 0), - 'CURRENCY': values['currency'] and values['currency'].name or '', - 'LANGUAGE': values.get('partner_lang'), - 'CN': values.get('partner_name'), - 'EMAIL': values.get('partner_email'), - 'OWNERZIP': values.get('partner_zip'), - 'OWNERADDRESS': values.get('partner_address'), - 'OWNERTOWN': values.get('partner_city'), - 'OWNERCTY': values.get('partner_country') and values.get('partner_country').code or '', - 'OWNERTELNO': values.get('partner_phone'), - 'ACCEPTURL': urls.url_join(base_url, OgoneController._accept_url), - 'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url), - 'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url), - 'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url), - 'PARAMPLUS': urls.url_encode(param_plus), - } - if self.save_token in ['ask', 'always']: - temp_ogone_tx_values.update({ - 'ALIAS': 'ODOO-NEW-ALIAS-%s' % time.time(), # something unique, - 'ALIASUSAGE': values.get('alias_usage') or self.ogone_alias_usage, - }) - shasign = self._ogone_generate_shasign('in', temp_ogone_tx_values) - temp_ogone_tx_values['SHASIGN'] = shasign - ogone_tx_values.update(temp_ogone_tx_values) - return ogone_tx_values - - def ogone_get_form_action_url(self): - self.ensure_one() - environment = 'prod' if self.state == 'enabled' else 'test' - return self._get_ogone_urls(environment)['ogone_standard_order_url'] - - def ogone_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' - - return False if error else True - - def ogone_s2s_form_process(self, data): - 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.env['payment.token'].sudo().create(values) - return pm_id - - -class PaymentTxOgone(models.Model): - _inherit = 'payment.transaction' - # ogone status - _ogone_valid_tx_status = [5, 9, 8] - _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99] - _ogone_pending_tx_status = [46, 81, 82] # 46 = 3DS HTML response - _ogone_cancel_tx_status = [1] - - # -------------------------------------------------- - # FORM RELATED METHODS - # -------------------------------------------------- - - @api.model - def _ogone_form_get_tx_from_data(self, data): - """ Given a data dict coming from ogone, verify it and find the related - transaction record. Create a payment token 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 shasign (%s)') % (reference, pay_id, shasign) - _logger.info(error_msg) - raise ValidationError(error_msg) - - # find tx -> @TDENOTE use paytid ? - tx = self.search([('reference', '=', reference)]) - if not tx or len(tx) > 1: - error_msg = _('Ogone: received data for reference %s') % (reference) - if not tx: - error_msg += _('; no order found') - else: - error_msg += _('; multiple order found') - _logger.info(error_msg) - raise ValidationError(error_msg) - - # verify shasign - shasign_check = tx.acquirer_id._ogone_generate_shasign('out', data) - if shasign_check.upper() != shasign.upper(): - error_msg = _('Ogone: invalid shasign, received %s, computed %s, for data %s') % (shasign, shasign_check, data) - _logger.info(error_msg) - raise ValidationError(error_msg) - - if not tx.acquirer_reference: - tx.acquirer_reference = pay_id - - # alias was created on ogone server, store it - if alias and tx.type == 'form_save': - Token = self.env['payment.token'] - domain = [('acquirer_ref', '=', alias)] - cardholder = data.get('CN') - if not Token.search_count(domain): - _logger.info('Ogone: saving alias %s for partner %s' % (data.get('CARDNO'), tx.partner_id)) - ref = Token.create({'name': data.get('CARDNO') + (' - ' + cardholder if cardholder else ''), - 'partner_id': tx.partner_id.id, - 'acquirer_id': tx.acquirer_id.id, - 'acquirer_ref': alias}) - tx.write({'payment_token_id': ref.id}) - - return tx - - def _ogone_form_get_invalid_parameters(self, data): - invalid_parameters = [] - - # TODO: txn_id: should be false at draft, set afterwards, and verified with txn details - if self.acquirer_reference and data.get('PAYID') != self.acquirer_reference: - invalid_parameters.append(('PAYID', data.get('PAYID'), self.acquirer_reference)) - # check what is bought - if float_compare(float(data.get('amount', '0.0')), self.amount, 2) != 0: - invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount)) - if data.get('currency') != self.currency_id.name: - invalid_parameters.append(('currency', data.get('currency'), self.currency_id.name)) - - return invalid_parameters - - def _ogone_form_validate(self, data): - if self.state not in ['draft', 'pending']: - _logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference) - return True - - status = int(data.get('STATUS', '0')) - if status in self._ogone_valid_tx_status: - vals = { - 'date': datetime.datetime.strptime(data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT), - 'acquirer_reference': data['PAYID'], - } - if data.get('ALIAS') and self.partner_id and \ - (self.type == 'form_save' or self.acquirer_id.save_token == 'always')\ - and not self.payment_token_id: - pm = self.env['payment.token'].create({ - 'partner_id': self.partner_id.id, - 'acquirer_id': self.acquirer_id.id, - 'acquirer_ref': data.get('ALIAS'), - 'name': '%s - %s' % (data.get('CARDNO'), data.get('CN')) - }) - vals.update(payment_token_id=pm.id) - self.write(vals) - if self.payment_token_id: - self.payment_token_id.verified = True - self._set_transaction_done() - self.execute_callback() - # if this transaction is a validation one, then we refund the money we just withdrawn - if self.type == 'validation': - self.s2s_do_refund() - - return True - elif status in self._ogone_cancel_tx_status: - self.write({'acquirer_reference': data.get('PAYID')}) - self._set_transaction_cancel() - elif status in self._ogone_pending_tx_status or status in self._ogone_wait_tx_status: - self.write({'acquirer_reference': data.get('PAYID')}) - self._set_transaction_pending() - else: - error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % { - 'error_str': data.get('NCERRORPLUS'), - 'error_code': data.get('NCERROR'), - 'error_msg': ogone.OGONE_ERROR_MAP.get(data.get('NCERROR')), - } - _logger.info(error) - self.write({ - 'state_message': error, - 'acquirer_reference': data.get('PAYID'), - }) - self._set_transaction_cancel() - return False - - # -------------------------------------------------- - # S2S RELATED METHODS - # -------------------------------------------------- - def ogone_s2s_do_transaction(self, **kwargs): - # TODO: create tx with s2s type - account = self.acquirer_id - reference = self.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), self.partner_id.id) - - param_plus = { - 'return_url': kwargs.get('return_url', False) - } - - data = { - 'PSPID': account.ogone_pspid, - 'USERID': account.ogone_userid, - 'PSWD': account.ogone_password, - 'ORDERID': reference, - 'AMOUNT': int(self.amount * 100), - 'CURRENCY': self.currency_id.name, - 'OPERATION': 'SAL', - 'ECI': 9, # Recurring (from eCommerce) - 'ALIAS': self.payment_token_id.acquirer_ref, - 'RTIMEOUT': 30, - 'PARAMPLUS': urls.url_encode(param_plus), - 'EMAIL': self.partner_id.email or '', - 'CN': self.partner_id.name or '', - } - - if request: - data['REMOTE_ADDR'] = request.httprequest.remote_addr - - if kwargs.get('3d_secure'): - data.update({ - 'FLAG3D': 'Y', - 'LANGUAGE': self.partner_id.lang or 'en_US', - }) - - 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 - - data['SHASIGN'] = self.acquirer_id._ogone_generate_shasign('in', data) - - direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % ('prod' if self.acquirer_id.state == 'enabled' else 'test') - - logged_data = data.copy() - logged_data.pop('PSWD') - _logger.info("ogone_s2s_do_transaction: Sending values to URL %s, values:\n%s", direct_order_url, pformat(logged_data)) - result = requests.post(direct_order_url, data=data).content - - try: - tree = objectify.fromstring(result) - _logger.info('ogone_s2s_do_transaction: Values received:\n%s', etree.tostring(tree, pretty_print=True, encoding='utf-8')) - except etree.XMLSyntaxError: - # invalid response from ogone - _logger.exception('Invalid xml response from ogone') - _logger.info('ogone_s2s_do_transaction: Values received:\n%s', result) - raise - - return self._ogone_s2s_validate_tree(tree) - - def ogone_s2s_do_refund(self, **kwargs): - account = self.acquirer_id - reference = self.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), self.partner_id.id) - - data = { - 'PSPID': account.ogone_pspid, - 'USERID': account.ogone_userid, - 'PSWD': account.ogone_password, - 'ORDERID': reference, - 'AMOUNT': int(self.amount * 100), - 'CURRENCY': self.currency_id.name, - 'OPERATION': 'RFS', - 'PAYID': self.acquirer_reference, - } - data['SHASIGN'] = self.acquirer_id._ogone_generate_shasign('in', data) - - direct_order_url = 'https://secure.ogone.com/ncol/%s/maintenancedirect.asp' % ('prod' if self.acquirer_id.state == 'enabled' else 'test') - - logged_data = data.copy() - logged_data.pop('PSWD') - _logger.info("ogone_s2s_do_refund: Sending values to URL %s, values:\n%s", direct_order_url, pformat(logged_data)) - result = requests.post(direct_order_url, data=data).content - - try: - tree = objectify.fromstring(result) - _logger.info('ogone_s2s_do_refund: Values received:\n%s', etree.tostring(tree, pretty_print=True, encoding='utf-8')) - except etree.XMLSyntaxError: - # invalid response from ogone - _logger.exception('Invalid xml response from ogone') - _logger.info('ogone_s2s_do_refund: Values received:\n%s', result) - raise - - return self._ogone_s2s_validate_tree(tree) - - def _ogone_s2s_validate(self): - tree = self._ogone_s2s_get_tx_status() - return self._ogone_s2s_validate_tree(tree) - - def _ogone_s2s_validate_tree(self, tree, tries=2): - if self.state not in ['draft', 'pending']: - _logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference) - return True - - status = int(tree.get('STATUS') or 0) - if status in self._ogone_valid_tx_status: - self.write({ - 'date': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT), - 'acquirer_reference': tree.get('PAYID'), - }) - if tree.get('ALIAS') and self.partner_id and \ - (self.type == 'form_save' or self.acquirer_id.save_token == 'always')\ - and not self.payment_token_id: - pm = self.env['payment.token'].create({ - 'partner_id': self.partner_id.id, - 'acquirer_id': self.acquirer_id.id, - 'acquirer_ref': tree.get('ALIAS'), - 'name': tree.get('CARDNO'), - }) - self.write({'payment_token_id': pm.id}) - if self.payment_token_id: - self.payment_token_id.verified = True - self._set_transaction_done() - self.execute_callback() - # if this transaction is a validation one, then we refund the money we just withdrawn - if self.type == 'validation': - self.s2s_do_refund() - return True - elif status in self._ogone_cancel_tx_status: - self.write({'acquirer_reference': tree.get('PAYID')}) - self._set_transaction_cancel() - elif status in self._ogone_pending_tx_status: - vals = { - 'acquirer_reference': tree.get('PAYID'), - } - if status == 46: # HTML 3DS - vals['html_3ds'] = ustr(base64.b64decode(tree.HTML_ANSWER.text)) - self.write(vals) - self._set_transaction_pending() - elif status in self._ogone_wait_tx_status and tries > 0: - time.sleep(0.5) - self.write({'acquirer_reference': tree.get('PAYID')}) - tree = self._ogone_s2s_get_tx_status() - return self._ogone_s2s_validate_tree(tree, tries - 1) - else: - error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % { - 'error_str': tree.get('NCERRORPLUS'), - 'error_code': tree.get('NCERROR'), - 'error_msg': ogone.OGONE_ERROR_MAP.get(tree.get('NCERROR')), - } - _logger.info(error) - self.write({ - 'state_message': error, - 'acquirer_reference': tree.get('PAYID'), - }) - self._set_transaction_cancel() - return False - - def _ogone_s2s_get_tx_status(self): - account = self.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': self.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' % ('prod' if self.acquirer_id.state == 'enabled' else 'test') - - logged_data = data.copy() - logged_data.pop('PSWD') - - _logger.info("_ogone_s2s_get_tx_status: Sending values to URL %s, values:\n%s", query_direct_url, pformat(logged_data)) - result = requests.post(query_direct_url, data=data).content - - try: - tree = objectify.fromstring(result) - _logger.info('_ogone_s2s_get_tx_status: Values received:\n%s', etree.tostring(tree, pretty_print=True, encoding='utf-8')) - except etree.XMLSyntaxError: - # invalid response from ogone - _logger.exception('Invalid xml response from ogone') - _logger.info('_ogone_s2s_get_tx_status: Values received:\n%s', result) - raise - - return tree - - -class PaymentToken(models.Model): - _inherit = 'payment.token' - - def ogone_create(self, values): - if values.get('cc_number'): - # create a alias via batch - values['cc_number'] = values['cc_number'].replace(' ', '') - acquirer = self.env['payment.acquirer'].browse(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': 'MTR', - 'OPERATION': 'SAL', - 'NB_PAYMENTS': 1, # even if we do not actually have any payment, ogone want it to not be 0 - 'FILE': normalize('NFKD', line).encode('ascii','ignore'), # Ogone Batch must be ASCII only - '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' % ('prod' if acquirer.state == 'enabled' else 'test') - _logger.info("ogone_create: Creating new alias %s via url %s", alias, url) - result = requests.post(url, data=data).content - - 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 = tree.get(error_code) - error = '%s\n\n%s: %s' % (error_str, error_code, error_msg) - _logger.error(error) - raise Exception(error) - - return { - 'acquirer_ref': alias, - 'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']) - } - return {} diff --git a/addons/payment_ingenico/tests/__init__.py b/addons/payment_ingenico/tests/__init__.py deleted file mode 100644 index 40a96afc6ff..00000000000 --- a/addons/payment_ingenico/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/addons/payment_ingenico/tests/test_ogone.py b/addons/payment_ingenico/tests/test_ogone.py deleted file mode 100644 index 9a8f577451e..00000000000 --- a/addons/payment_ingenico/tests/test_ogone.py +++ /dev/null @@ -1,195 +0,0 @@ -# -*- coding: utf-8 -*- - -from lxml import objectify -import time - -from odoo.addons.payment.models.payment_acquirer import ValidationError -from odoo.addons.payment.tests.common import PaymentAcquirerCommon -from odoo.addons.payment_ingenico.controllers.main import OgoneController -from werkzeug import urls - -from odoo.tools import mute_logger -from odoo.tests import tagged - - -@tagged('post_install', '-at_install', 'external', '-standard') -class OgonePayment(PaymentAcquirerCommon): - - @classmethod - def setUpClass(cls, chart_template_ref=None): - super().setUpClass(chart_template_ref=chart_template_ref) - - cls.ogone = cls.env.ref('payment.payment_acquirer_ogone') - cls.ogone.write({ - 'ogone_pspid': 'dummy', - 'ogone_userid': 'dummy', - 'ogone_password': 'dummy', - 'ogone_shakey_in': 'dummy', - 'ogone_shakey_out': 'dummy', - 'state': 'test', - }) - - def test_10_ogone_form_render(self): - base_url = self.env['ir.config_parameter'].get_param('web.base.url') - # be sure not to do stupid thing - self.assertEqual(self.ogone.state, 'test', 'test without test environment') - - # ---------------------------------------- - # Test: button direct rendering + shasign - # ---------------------------------------- - - form_values = { - 'PSPID': 'dummy', - 'ORDERID': 'test_ref0', - 'AMOUNT': '1', - 'CURRENCY': 'EUR', - 'LANGUAGE': 'en_US', - 'CN': 'Norbert Buyer', - 'EMAIL': 'norbert.buyer@example.com', - 'OWNERZIP': '1000', - 'OWNERADDRESS': 'Huge Street 2/543', - 'OWNERCTY': 'Belgium', - 'OWNERTOWN': 'Sin City', - 'OWNERTELNO': '0032 12 34 56 78', - 'SHASIGN': '815f67b8ff70d234ffcf437c13a9fa7f807044cc', - 'ACCEPTURL': urls.url_join(base_url, OgoneController._accept_url), - 'DECLINEURL': urls.url_join(base_url, OgoneController._decline_url), - 'EXCEPTIONURL': urls.url_join(base_url, OgoneController._exception_url), - 'CANCELURL': urls.url_join(base_url, OgoneController._cancel_url), - } - - # render the button - res = self.ogone.render( - 'test_ref0', 0.01, self.currency_euro.id, - partner_id=None, - partner_values=self.buyer_values) - - # check form result - tree = objectify.fromstring(res) - self.assertEqual(tree.get('action'), 'https://secure.ogone.com/ncol/test/orderstandard.asp', 'ogone: wrong form POST url') - for form_input in tree.input: - if form_input.get('name') in ['submit']: - continue - self.assertEqual( - form_input.get('value'), - form_values[form_input.get('name')], - 'ogone: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')]) - ) - - # ---------------------------------------- - # Test2: button using tx + validation - # ---------------------------------------- - - # create a new draft tx - tx = self.env['payment.transaction'].create({ - 'amount': 0.01, - 'acquirer_id': self.ogone.id, - 'currency_id': self.currency_euro.id, - 'reference': 'test_ref0', - 'partner_id': self.buyer_id}) - # render the button - res = self.ogone.render( - 'should_be_erased', 0.01, self.currency_euro, - tx_id=tx.id, - partner_id=None, - partner_values=self.buyer_values) - - # check form result - tree = objectify.fromstring(res) - self.assertEqual(tree.get('action'), 'https://secure.ogone.com/ncol/test/orderstandard.asp', 'ogone: wrong form POST url') - for form_input in tree.input: - if form_input.get('name') in ['submit']: - continue - self.assertEqual( - form_input.get('value'), - form_values[form_input.get('name')], - 'ingenico: wrong value for form input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')]) - ) - - @mute_logger('odoo.addons.payment_ingenico.models.payment', 'ValidationError') - def test_20_ogone_form_management(self): - # be sure not to do stupid thing - self.assertEqual(self.ogone.state, 'test', 'test without test environment') - - # typical data posted by ogone after client has successfully paid - ogone_post_data = { - 'orderID': u'test_ref_2', - 'STATUS': u'9', - 'CARDNO': u'XXXXXXXXXXXX0002', - 'PAYID': u'25381582', - 'CN': u'Norbert Buyer', - 'NCERROR': u'0', - 'TRXDATE': u'11/15/13', - 'IP': u'85.201.233.72', - 'BRAND': u'VISA', - 'ACCEPTANCE': u'test123', - 'currency': u'EUR', - 'amount': u'1.95', - 'SHASIGN': u'7B7B0ED9CBC4A85543A9073374589033A62A05A5', - 'ED': u'0315', - 'PM': u'CreditCard' - } - - # should raise error about unknown tx - with self.assertRaises(ValidationError): - self.env['payment.transaction'].form_feedback(ogone_post_data) - - # create tx - tx = self.env['payment.transaction'].create({ - 'amount': 1.95, - 'acquirer_id': self.ogone.id, - 'currency_id': self.currency_euro.id, - 'reference': 'test_ref_2-1', - 'partner_name': 'Norbert Buyer', - 'partner_country_id': self.country_france.id}) - # validate it - tx.form_feedback(ogone_post_data) - # check state - self.assertEqual(tx.state, 'done', 'ogone: validation did not put tx into done state') - self.assertEqual(tx.ogone_payid, ogone_post_data.get('PAYID'), 'ogone: validation did not update tx payid') - - # reset tx - tx = self.env['payment.transaction'].create({ - 'amount': 1.95, - 'acquirer_id': self.ogone.id, - 'currency_id': self.currency_euro.id, - 'reference': 'test_ref_2-2', - 'partner_name': 'Norbert Buyer', - 'partner_country_id': self.country_france.id}) - - # now ogone post is ok: try to modify the SHASIGN - ogone_post_data['SHASIGN'] = 'a4c16bae286317b82edb49188d3399249a784691' - with self.assertRaises(ValidationError): - tx.form_feedback(ogone_post_data) - - # simulate an error - ogone_post_data['STATUS'] = 2 - ogone_post_data['SHASIGN'] = 'a4c16bae286317b82edb49188d3399249a784691' - tx.form_feedback(ogone_post_data) - # check state - self.assertEqual(tx.state, 'cancel', 'ogone: erroneous validation did not put tx into error state') - - def test_30_ogone_s2s(self): - test_ref = 'test_ref_%.15f' % time.time() - # be sure not to do stupid thing - self.assertEqual(self.ogone.state, 'test', 'test without test environment') - - # create a new draft tx - tx = self.env['payment.transaction'].create({ - 'amount': 0.01, - 'acquirer_id': self.ogone.id, - 'currency_id': self.currency_euro.id, - 'reference': test_ref, - 'partner_id': self.buyer_id, - 'type': 'server2server', - }) - - # create an alias - res = tx.ogone_s2s_create_alias({ - 'expiry_date_mm': '01', - 'expiry_date_yy': '2015', - 'holder_name': 'Norbert Poilu', - 'number': '4000000000000002', - 'brand': 'VISA'}) - - res = tx.ogone_s2s_execute({}) diff --git a/addons/payment_ingenico/views/payment_ingenico_templates.xml b/addons/payment_ingenico/views/payment_ingenico_templates.xml deleted file mode 100644 index b27ce00a6f4..00000000000 --- a/addons/payment_ingenico/views/payment_ingenico_templates.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - diff --git a/addons/payment_ingenico/views/payment_views.xml b/addons/payment_ingenico/views/payment_views.xml deleted file mode 100644 index 82163fa6db6..00000000000 --- a/addons/payment_ingenico/views/payment_views.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - acquirer.form.ogone - payment.acquirer - - - - - - - - - - - - - - - - - diff --git a/addons/payment_ingenico/__init__.py b/addons/payment_ogone/__init__.py similarity index 58% rename from addons/payment_ingenico/__init__.py rename to addons/payment_ogone/__init__.py index 09f44ea4916..a852ddecf9d 100644 --- a/addons/payment_ingenico/__init__.py +++ b/addons/payment_ogone/__init__.py @@ -1,10 +1,11 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import models from . import controllers -from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers -from odoo.addons.payment import reset_payment_provider +from . import models + +from odoo.addons.payment import reset_payment_acquirer +from odoo.addons.payment.models.payment_acquirer import create_missing_journals # post-init hook + def uninstall_hook(cr, registry): - reset_payment_provider(cr, registry, 'ogone') + reset_payment_acquirer(cr, registry, 'ogone') diff --git a/addons/payment_ogone/__manifest__.py b/addons/payment_ogone/__manifest__.py new file mode 100644 index 00000000000..0959dc5e627 --- /dev/null +++ b/addons/payment_ogone/__manifest__.py @@ -0,0 +1,19 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Ogone Payment Acquirer', + 'version': '2.0', + 'category': 'Accounting/Payment Acquirers', + 'sequence': 360, + 'summary': 'Payment Acquirer: Ogone Implementation', + 'description': """Ogone Payment Acquirer""", + 'depends': ['payment'], + 'data': [ + 'views/payment_views.xml', + 'views/payment_ogone_templates.xml', + 'data/payment_acquirer_data.xml', + ], + 'application': True, + 'post_init_hook': 'create_missing_journals', + 'uninstall_hook': 'uninstall_hook', +} diff --git a/addons/payment_ogone/controllers/__init__.py b/addons/payment_ogone/controllers/__init__.py new file mode 100644 index 00000000000..80ee4da1c5e --- /dev/null +++ b/addons/payment_ogone/controllers/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py new file mode 100644 index 00000000000..edb11b6a475 --- /dev/null +++ b/addons/payment_ogone/controllers/main.py @@ -0,0 +1,129 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import logging +import pprint +import re + +import werkzeug + +from odoo import _, http +from odoo.exceptions import ValidationError +from odoo.http import request +from odoo.tools import ustr + +_logger = logging.getLogger(__name__) + + +class OgoneController(http.Controller): + _flexcheckout_return_url = '/payment/ogone/flexchekout' + _directlink_return_url = '/payment/ogone/directlink' + + @http.route( + _flexcheckout_return_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False + ) # 'GET' or 'POST' depending on the configuration in Ogone backend + def ogone_return_from_flexcheckout(self, **feedback_data): + """ Process the data returned by Ogone after redirection to Flexcheckout. + + :param dict feedback_data: The feedback data + """ + # Check the source and integrity of the data + data = self._homogenize_data(feedback_data) + self._verify_signature(feedback_data, data) + + # Create a token from the feedback data + data['FEEDBACK_TYPE'] = 'flexcheckout' + _logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(data)) + tx_sudo = request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data) + + # Process the payment through the token + tree = tx_sudo._ogone_send_order_request(request_3ds_authentication=True) + + # Handle the response + redirect_html_element = tree.find('HTML_ANSWER') + if redirect_html_element: + # Ogone has inserted an HTML_ANSWER element in its response XML tree. This means that a + # redirection to DirectLink's authentication page is required, as FlexCheckout is not + # capable of handling authentications... + # As per the documentation, the HTML must be inserted as-is into the current page, and + # consists of a
bundled with a script to auto-submit the form once its inserted. + # The content of the HTML is not sanitized to preserve the redirection in the script. + # After redirection, the customer comes back to the directlink return URL and we proceed + # with the (now authenticated) payment request. + redirect_html = ustr(base64.b64decode(redirect_html_element.text)) + return request.render( + 'payment_ogone.directlink_feedback', {'redirect_html': redirect_html} + ) + else: + feedback_data = { + 'FEEDBACK_TYPE': 'directlink', + 'ORDERID': tree.get('orderID'), + 'tree': tree, + } + _logger.info( + "entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data) + ) + request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', feedback_data) + if tx_sudo.state in ('cancel', 'error'): + tx_sudo.token_id.active = False # The initial payment failed, archive the token + return werkzeug.utils.redirect('/payment/status') + + @http.route( + _directlink_return_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False + ) # 'GET' or 'POST' depending on the configuration in Ogone backend + def ogone_return_from_directlink(self, **feedback_data): + """ Process the data returned by Ogone after redirection to Directlink authentication page. + + A redirection to Directlink can happen if a 3DS1 authentication is requested when sending + the request for a new order. This should normally only happen for the first payment of a + token as this is the only case where we specifically request the authentication if necessary + and handle the redirection request if one is returned. + + :param dict feedback_data: The feedback data + """ + # Check the source and integrity of the data + data = self._homogenize_data(feedback_data) + self._verify_signature(feedback_data, data) + + # Handle the feedback data + data['FEEDBACK_TYPE'] = 'directlink' + _logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(data)) + tx_sudo = request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data) + if tx_sudo.state in ('cancel', 'error'): + tx_sudo.token_id.active = False # The initial payment failed, archive the token + return werkzeug.utils.redirect('/payment/status') + + def _homogenize_data(self, data): + """ Format keys to follow an homogenized convention inspired by Ogone Directlink API. + + The keys received from Ogone APIs have inconsistent formatting and must be homogenized to + allow re-using the same methods. We reformat them to follow a unified nomenclature inspired + by DirectLink's order direct endpoint. + + Formatting steps: + 1) Uppercase key strings: 'Something' -> 'SOMETHING', 'something' -> 'SOMETHING' + 2) Remove the prefix: 'CARD.SOMETHING' -> 'SOMETHING', 'ALIAS.SOMETHING' -> 'SOMETHING' + """ + return {re.sub(r'.*\.', '', k.upper()): v for k, v in data.items()} + + def _verify_signature(self, sign_data, data): + """ Check that the signature computed from the feedback matches the received one. + + :param dict sign_data: The original feedback data used to compute the signature + :param dict sign_data: The formatted feedback data used to find the tx and received sig + :return: None + :raise: ValidationError if the signatures don't match + """ + acquirer_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data( + 'ogone', data + ).acquirer_id # Find the acquirer based on the transaction + received_signature = data.get('SHASIGN') + expected_signature = acquirer_sudo._ogone_generate_signature(sign_data) + if received_signature != expected_signature.upper(): + raise ValidationError( + "Ogone: " + _( + "Received data with invalid signature. expected: %(exp)s ; received: %(rec)s ; " + "data:\n%(data)s", + exp=expected_signature, rec=received_signature, data=pprint.pformat(sign_data) + ) + ) diff --git a/addons/payment_ogone/data/payment_acquirer_data.xml b/addons/payment_ogone/data/payment_acquirer_data.xml new file mode 100644 index 00000000000..bb02c17fd80 --- /dev/null +++ b/addons/payment_ogone/data/payment_acquirer_data.xml @@ -0,0 +1,13 @@ + + + + + ogone + + False + False + True + True + + + diff --git a/addons/payment_ingenico/i18n/ar.po b/addons/payment_ogone/i18n/ar.po similarity index 100% rename from addons/payment_ingenico/i18n/ar.po rename to addons/payment_ogone/i18n/ar.po diff --git a/addons/payment_ingenico/i18n/cs.po b/addons/payment_ogone/i18n/cs.po similarity index 100% rename from addons/payment_ingenico/i18n/cs.po rename to addons/payment_ogone/i18n/cs.po diff --git a/addons/payment_ingenico/i18n/da.po b/addons/payment_ogone/i18n/da.po similarity index 100% rename from addons/payment_ingenico/i18n/da.po rename to addons/payment_ogone/i18n/da.po diff --git a/addons/payment_ingenico/i18n/de.po b/addons/payment_ogone/i18n/de.po similarity index 100% rename from addons/payment_ingenico/i18n/de.po rename to addons/payment_ogone/i18n/de.po diff --git a/addons/payment_ingenico/i18n/es.po b/addons/payment_ogone/i18n/es.po similarity index 100% rename from addons/payment_ingenico/i18n/es.po rename to addons/payment_ogone/i18n/es.po diff --git a/addons/payment_ingenico/i18n/fr.po b/addons/payment_ogone/i18n/fr.po similarity index 100% rename from addons/payment_ingenico/i18n/fr.po rename to addons/payment_ogone/i18n/fr.po diff --git a/addons/payment_ingenico/i18n/he.po b/addons/payment_ogone/i18n/he.po similarity index 100% rename from addons/payment_ingenico/i18n/he.po rename to addons/payment_ogone/i18n/he.po diff --git a/addons/payment_ingenico/i18n/hr.po b/addons/payment_ogone/i18n/hr.po similarity index 100% rename from addons/payment_ingenico/i18n/hr.po rename to addons/payment_ogone/i18n/hr.po diff --git a/addons/payment_ingenico/i18n/hu.po b/addons/payment_ogone/i18n/hu.po similarity index 100% rename from addons/payment_ingenico/i18n/hu.po rename to addons/payment_ogone/i18n/hu.po diff --git a/addons/payment_ingenico/i18n/it.po b/addons/payment_ogone/i18n/it.po similarity index 100% rename from addons/payment_ingenico/i18n/it.po rename to addons/payment_ogone/i18n/it.po diff --git a/addons/payment_ingenico/i18n/lb.po b/addons/payment_ogone/i18n/lb.po similarity index 100% rename from addons/payment_ingenico/i18n/lb.po rename to addons/payment_ogone/i18n/lb.po diff --git a/addons/payment_ingenico/i18n/lt.po b/addons/payment_ogone/i18n/lt.po similarity index 100% rename from addons/payment_ingenico/i18n/lt.po rename to addons/payment_ogone/i18n/lt.po diff --git a/addons/payment_ingenico/i18n/mn.po b/addons/payment_ogone/i18n/mn.po similarity index 100% rename from addons/payment_ingenico/i18n/mn.po rename to addons/payment_ogone/i18n/mn.po diff --git a/addons/payment_ingenico/i18n/nb.po b/addons/payment_ogone/i18n/nb.po similarity index 100% rename from addons/payment_ingenico/i18n/nb.po rename to addons/payment_ogone/i18n/nb.po diff --git a/addons/payment_ingenico/i18n/nl.po b/addons/payment_ogone/i18n/nl.po similarity index 100% rename from addons/payment_ingenico/i18n/nl.po rename to addons/payment_ogone/i18n/nl.po diff --git a/addons/payment_ingenico/i18n/payment_ingenico.pot b/addons/payment_ogone/i18n/payment_ingenico.pot similarity index 100% rename from addons/payment_ingenico/i18n/payment_ingenico.pot rename to addons/payment_ogone/i18n/payment_ingenico.pot diff --git a/addons/payment_ingenico/i18n/pl.po b/addons/payment_ogone/i18n/pl.po similarity index 100% rename from addons/payment_ingenico/i18n/pl.po rename to addons/payment_ogone/i18n/pl.po diff --git a/addons/payment_ingenico/i18n/pt.po b/addons/payment_ogone/i18n/pt.po similarity index 100% rename from addons/payment_ingenico/i18n/pt.po rename to addons/payment_ogone/i18n/pt.po diff --git a/addons/payment_ingenico/i18n/pt_BR.po b/addons/payment_ogone/i18n/pt_BR.po similarity index 100% rename from addons/payment_ingenico/i18n/pt_BR.po rename to addons/payment_ogone/i18n/pt_BR.po diff --git a/addons/payment_ingenico/i18n/ro.po b/addons/payment_ogone/i18n/ro.po similarity index 100% rename from addons/payment_ingenico/i18n/ro.po rename to addons/payment_ogone/i18n/ro.po diff --git a/addons/payment_ingenico/i18n/ru.po b/addons/payment_ogone/i18n/ru.po similarity index 100% rename from addons/payment_ingenico/i18n/ru.po rename to addons/payment_ogone/i18n/ru.po diff --git a/addons/payment_ingenico/i18n/tr.po b/addons/payment_ogone/i18n/tr.po similarity index 100% rename from addons/payment_ingenico/i18n/tr.po rename to addons/payment_ogone/i18n/tr.po diff --git a/addons/payment_ingenico/i18n/uk.po b/addons/payment_ogone/i18n/uk.po similarity index 100% rename from addons/payment_ingenico/i18n/uk.po rename to addons/payment_ogone/i18n/uk.po diff --git a/addons/payment_ingenico/i18n/vi.po b/addons/payment_ogone/i18n/vi.po similarity index 100% rename from addons/payment_ingenico/i18n/vi.po rename to addons/payment_ogone/i18n/vi.po diff --git a/addons/payment_ingenico/i18n/zh_CN.po b/addons/payment_ogone/i18n/zh_CN.po similarity index 100% rename from addons/payment_ingenico/i18n/zh_CN.po rename to addons/payment_ogone/i18n/zh_CN.po diff --git a/addons/payment_ingenico/i18n/zh_TW.po b/addons/payment_ogone/i18n/zh_TW.po similarity index 100% rename from addons/payment_ingenico/i18n/zh_TW.po rename to addons/payment_ogone/i18n/zh_TW.po diff --git a/addons/payment_ogone/models/__init__.py b/addons/payment_ogone/models/__init__.py new file mode 100644 index 00000000000..22eb69117db --- /dev/null +++ b/addons/payment_ogone/models/__init__.py @@ -0,0 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import payment_acquirer +from . import payment_token +from . import payment_transaction diff --git a/addons/payment_ogone/models/const.py b/addons/payment_ogone/models/const.py new file mode 100644 index 00000000000..ba900d1737c --- /dev/null +++ b/addons/payment_ogone/models/const.py @@ -0,0 +1,97 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# See https://epayments-support.ingenico.com/en/integration/all-sales-channels/flexcheckout/guide#flexcheckout_integration_guides_sha_out +FLEXCHECKOUT_KEYS = [ + 'ALIAS.ALIASID', + 'ALIAS.NCERROR', + 'ALIAS.NCERRORCARDNO', + 'ALIAS.NCERRORCN', + 'ALIAS.NCERRORCVC', + 'ALIAS.NCERRORED', + 'ALIAS.ORDERID', + 'ALIAS.STATUS', + 'ALIAS.STOREPERMANENTLY', + 'CARD.BIC', + 'CARD.BIN', + 'CARD.BRAND', + 'CARD.CARDHOLDERNAME', + 'CARD.CARDNUMBER', + 'CARD.CVC', + 'CARD.EXPIRYDATE' +] +# See https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_request_a_new_order +# See https://epayments-support.ingenico.com/en/integration-solutions/integrations/directlink#directlink_integration_guides_order_response +DIRECTLINK_KEYS = [ + 'AAVADDRESS', + 'AAVCHECK', + 'AAVMAIL', + 'AAVNAME', + 'AAVPHONE', + 'AAVZIP', + 'ACCEPTANCE', + 'ALIAS', + 'AMOUNT', + 'BIC', + 'BIN', + 'BRAND', + 'CARDNO', + 'CCCTY', + 'CN', + 'COLLECTOR_BIC', + 'COLLECTOR_IBAN', + 'COMPLUS', + 'CREATION_STATUS', + 'CREDITDEBIT', + 'CURRENCY', + 'CVCCHECK', + 'DCC_COMMPERCENTAGE', + 'DCC_CONVAMOUNT', + 'DCC_CONVCCY', + 'DCC_EXCHRATE', + 'DCC_EXCHRATESOURCE', + 'DCC_EXCHRATETS', + 'DCC_INDICATOR', + 'DCC_MARGINPERCENTAGE', + 'DCC_VALIDHOURS', + 'DEVICEID', + 'DIGESTCARDNO', + 'ECI', + 'ED', + 'EMAIL', + 'ENCCARDNO', + 'FXAMOUNT', + 'FXCURRENCY', + 'IP', + 'IPCTY', + 'MANDATEID', + 'MOBILEMODE', + 'NBREMAILUSAGE', + 'NBRIPUSAGE', + 'NBRIPUSAGE_ALLTX', + 'NBRUSAGE', + 'NCERROR', + 'ORDERID', + 'PAYID', + 'PAYIDSUB', + 'PAYMENT_REFERENCE', + 'PM', + 'SCO_CATEGORY', + 'SCORING', + 'SEQUENCETYPE', + 'SIGNDATE', + 'STATUS', + 'SUBBRAND', + 'SUBSCRIPTION_ID', + 'TICKET', + 'TRXDATE', + 'VC', +] +VALID_KEYS = DIRECTLINK_KEYS + FLEXCHECKOUT_KEYS + + +# See https://epayments-support.ingenico.com/en/get-started/transaction-status-full/ +PAYMENT_STATUS_MAPPING = { + 'pending': (41, 46, 50, 51, 52, 55, 56, 81, 82, 91, 92, 99), # 46 = 3DS + 'done': (5, 8, 9), + 'cancel': (1,), +} diff --git a/addons/payment_ogone/models/payment_acquirer.py b/addons/payment_ogone/models/payment_acquirer.py new file mode 100644 index 00000000000..fd09c53601e --- /dev/null +++ b/addons/payment_ogone/models/payment_acquirer.py @@ -0,0 +1,122 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +from hashlib import sha256 + +import requests + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +from .const import VALID_KEYS + +_logger = logging.getLogger(__name__) + + +class PaymentAcquirer(models.Model): + _inherit = 'payment.acquirer' + + provider = fields.Selection( + selection_add=[('ogone', "Ogone")], ondelete={'ogone': 'set default'}) + ogone_pspid = fields.Char( + string="PSPID", help="The ID solely used to identify the account with Ogone", + required_if_provider='ogone') + ogone_userid = fields.Char( + string="API User ID", help="The ID solely used to identify the API user with Ogone", + required_if_provider='ogone') + ogone_password = fields.Char( + string="API User Password", required_if_provider='ogone', groups='base.group_system') + ogone_shakey_in = fields.Char( + string="SHA Key IN", size=32, required_if_provider='ogone', groups='base.group_system') + ogone_shakey_out = fields.Char( + string="SHA Key OUT", size=32, required_if_provider='ogone', groups='base.group_system') + + def _get_validation_amount(self): + """ Override of payment to return the amount for Ogone validation operations. + + :return: The validation amount + :rtype: float + """ + res = super()._get_validation_amount() + if self.provider != 'ogone': + return res + + return 1.0 + + def _ogone_get_api_url(self, api_key): + """ Return the appropriate URL of the requested API for the acquirer state. + + Note: self.ensure_one() + + :param str api_key: The API whose URL to get: 'flexcheckout' or 'directlink' + :return: The API URL + :rtype: str + """ + self.ensure_one() + + if self.state == 'enabled': + api_urls = { + 'flexcheckout': 'https://secure.ogone.com/Tokenization/HostedPage', + 'directlink': 'https://secure.ogone.com/ncol/prod/orderdirect.asp', + 'maintenancedirect': 'https://secure.ogone.com/ncol/prod/maintenancedirect.asp', + } + else: # 'test' + api_urls = { + 'flexcheckout': 'https://ogone.test.v-psp.com/Tokenization/HostedPage', + 'directlink': 'https://ogone.test.v-psp.com/ncol/test/orderdirect.asp', + 'maintenancedirect': 'https://ogone.test.v-psp.com/ncol/test/maintenancedirect.asp', + } + return api_urls.get(api_key) + + def _ogone_generate_signature(self, values, incoming=True, format_keys=False): + """ Generate the signature for incoming or outgoing communications. + + :param dict values: The values used to generate the signature + :param bool incoming: Whether the signature must be generated for an incoming (Ogone to + Odoo) or outgoing (Odoo to Ogone) communication. + :param bool format_keys: Whether the keys must be formatted as uppercase, dot-separated + strings to comply with Ogone APIs. This must be used when the keys + are formatted as underscore-separated strings to be compliant with + QWeb's `t-att-value`. + :return: The signature + :rtype: str + """ + + def _filter_key(_key): + return not incoming or _key in VALID_KEYS + + key = self.ogone_shakey_in if incoming else self.ogone_shakey_out + if format_keys: + formatted_items = [(k.upper().replace('_', '.'), v) for k, v in values.items()] + else: + formatted_items = [(k.upper(), v) for k, v in values.items()] + sorted_items = sorted(formatted_items) + signing_string = ''.join(f'{k}={v}{key}' for k, v in sorted_items if _filter_key(k) and v) + shasign = sha256(signing_string.encode("utf-8")).hexdigest() + return shasign + + def _ogone_make_request(self, api_key, payload=None, method='POST'): + """ Make a request to one of Ogone APIs. + + Note: self.ensure_one() + + :param str api_key: The API to which the request is made: 'flexcheckout' or 'directlink' + :param dict payload: The payload of the request + :param str method: The HTTP method of the request + :return The content of the response + :rtype: bytes + :raise: ValidationError if an HTTP error occurs + """ + self.ensure_one() + + url = self._ogone_get_api_url(api_key) + try: + response = requests.request(method, url, data=payload, timeout=60) + response.raise_for_status() + except requests.exceptions.ConnectionError: + _logger.exception("unable to reach endpoint at %s", url) + raise ValidationError("Ogone: " + _("Could not establish the connection to the API.")) + except requests.exceptions.HTTPError: + _logger.exception("invalid API request at %s with data %s", url, payload) + raise ValidationError("Ogone: " + _("The communication with the API failed.")) + return response.content diff --git a/addons/payment_ogone/models/payment_token.py b/addons/payment_ogone/models/payment_token.py new file mode 100644 index 00000000000..40b89deb9ce --- /dev/null +++ b/addons/payment_ogone/models/payment_token.py @@ -0,0 +1,26 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, models +from odoo.exceptions import UserError + + +class PaymentToken(models.Model): + _inherit = 'payment.token' + + def _handle_reactivation_request(self): + """ Override of payment to raise an error informing that Ogone tokens cannot be restored. + + More specifically, permanents tokens are never deleted in Ogone's backend but we don't + distinguish them from temporary tokens which are archived at creation time. So we simply + block the reactivation of every token. + + Note: self.ensure_one() + + :return: None + :raise: UserError if the token is managed by Ogone + """ + super()._handle_reactivation_request() + if self.provider != 'ogone': + return + + raise UserError(_("Saved payment methods cannot be restored once they have been archived.")) diff --git a/addons/payment_ogone/models/payment_transaction.py b/addons/payment_ogone/models/payment_transaction.py new file mode 100644 index 00000000000..f7c4fcf854b --- /dev/null +++ b/addons/payment_ogone/models/payment_transaction.py @@ -0,0 +1,284 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import pprint + +from lxml import etree, objectify +from werkzeug import urls + +from odoo import _, api, models +from odoo.exceptions import UserError, ValidationError + +from . import const +from odoo.addons.payment import utils as payment_utils +from odoo.addons.payment_ogone.controllers.main import OgoneController + +_logger = logging.getLogger(__name__) + + +class PaymentTransaction(models.Model): + _inherit = 'payment.transaction' + + @api.model + def _compute_reference(self, provider, prefix=None, **kwargs): + """ Override of payment to ensure that Ogone requirements for references are satisfied. + + Ogone requirements for references are as follows: + - References must be unique at provider level for a given merchant account. + This is satisfied by singularizing the prefix with the current datetime. If two + transactions are created simultaneously, `_compute_reference` ensures the uniqueness of + references by suffixing a sequence number. + + :param str provider: The provider of the acquirer handling the transaction + :param str prefix: The custom prefix used to compute the full reference + :return: The unique reference for the transaction + :rtype: str + """ + if provider != 'ogone': + return super()._compute_reference(provider, prefix=prefix, **kwargs) + + prefix = payment_utils.singularize_reference_prefix(prefix=prefix, max_length=40) + return super()._compute_reference(provider, prefix=prefix, **kwargs) + + def _get_specific_rendering_values(self, processing_values): + """ Override of payment to return Ogone-specific rendering values. + + Note: self.ensure_one() from `_get_processing_values` + + :param dict processing_values: The generic and specific processing values of the transaction + :return: The dict of acquirer-specific processing values + :rtype: dict + """ + res = super()._get_specific_rendering_values(processing_values) + if self.acquirer_id.provider != 'ogone': + return res + + base_url = self.acquirer_id._get_base_url() + return_url = urls.url_join(base_url, OgoneController._flexcheckout_return_url) + rendering_values = { + 'ACCOUNT_PSPID': self.acquirer_id.ogone_pspid, + 'ALIAS_ALIASID': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'), + 'ALIAS_ORDERID': self.reference, + 'ALIAS_STOREPERMANENTLY': 'Y' if self.tokenize else 'N', + 'CARD_PAYMENTMETHOD': 'CreditCard', + 'LAYOUT_LANGUAGE': self.partner_lang, + 'PARAMETERS_ACCEPTURL': return_url, + 'PARAMETERS_EXCEPTIONURL': return_url, + } + rendering_values.update({ + 'SHASIGNATURE_SHASIGN': self.acquirer_id._ogone_generate_signature( + rendering_values, incoming=False, format_keys=True + ).upper(), + 'api_url': self.acquirer_id._ogone_get_api_url('flexcheckout'), + }) + return rendering_values + + def _send_payment_request(self): + """ Override of payment to send a payment request to Ogone. + + Note: self.ensure_one() + + :return: None + :raise: UserError if the transaction is not linked to a token + """ + super()._send_payment_request() + if self.provider != 'ogone': + return + + if not self.token_id: + raise UserError("Ogone: " + _("The transaction is not linked to a token.")) + + tree = self._ogone_send_order_request() + feedback_data = { + 'FEEDBACK_TYPE': 'directlink', + 'ORDERID': tree.get('orderID'), + 'tree': tree, + } + _logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data)) + self._handle_feedback_data('ogone', feedback_data) + + def _ogone_send_order_request(self, request_3ds_authentication=False): + """ Make a new order request to Ogone and return the lxml etree parsed from the response. + + :param bool request_3ds_authentication: Whether a 3DS authentication should be requested if + necessary to process the payment + :return: The lxml etree + :raise: ValidationError if the response can not be parsed to an lxml etree + """ + base_url = self.acquirer_id.get_base_url() + return_url = urls.url_join(base_url, OgoneController._directlink_return_url) + data = { + # DirectLink parameters + 'PSPID': self.acquirer_id.ogone_pspid, + 'ORDERID': self.reference, + 'USERID': self.acquirer_id.ogone_userid, + 'PSWD': self.acquirer_id.ogone_password, + 'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2), + 'CURRENCY': self.currency_id.name, + 'CN': self.partner_name or '', # Cardholder Name + 'EMAIL': self.partner_email or '', + 'OWNERADDRESS': self.partner_address or '', + 'OWNERZIP': self.partner_zip or '', + 'OWNERTOWN': self.partner_city or '', + 'OWNERCTY': self.partner_country_id.code or '', + 'OWNERTELNO': self.partner_phone or '', + 'OPERATION': 'SAL', # direct sale + # Alias Manager parameters + 'ALIAS': self.token_id.acquirer_ref, + 'ALIASPERSISTEDAFTERUSE': 'Y' if self.token_id.active else 'N', + 'ECI': 9, # Recurring (from eCommerce) + # 3DS parameters + 'ACCEPTURL': return_url, + 'DECLINEURL': return_url, + 'EXCEPTIONURL': return_url, + 'LANGUAGE': self.partner_lang or 'en_US', + 'FLAG3D': 'Y' if request_3ds_authentication else 'N', + } + data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(data, incoming=False) + + _logger.info( + "making payment request:\n%s", + pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'}) + ) # Log the payment request data without the password + response_content = self.acquirer_id._ogone_make_request('directlink', data) + try: + tree = objectify.fromstring(response_content) + except etree.XMLSyntaxError: + raise ValidationError("Ogone: " + "Received badly structured response from the API.") + _logger.info( + "received payment request response as an etree:\n%s", + etree.tostring(tree, pretty_print=True, encoding='utf-8') + ) + return tree + + @api.model + def _get_tx_from_feedback_data(self, provider, data): + """ Override of payment to find the transaction based on Ogone data. + + :param str provider: The provider of the acquirer that handled the transaction + :param dict data: The feedback data sent by the provider + :return: The transaction if found + :rtype: recordset of `payment.transaction` + :raise: ValidationError if the data match no transaction + """ + tx = super()._get_tx_from_feedback_data(provider, data) + if provider != 'ogone': + return tx + + reference = data.get('ORDERID') + tx = self.search([('reference', '=', reference), ('provider', '=', 'ogone')]) + if not tx: + raise ValidationError( + "Ogone: " + _("No transaction found matching reference %s.", reference) + ) + return tx + + def _process_feedback_data(self, data): + """ Override of payment to process the transaction based on Ogone data. + + Note: self.ensure_one() + + :param dict data: The feedback data sent by the provider + :return: None + :raise: ValidationError if inconsistent data were received + """ + super()._process_feedback_data(data) + if self.provider != 'ogone': + return + + feedback_type = data.get('FEEDBACK_TYPE') + if feedback_type == 'flexcheckout': + self._process_flexcheckout_data(data) + elif feedback_type == 'directlink': + self._process_directlink_data(data) + else: + raise ValidationError( + "Ogone: " + _("Received feedback data with unknown type: %s", feedback_type) + ) + + def _process_flexcheckout_data(self, data): + """ Create a token from Flexcheckout feedback data. + + :param dict data: The feedback data from Flexcheckout + :return: None + """ + token = self.env['payment.token'].create({ + 'acquirer_id': self.acquirer_id.id, + 'name': data.get('CARDNUMBER'), # Already padded with 'X's + 'partner_id': self.partner_id.id, + 'acquirer_ref': data['ALIASID'], + 'verified': False, # No payment has been processed through this token yet + 'active': self.tokenize, # Immediately archive the token if it was not requested + }) + self.write({ + 'token_id': token.id, + 'tokenize': False, + }) + + def _process_directlink_data(self, data): + """ Update the transaction state and the acquirer reference based on the feedback data. + + :param dict data: The feedback data from DirectLink + :return: None + """ + if 'tree' in data: + data = data['tree'] + self.acquirer_reference = data.get('PAYID') + payment_status = int(data.get('STATUS', '0')) + if payment_status in const.PAYMENT_STATUS_MAPPING['pending']: + self._set_pending() + elif payment_status in const.PAYMENT_STATUS_MAPPING['done']: + self.token_id.verified = True # The payment has been authorized, the token is valid + self._set_done() + elif payment_status in const.PAYMENT_STATUS_MAPPING['cancel']: + self._set_canceled() + else: # Classify unknown payment statuses as `error` tx state + _logger.info("received data with invalid payment status: %s", payment_status) + self._set_error( + "Ogone: " + _("Received data with invalid payment status: %s", payment_status) + ) + + def _send_refund_request(self): + """ Override of payment to send a refund request to Authorize. + + Note: self.ensure_one() + + :return: None + :raise: ValidationError if a badly structured response is received + """ + super()._send_refund_request() + if self.provider != 'ogone': + return + + data = { + 'PSPID': self.acquirer_id.ogone_pspid, + 'ORDERID': self.reference, + 'PAYID': self.acquirer_reference, + 'USERID': self.acquirer_id.ogone_userid, + 'PSWD': self.acquirer_id.ogone_password, + 'AMOUNT': payment_utils.to_minor_currency_units(self.amount, None, 2), + 'CURRENCY': self.currency_id.name, + 'OPERATION': 'RFS', # refund + } + data['SHASIGN'] = self.acquirer_id._ogone_generate_signature(data, incoming=False) + + _logger.info( + "making refund request:\n%s", + pprint.pformat({k: v for k, v in data.items() if k != 'PSWD'}) + ) # Log the refund request data without the password + response_content = self.acquirer_id._ogone_make_request('maintenancedirect', data) + try: + tree = objectify.fromstring(response_content) + except etree.XMLSyntaxError: + raise ValidationError("Ogone: " + "Received badly structured response from the API.") + _logger.info( + "received refund request response as an etree:\n%s", + etree.tostring(tree, pretty_print=True, encoding='utf-8') + ) + feedback_data = { + 'FEEDBACK_TYPE': 'directlink', + 'ORDERID': tree.get('orderID'), + 'tree': tree, + } + _logger.info("entering _handle_feedback_data with data:\n%s", pprint.pformat(feedback_data)) + self._handle_feedback_data('ogone', feedback_data) diff --git a/addons/payment_ingenico/static/description/icon.png b/addons/payment_ogone/static/description/icon.png similarity index 100% rename from addons/payment_ingenico/static/description/icon.png rename to addons/payment_ogone/static/description/icon.png diff --git a/addons/payment_ingenico/static/description/icon.svg b/addons/payment_ogone/static/description/icon.svg similarity index 100% rename from addons/payment_ingenico/static/description/icon.svg rename to addons/payment_ogone/static/description/icon.svg diff --git a/addons/payment_ingenico/static/src/img/ingenico_icon.png b/addons/payment_ogone/static/src/img/ingenico_icon.png similarity index 100% rename from addons/payment_ingenico/static/src/img/ingenico_icon.png rename to addons/payment_ogone/static/src/img/ingenico_icon.png diff --git a/addons/payment_ogone/tests/__init__.py b/addons/payment_ogone/tests/__init__.py new file mode 100644 index 00000000000..e06c5c20d37 --- /dev/null +++ b/addons/payment_ogone/tests/__init__.py @@ -0,0 +1,4 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common +from . import test_ogone diff --git a/addons/payment_ogone/tests/common.py b/addons/payment_ogone/tests/common.py new file mode 100644 index 00000000000..0be43a3d7cc --- /dev/null +++ b/addons/payment_ogone/tests/common.py @@ -0,0 +1,20 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.payment.tests.common import PaymentCommon + + +class OgoneCommon(PaymentCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.ogone = cls._prepare_acquirer('ogone', update_values={ + 'ogone_pspid': 'dummy', + 'ogone_userid': 'dummy', + 'ogone_password': 'dummy', + 'ogone_shakey_in': 'dummy', + 'ogone_shakey_out': 'dummy', + }) + + cls.acquirer = cls.ogone + cls.currency = cls.currency_euro diff --git a/addons/payment_ogone/tests/test_ogone.py b/addons/payment_ogone/tests/test_ogone.py new file mode 100644 index 00000000000..05a9bb341f8 --- /dev/null +++ b/addons/payment_ogone/tests/test_ogone.py @@ -0,0 +1,70 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from freezegun import freeze_time + +from odoo.tests import tagged +from odoo.tools import mute_logger + +from odoo.addons.payment import utils as payment_utils + +from .common import OgoneCommon +from ..controllers.main import OgoneController + + +@tagged('post_install', '-at_install') +class OgoneTest(OgoneCommon): + + def test_validation_amount(self): + self.assertEqual(self.ogone._get_validation_amount(), 1.0) + + # freeze time for consistent singularize_prefix behavior during the test + @freeze_time("2011-11-02 12:00:21") + def test_reference(self): + tx = self.create_transaction(flow="redirect", reference="") + self.assertEqual(tx.reference, "tx-20111102120021", + "Ogone: transaction reference wasn't correctly singularized.") + + # Test prefixes of length > 40 chars + reference = self.env['payment.transaction']._compute_reference( + provider=self.ogone.provider, + prefix="This is a reference of more than 40 characters to annoy ogone", + ) + self.assertEqual(reference, "This is a reference of mo-20111102120021") + self.assertEqual(len(reference), 40) + + # freeze time for consistent singularize_prefix behavior during the test + @freeze_time("2011-11-02 12:00:21") + def test_redirect_form_values(self): + return_url = self._build_url(OgoneController._flexcheckout_return_url) + expected_values = { + 'ACCOUNT_PSPID': self.ogone.ogone_pspid, + 'ALIAS_ALIASID': payment_utils.singularize_reference_prefix(prefix='ODOO-ALIAS'), + 'ALIAS_ORDERID': self.reference, + 'ALIAS_STOREPERMANENTLY': 'N', # 'Y' if self.tokenize + 'CARD_PAYMENTMETHOD': 'CreditCard', + 'LAYOUT_LANGUAGE': self.partner.lang, + 'PARAMETERS_ACCEPTURL': return_url, + 'PARAMETERS_EXCEPTIONURL': return_url, + } + expected_values['SHASIGNATURE_SHASIGN'] = self.ogone._ogone_generate_signature( + expected_values, incoming=False, format_keys=True + ).upper() + + tx = self.create_transaction(flow="redirect") + with mute_logger('odoo.addons.payment.models.payment_transaction'): + processing_values = tx._get_processing_values() + + form_info = self._extract_values_from_html_form(processing_values['redirect_form_html']) + + self.assertEqual(form_info['action'], 'https://ogone.test.v-psp.com/Tokenization/HostedPage') + inputs = form_info['inputs'] + self.assertEqual(len(expected_values), len(inputs)) + for rendering_key, value in expected_values.items(): + form_key = rendering_key.replace('_', '.') + self.assertEqual( + inputs[form_key], + value, + "Ogone: received value %s for input %s (expected %s)" % ( + inputs[form_key], form_key, value, + ) + ) diff --git a/addons/payment_ogone/views/payment_ogone_templates.xml b/addons/payment_ogone/views/payment_ogone_templates.xml new file mode 100644 index 00000000000..697410a7207 --- /dev/null +++ b/addons/payment_ogone/views/payment_ogone_templates.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/addons/payment_ogone/views/payment_views.xml b/addons/payment_ogone/views/payment_views.xml new file mode 100644 index 00000000000..3f2935dad75 --- /dev/null +++ b/addons/payment_ogone/views/payment_views.xml @@ -0,0 +1,21 @@ + + + + + Ogone Acquirer Form + payment.acquirer + + + + + + + + + + + + + + +