Odoo18-Base/addons/payment_worldline/models/payment_transaction.py
2025-01-06 10:57:38 +07:00

348 lines
14 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
from werkzeug import urls
from odoo import _, models
from odoo.exceptions import UserError, ValidationError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_worldline import const
from odoo.addons.payment_worldline.controllers.main import WorldlineController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
def _compute_reference(self, provider_code, prefix=None, separator='-', **kwargs):
""" Override of `payment` to ensure that Worldline requirement for references is satisfied.
Worldline requires for references to be at most 30 characters long.
:param str provider_code: The code of the provider handling the transaction.
:param str prefix: The custom prefix used to compute the full reference.
:param str separator: The custom separator used to separate the prefix from the suffix.
:return: The unique reference for the transaction.
:rtype: str
"""
reference = super()._compute_reference(
provider_code, prefix=prefix, separator=separator, **kwargs
)
if provider_code != 'worldline':
return reference
if len(reference) <= 30: # Worldline transaction merchantReference is limited to 30 chars
return reference
prefix = payment_utils.singularize_reference_prefix(prefix='WL')
return super()._compute_reference(
provider_code, prefix=prefix, separator=separator, **kwargs
)
def _get_specific_processing_values(self, processing_values):
""" Override of `payment` to redirect failed token-flow transactions.
If the financial institution insists on user authentication,
this override will reset the transaction, and switch the flow to redirect.
Note: self.ensure_one() from `_get_processing_values`.
:param dict processing_values: The generic processing values of the transaction.
:return: The dict of provider-specific processing values.
:rtype: dict
"""
res = super()._get_specific_processing_values(processing_values)
if (
self.provider_code == 'worldline'
and self.operation == 'online_token'
and self.state == 'error'
and self.state_message.endswith('AUTHORIZATION_REQUESTED')
):
# Tokenized payment failed due to 3-D Secure authentication request.
# Reset transaction to draft and switch to redirect flow.
self.write({
'state': 'draft',
'operation': 'online_redirect',
})
res['force_flow'] = 'redirect'
return res
def _get_specific_rendering_values(self, processing_values):
""" Override of `payment` to return Worldline-specific processing values.
Note: self.ensure_one() from `_get_processing_values`.
:param dict processing_values: The generic processing values of the transaction.
:return: The dict of provider-specific processing values.
:rtype: dict
"""
res = super()._get_specific_rendering_values(processing_values)
if self.provider_code != 'worldline':
return res
checkout_session_data = self._worldline_create_checkout_session()
return {'api_url': checkout_session_data['redirectUrl']}
def _worldline_create_checkout_session(self):
""" Create a hosted checkout session and return the response data.
:return: The hosted checkout session data.
:rtype: dict
"""
self.ensure_one()
base_url = self.provider_id.get_base_url()
return_route = WorldlineController._return_url
return_url_params = urls.url_encode({'provider_id': str(self.provider_id.id)})
return_url = f'{urls.url_join(base_url, return_route)}?{return_url_params}'
first_name, last_name = payment_utils.split_partner_name(self.partner_name)
payload = {
'hostedCheckoutSpecificInput': {
'locale': self.partner_lang or '',
'returnUrl': return_url,
'showResultPage': False,
},
'order': {
'amountOfMoney': {
'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id),
'currencyCode': self.currency_id.name,
},
'customer': { # required to create a token and for some redirected payment methods
'billingAddress': {
'city': self.partner_city or '',
'countryCode': self.partner_country_id.code or '',
'state': self.partner_state_id.name or '',
'street': self.partner_address or '',
'zip': self.partner_zip or '',
},
'contactDetails': {
'emailAddress': self.partner_email or '',
'phoneNumber': self.partner_phone or '',
},
'personalInformation': {
'name': {
'firstName': first_name or '',
'surname': last_name or '',
},
},
},
'references': {
'descriptor': self.reference,
'merchantReference': self.reference,
},
},
}
if self.payment_method_id.code in const.REDIRECT_PAYMENT_METHODS:
payload['redirectPaymentMethodSpecificInput'] = {
'requiresApproval': False, # Force the capture.
'paymentProductId': const.PAYMENT_METHODS_MAPPING[self.payment_method_id.code],
'redirectionData': {
'returnUrl': return_url,
},
}
else:
payload['cardPaymentMethodSpecificInput'] = {
'authorizationMode': 'SALE', # Force the capture.
'tokenize': self.tokenize,
}
if not self.payment_method_id.brand_ids and self.payment_method_id.code != 'card':
worldline_code = const.PAYMENT_METHODS_MAPPING.get(self.payment_method_id.code, 0)
payload['cardPaymentMethodSpecificInput']['paymentProductId'] = worldline_code
else:
pm_codes = self.env['payment.method'].search([
('active', 'in', [True, False]),
('primary_payment_method_id', '=', self.payment_method_id.id),
]).mapped('code')
worldline_codes = [
const.PAYMENT_METHODS_MAPPING[code] for code in pm_codes
if code in const.PAYMENT_METHODS_MAPPING
]
payload['hostedCheckoutSpecificInput']['paymentProductFilters'] = {
'restrictTo': {
'products': worldline_codes,
},
}
_logger.info(
"Sending '/hostedcheckouts' request for transaction with reference %s:\n%s",
self.reference, pprint.pformat(payload)
)
checkout_session_data = self.provider_id._worldline_make_request(
'hostedcheckouts', payload=payload
)
_logger.info(
"Response of '/hostedcheckouts' request for transaction with reference %s:\n%s",
self.reference, pprint.pformat(checkout_session_data)
)
return checkout_session_data
def _send_payment_request(self):
""" Override of `payment` to send a payment request to Worldline.
Note: self.ensure_one()
:return: None
:raise UserError: If the transaction is not linked to a token.
"""
super()._send_payment_request()
if self.provider_code != 'worldline':
return
# Prepare the payment request to Worldline.
if not self.token_id:
raise UserError("Worldline: " + _("The transaction is not linked to a token."))
payload = {
'cardPaymentMethodSpecificInput': {
'authorizationMode': 'SALE', # Force the capture.
'token': self.token_id.provider_ref,
'unscheduledCardOnFileRequestor': 'merchantInitiated',
'unscheduledCardOnFileSequenceIndicator': 'subsequent',
},
'order': {
'amountOfMoney': {
'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id),
'currencyCode': self.currency_id.name,
},
'references': {
'merchantReference': self.reference,
},
},
}
# Make the payment request to Worldline.
response_content = self.provider_id._worldline_make_request(
'payments',
payload=payload,
idempotency_key=payment_utils.generate_idempotency_key(
self, scope='payment_request_token'
)
)
# Handle the payment request response.
_logger.info(
"Response of /payment request for transaction with reference %s:\n%s",
self.reference, pprint.pformat(response_content)
)
self._handle_notification_data('worldline', response_content)
def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Override of `payment` to find the transaction based on Worldline data.
:param str provider_code: The code of the provider that handled the transaction.
:param dict notification_data: The notification data sent by the provider.
:return: The transaction if found.
:rtype: payment.transaction
:raise ValidationError: If inconsistent data are received.
:raise ValidationError: If the data match no transaction.
"""
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
if provider_code != 'worldline' or len(tx) == 1:
return tx
payment_output = notification_data.get('payment', {}).get('paymentOutput', {})
reference = payment_output.get('references', {}).get('merchantReference', '')
if not reference:
raise ValidationError(
"Worldline: " + _("Received data with missing reference %(ref)s.", ref=reference)
)
tx = self.search([('reference', '=', reference), ('provider_code', '=', 'worldline')])
if not tx:
raise ValidationError(
"Worldline: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _process_notification_data(self, notification_data):
""" Override of `payment' to process the transaction based on Worldline data.
Note: self.ensure_one()
:param dict notification_data: The notification data sent by the provider.
:return: None
:raise ValidationError: If inconsistent data are received.
"""
super()._process_notification_data(notification_data)
if self.provider_code != 'worldline':
return
# Update the provider reference.
payment_data = notification_data['payment']
self.provider_reference = payment_data.get('id', '').rsplit('_', 1)[0]
# Update the payment method.
payment_output = payment_data.get('paymentOutput', {})
if 'cardPaymentMethodSpecificOutput' in payment_output:
payment_method_data = payment_output['cardPaymentMethodSpecificOutput']
else:
payment_method_data = payment_output.get('redirectPaymentMethodSpecificOutput', {})
payment_method_code = payment_method_data.get('paymentProductId', '')
payment_method = self.env['payment.method']._get_from_code(
payment_method_code, mapping=const.PAYMENT_METHODS_MAPPING
)
self.payment_method_id = payment_method or self.payment_method_id
# Update the payment state.
status = payment_data.get('status')
has_token_data = 'token' in payment_method_data
if not status:
raise ValidationError("Worldline: " + _("Received data with missing payment state."))
if status in const.PAYMENT_STATUS_MAPPING['pending']:
if status == 'AUTHORIZATION_REQUESTED':
self._set_error("Worldline: " + status)
elif self.operation == 'validation' \
and status in {'PENDING_CAPTURE', 'CAPTURE_REQUESTED'} \
and has_token_data:
self._worldline_tokenize_from_notification_data(payment_method_data)
self._set_done()
else:
self._set_pending()
elif status in const.PAYMENT_STATUS_MAPPING['done']:
if self.tokenize and has_token_data:
self._worldline_tokenize_from_notification_data(payment_method_data)
self._set_done()
else: # Classify unsupported payment status as the `error` tx state.
_logger.info(
"Received data with invalid payment status (%(status)s) for transaction with"
" reference %(ref)s",
{'status': status, 'ref': self.reference},
)
self._set_error("Worldline: " + _(
"Received invalid transaction status %(status)s.", status=status
))
def _worldline_tokenize_from_notification_data(self, pm_data):
""" Create a new token based on the notification data.
Note: self.ensure_one()
:param dict pm_data: The payment method data sent by the provider
:return: None
"""
self.ensure_one()
token = self.env['payment.token'].create({
'provider_id': self.provider_id.id,
'payment_method_id': self.payment_method_id.id,
'payment_details': pm_data.get('card', {}).get('cardNumber', '')[-4:], # Padded with *
'partner_id': self.partner_id.id,
'provider_ref': pm_data['token'],
})
self.write({
'token_id': token,
'tokenize': False,
})
_logger.info(
"Created token with id %(token_id)s for partner with id %(partner_id)s from "
"transaction with reference %(ref)s",
{'token_id': token.id, 'partner_id': self.partner_id.id, 'ref': self.reference},
)