1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
|
# 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 _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.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}'
payload = {
'hostedCheckoutSpecificInput': {
'locale': self.partner_lang,
'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,
'countryCode': self.partner_country_id.code,
'state': self.partner_state_id.name,
'street': self.partner_address,
'zip': self.partner_zip,
},
'contactDetails': {
'emailAddress': self.partner_email,
'phoneNumber': self.partner_phone,
},
},
'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)
# 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', '').rstrip('_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')
if not status:
raise ValidationError("Worldline: " + _("Received data with missing payment state."))
if status in const.PAYMENT_STATUS_MAPPING['pending']:
self._set_pending()
elif status in const.PAYMENT_STATUS_MAPPING['done']:
has_token_data = 'token' in payment_method_data
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},
)
|