From 40c880bfdc8284369afa8865a404347b47ebd9fd Mon Sep 17 00:00:00 2001 From: Abdurrahman Saber Date: Wed, 31 Dec 2025 19:01:44 +0400 Subject: [PATCH] [IMP] payment_hyperpay_tokenization: change the flow of CIT to be done with the registration --- .../controllers/main.py | 102 +++++++++++++++--- .../models/payment.py | 36 +------ 2 files changed, 91 insertions(+), 47 deletions(-) diff --git a/odex25_donation/payment_hyperpay_tokenization/controllers/main.py b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py index 4bdfc71b8..ede065c19 100644 --- a/odex25_donation/payment_hyperpay_tokenization/controllers/main.py +++ b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py @@ -1,10 +1,12 @@ import requests import re 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 _logger = logging.getLogger(__name__) @@ -36,16 +38,16 @@ class HyperPayTokenization(Controller): 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_icon) > 1): 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 - base_url = request.httprequest.host_url - payload = { - "entityId": acquirer.hyperpay_merchant_id, - 'createRegistration': True, - } - - payload.update(self._get_hyperpay_token_custom_parameters(data)) + tx = self._create_validation_transaction(acquirer) + payload = self._get_hyperpay_token_payload(acquirer, data_brands, data) + + payload.update({ + "merchantTransactionId": tx.reference, + }) if acquirer.state == 'test': domain = TEST_URL @@ -68,7 +70,7 @@ class HyperPayTokenization(Controller): 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): return {'state': False, 'message': result.get('description', '')} - return_url = f'{base_url}hyperpay/tokens/result?acquirer_id={acquirer_id}' + return_url = f'{base_url}hyperpay/tokens/result?transaction_id={tx.id}' return { 'state': True, @@ -79,20 +81,76 @@ class HyperPayTokenization(Controller): 'return_url': return_url, } - def _get_hyperpay_token_custom_parameters(self, data): + def _get_hyperpay_token_payload(self, acquirer, data_brands, 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', + 'standingInstruction.recurringType': 'STANDING_ORDER', + 'customParameters[paymentFrequency]': 'OTHER', + 'standingInstruction.type': 'UNSCHEDULED', + "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.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.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, } + def _create_validation_transaction(self, acquirer_id): + partner_id = request.env.user.partner_id + currency = partner_id.currency_id + amount = self._get_validation_amount(currency) + + reference = "VALIDATION-%s" % (datetime.now().strftime('%y%m%d_%H%M%S')) + tx = request.env['payment.transaction'].sudo().create({ + 'amount': amount, + 'acquirer_id': acquirer_id.id, + 'type': 'validation', + 'currency_id': currency.id, + 'reference': reference, + 'partner_id': partner_id.id, + 'partner_country_id': partner_id.country_id.id, + 'state_message': _('This Transaction was automatically processed & refunded in order to validate a new credit card.'), + }) + + return tx + + def _get_validation_amount(self, currency): + if PaymentToken.VALIDATION_AMOUNTS.get(currency.name): + 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) - acquirer_id = request.env['payment.acquirer'].sudo().search([('id', '=', int(post.get('acquirer_id', 0)))]) + transaction_id = request.env['payment.transaction'].sudo().search([('id', '=', int(post.get('transaction_id', 0)))]) + acquirer_id = transaction_id.acquirer_id if acquirer_id.state == 'test': domain = TEST_URL @@ -119,26 +177,38 @@ class HyperPayTokenization(Controller): card = resp.get('card', {}) if not card: return {'state': False, 'message': 'Card data not found'} - self._post_process_token_return(resp) + self._post_process_token_return(transaction_id, resp) except Exception as er: # request.env.cr.rollback() _logger.error(er) return request.redirect('/my/recurring_donation') - def _post_process_token_return(self, data): - acquirer_id = int(data.get('customParameters', {}).get('SHOPPER_acquirer_id', 0)) + 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 + + 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, + 'acquirer_id': acquirer_id.id, 'acquirer_ref': data.get('id', ''), 'hyperpay_payment_brand': data.get('paymentBrand'), + 'initial_transaction_id': data.get('resultDetails.CardholderInitiatedTransactionID') or data.get('CardholderInitiatedTransactionID', ''), + 'verified': True, } token_id = request.env['payment.token'].sudo().create(card_vals) - token_id.validate() 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: diff --git a/odex25_donation/payment_hyperpay_tokenization/models/payment.py b/odex25_donation/payment_hyperpay_tokenization/models/payment.py index cd4678d12..0df4ac766 100644 --- a/odex25_donation/payment_hyperpay_tokenization/models/payment.py +++ b/odex25_donation/payment_hyperpay_tokenization/models/payment.py @@ -45,45 +45,20 @@ class HyperPayTransaction(models.Model): return self._hyperpay_s2s_validate_transaction(data) def _hyperpay_get_s2s_transaction_payload(self, data): - partner_id = self.env.user.partner_id base_url = request.httprequest.host_url - lang_code = str(self.env['res.lang'].sudo().search([('code', '=', self.env.user.lang)]).iso_code or '').upper() payload = { "entityId": self.acquirer_id.hyperpay_s2s_entity_id, "amount": '%.2f' % self.amount, "currency": self.currency_id.name, 'paymentBrand': self.payment_token_id.hyperpay_payment_brand, "paymentType": "DB", - "createRegistration": True, - 'standingInstruction.mode': 'INITIAL', - 'standingInstruction.source': 'CIT', + 'standingInstruction.mode': 'REPEATED', + 'standingInstruction.source': 'MIT', 'standingInstruction.recurringType': 'STANDING_ORDER', - 'customParameters[paymentFrequency]': 'OTHER', + 'standingInstruction.initialTransactionId': self.payment_token_id.hyperpay_initial_transaction_id, 'standingInstruction.type': 'UNSCHEDULED', 'shopperResultUrl': f'{base_url}hyperpay/tokens/result?transaction_id={self.id}', - 'notificationUrl': f'{base_url}hyperpay/tokens/result?transaction_id={self.id}', - "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.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.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, } - if self.type != 'validation': - payload.update({ - 'standingInstruction.mode': 'REPEATED', - 'standingInstruction.source': 'MIT', - 'standingInstruction.initialTransactionId': self.payment_token_id.hyperpay_initial_transaction_id - }) - payload.pop('customParameters[paymentFrequency]', None) - payload.pop('createRegistration', None) return payload def hyperpay_s2s_do_refund(self, **kwargs): @@ -95,8 +70,6 @@ class HyperPayTransaction(models.Model): domain = LIVE_URL url = f"{domain}/v1/payments/{self.acquirer_reference}" - base_url = request.httprequest.host_url - payload = { 'entityId': self.acquirer_id.hyperpay_s2s_entity_id, @@ -104,13 +77,14 @@ class HyperPayTransaction(models.Model): 'paymentType': 'RF', 'amount': '%.2f' % self.amount, 'currency': self.currency_id.name, - 'shopperResultUrl': f'{base_url}hyperpay/tokens/result', } headers = { "Authorization": f"Bearer {self.acquirer_id.hyperpay_authorization}" } + _logger.info('Hyperpay S2S Refund Payload: %s' % payload) response = requests.post(url, data=payload, headers=headers) data = response.json() + _logger.info('Hyperpay S2S Refund Response JSON: %s' % data) return self._hyperpay_s2s_validate_transaction(data) def _hyperpay_s2s_validate_transaction(self, data):