From eec6d4fd06e23188aa5642dd12aea469fdf9b5e9 Mon Sep 17 00:00:00 2001 From: Abdurrahman Saber Date: Wed, 31 Dec 2025 22:51:07 +0400 Subject: [PATCH 1/2] [FIX] payment_hyperpay_tokenization: fix brand error & http response, improve logging --- .../controllers/main.py | 114 ++++++++++++------ 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/odex25_donation/payment_hyperpay_tokenization/controllers/main.py b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py index ede065c19..4e1152b51 100644 --- a/odex25_donation/payment_hyperpay_tokenization/controllers/main.py +++ b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py @@ -4,7 +4,6 @@ import logging from odoo import _ from odoo.http import route, request, Controller from odoo.addons.payment_hyperpay.data.payment_icon import payment_icon -from odoo.addons.payment.controllers.portal import PaymentProcessing from odoo.addons.payment.models.payment_acquirer import PaymentToken from datetime import datetime @@ -24,9 +23,10 @@ class HyperPayTokenization(Controller): checkout_data = self._register_payment_card(kwargs) return checkout_data except Exception as er: + _logger.exception('Error in token_checkout: %s' % er) return { 'state': False, - 'message': er + 'message': str(er) } def _save_session_data(self, data): @@ -39,11 +39,11 @@ class HyperPayTokenization(Controller): payment_icons = acquirer.payment_icon_ids.mapped('name') data_brands = "VISA" base_url = request.httprequest.host_url - if (len(payment_icon) > 1): + if len(payment_icons) > 0: brands = [payment_icon[i.upper()] for i in payment_icons if i.upper() in payment_icon.keys()] data_brands = brands and " ".join(brands) or data_brands tx = self._create_validation_transaction(acquirer) - payload = self._get_hyperpay_token_payload(acquirer, data_brands, data) + payload = self._get_hyperpay_token_payload(acquirer, data) payload.update({ "merchantTransactionId": tx.reference, @@ -81,19 +81,17 @@ class HyperPayTokenization(Controller): 'return_url': return_url, } - def _get_hyperpay_token_payload(self, acquirer, data_brands, data): + def _get_hyperpay_token_payload(self, acquirer, data): reference_id = request.session.get('hyperpay_token_reference_id') reference_model = request.session.get('hyperpay_token_reference_model') partner_id = request.env.user.partner_id currency = partner_id.currency_id - lang_code = str(request.env['res.lang'].sudo().search([('code', '=', request.env.user.lang)]).iso_code or '').upper() amount = self._get_validation_amount(currency) return { "entityId": acquirer.hyperpay_merchant_id, 'createRegistration': True, "amount": '%.2f' % amount, "currency": currency.name, - 'paymentBrand': data_brands, "paymentType": "DB", 'standingInstruction.mode': 'INITIAL', 'standingInstruction.source': 'CIT', @@ -103,16 +101,14 @@ class HyperPayTokenization(Controller): "billing.street1": partner_id.street or 'Riyadh', "billing.street2": partner_id.street2 or '', "billing.city": partner_id.city or 'Riyadh', - "billing.state": partner_id.state_id.name or 'Riyadh', + "billing.state": partner_id.state_id.name if partner_id.state_id else 'Riyadh', "billing.postcode": partner_id.zip or '', "billing.country": partner_id.country_id.code or 'SA', "customer.givenName": partner_id.name, "customer.surname": '', - "customer.email": partner_id.email, + "customer.email": partner_id.email or 'noemail@example.com', "customer.mobile": partner_id.mobile or partner_id.phone or '', "customer.phone": partner_id.phone or partner_id.mobile or '', - 'customer.ip': request.httprequest.environ["REMOTE_ADDR"], - 'customer.language': lang_code, "customParameters[SHOPPER_acquirer_id]": data.get('acquirer_id', 0), "customParameters[SHOPPER_hyperpay_token_reference_id]": reference_id, "customParameters[SHOPPER_hyperpay_token_reference_model]": reference_model, @@ -142,22 +138,34 @@ class HyperPayTokenization(Controller): return PaymentToken.VALIDATION_AMOUNTS.get(currency.name) else: # If we don't find the user's currency, then we set the currency to EUR and the amount to 1€50. - currency = self.env['res.currency'].search([('name', '=', 'EUR')]) return 1.5 @route('/hyperpay/tokens/result', type='http', auth='public', website=True) def token_return(self, **post): try: _logger.info('Hyperpay Token Return Post: %s' % post) - transaction_id = request.env['payment.transaction'].sudo().search([('id', '=', int(post.get('transaction_id', 0)))]) + + transaction_id = request.env['payment.transaction'].sudo().search([('id', '=', int(post.get('transaction_id', 0)))], limit=1) + if not transaction_id: + _logger.error('Transaction not found: %s' % post.get('transaction_id')) + return request.make_response('Transaction not found', headers=[('Content-Type', 'text/html')]) + acquirer_id = transaction_id.acquirer_id + if not acquirer_id: + _logger.error('Acquirer not found for transaction: %s' % transaction_id.id) + return request.make_response('Payment provider not found', headers=[('Content-Type', 'text/html')]) + + resource_path = post.get('resourcePath') + if not resource_path: + _logger.error('resourcePath not found in callback data: %s' % post) + return request.make_response('Invalid callback data', headers=[('Content-Type', 'text/html')]) if acquirer_id.state == 'test': domain = TEST_URL else: domain = LIVE_URL - url = f"{domain}{post.get('resourcePath')}?entityId={acquirer_id.hyperpay_merchant_id}" + url = f"{domain}{resource_path}?entityId={acquirer_id.hyperpay_merchant_id}" headers = { "Authorization": f"Bearer {acquirer_id.hyperpay_authorization}" } @@ -166,54 +174,84 @@ class HyperPayTokenization(Controller): _logger.info('Hyperpay Token Status Response Text: %s' % response_data.text) resp = response_data.json() _logger.info('Hyperpay Token Status Response JSON: %s' % resp) - result = resp.get('result', {}) - result_code = result.get('code') + + resp.update({'tx_id': transaction_id.id}) + request.env['payment.transaction'].sudo().form_feedback(resp, "hyperpay") + + transaction_id = request.env['payment.transaction'].sudo().search([('id', '=', transaction_id.id)], limit=1) + + if transaction_id.state != 'done': + _logger.error('Validation transaction not done: %s (state: %s)' % (transaction_id.id, transaction_id.state)) + return request.make_response('Payment validation failed. Please try again or contact support.', headers=[('Content-Type', 'text/html')]) - if result_code and not re.match(r"^(000\.000\.|000\.100\.1|000\.[36]|000\.400\.1[12]0|000\.400\.0[^3]|000\.400\.100|000\.200)", result_code): - # Handle failed cards logic here - return {'state': False, 'message': result.get('description', ''), 'errors': resp.get('parameterErrors', [])} - - # create card record and activate recurring donation - card = resp.get('card', {}) - if not card: - return {'state': False, 'message': 'Card data not found'} - self._post_process_token_return(transaction_id, resp) + res = self._post_process_token_return(transaction_id, resp) + if not res.get('state'): + return request.make_response(res.get('message'), headers=[('Content-Type', 'text/html')]) except Exception as er: # request.env.cr.rollback() - _logger.error(er) + _logger.exception('Error processing token return: %s' % er) + return request.make_response('An error occurred while processing your payment. Please contact support.\n Stack Trace: %s' % er, headers=[('Content-Type', 'text/html')]) + return request.redirect('/my/recurring_donation') - def _post_process_token_return(self, transaction_id, data): - data.update({'tx_id': transaction_id.id}) - request.env['payment.transaction'].sudo().form_feedback(data, "hyperpay") - if not transaction_id.state == 'done': - _logger.error('Hyperpay Token Return Transaction not done: %s' % transaction_id.id) - return False + + card = data.get('card', {}) + if not card: + _logger.error('Card data not found in response') + return {'state': False, 'message': 'Card data not found in response'} + + initial_tx_id = data.get('resultDetails', {}).get('CardholderInitiatedTransactionID') or data.get('CardholderInitiatedTransactionID', '') + registration_id = data.get('id', '') + + if not initial_tx_id: + _logger.error('Initial transaction ID not found - token will not work for recurring payments') + return {'state': False, 'message': 'Initial transaction ID not found - token will not work for recurring payments'} + + if not registration_id: + _logger.error('Registration ID not found in response') + return {'state': False, 'message': 'Registration ID not found in response'} + + acquirer_id = transaction_id.acquirer_id + existing_token = request.env['payment.token'].sudo().search([ + ('acquirer_ref', '=', registration_id), + ('acquirer_id', '=', acquirer_id.id) + ], limit=1) + + if existing_token: + _logger.warning('Token already exists for registration %s, returning existing token' % registration_id) + reference_id = int(data.get('customParameters', {}).get('SHOPPER_hyperpay_token_reference_id', 0)) + reference_model = data.get('customParameters', {}).get('SHOPPER_hyperpay_token_reference_model', '') + if reference_id and reference_model and reference_model in request.env: + record_id = request.env[reference_model].sudo().search([('id', '=', reference_id)]) + if record_id and hasattr(record_id, '_post_process_card_tokenization'): + record_id._post_process_card_tokenization(existing_token) + return {'state': True, 'message': 'Token already exists for registration %s, returning existing token' % registration_id} try: transaction_id.hyperpay_s2s_do_refund() except Exception as er: _logger.error('Hyperpay Token Return Transaction refund failed: %s' % er) - acquirer_id = transaction_id.acquirer_id - card = data.get('card', {}) + card_vals = { 'name': f"{card.get('bin', '')}XXXXXXXXXXXX{card.get('last4Digits', '')}", 'partner_id': request.env.user.partner_id.id, 'acquirer_id': acquirer_id.id, - 'acquirer_ref': data.get('id', ''), + 'acquirer_ref': registration_id, 'hyperpay_payment_brand': data.get('paymentBrand'), - 'initial_transaction_id': data.get('resultDetails.CardholderInitiatedTransactionID') or data.get('CardholderInitiatedTransactionID', ''), + 'hyperpay_initial_transaction_id': initial_tx_id, 'verified': True, } token_id = request.env['payment.token'].sudo().create(card_vals) + _logger.info('Created payment token %s for partner %s' % (token_id.id, request.env.user.partner_id.id)) + reference_id = int(data.get('customParameters', {}).get('SHOPPER_hyperpay_token_reference_id', 0)) reference_model = data.get('customParameters', {}).get('SHOPPER_hyperpay_token_reference_model', '') if reference_id and reference_model and reference_model in request.env: record_id = request.env[reference_model].sudo().search([('id', '=', reference_id)]) if record_id and hasattr(record_id, '_post_process_card_tokenization'): record_id._post_process_card_tokenization(token_id) - return token_id - + + return {'state': True, 'message': 'Token created successfully'} \ No newline at end of file From a81535f7bc7d17d8eb9ce53073098f68b9f5bfb2 Mon Sep 17 00:00:00 2001 From: Abdurrahman Saber Date: Wed, 31 Dec 2025 23:00:34 +0400 Subject: [PATCH 2/2] [IMP] brands computation --- .../payment_hyperpay_tokenization/controllers/main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/odex25_donation/payment_hyperpay_tokenization/controllers/main.py b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py index 4e1152b51..250de43bf 100644 --- a/odex25_donation/payment_hyperpay_tokenization/controllers/main.py +++ b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py @@ -37,11 +37,9 @@ class HyperPayTokenization(Controller): acquirer_id = int(data.get('acquirer_id')) acquirer = request.env['payment.acquirer'].sudo().search([('id', '=', acquirer_id)]) payment_icons = acquirer.payment_icon_ids.mapped('name') - data_brands = "VISA" base_url = request.httprequest.host_url - if len(payment_icons) > 0: - brands = [payment_icon[i.upper()] for i in payment_icons if i.upper() in payment_icon.keys()] - data_brands = brands and " ".join(brands) or data_brands + brands = [payment_icon[i.upper()] for i in payment_icons if i.upper() in payment_icon.keys()] + data_brands = brands and " ".join(brands) or "VISA" tx = self._create_validation_transaction(acquirer) payload = self._get_hyperpay_token_payload(acquirer, data)