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
|
# 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_flutterwave import const
from odoo.addons.payment_flutterwave.controllers.main import FlutterwaveController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return Flutterwave-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 provider-specific processing values.
:rtype: dict
"""
res = super()._get_specific_rendering_values(processing_values)
if self.provider_code != 'flutterwave':
return res
# Initiate the payment and retrieve the payment link data.
base_url = self.provider_id.get_base_url()
payload = {
'tx_ref': self.reference,
'amount': self.amount,
'currency': self.currency_id.name,
'redirect_url': urls.url_join(base_url, FlutterwaveController._return_url),
'customer': {
'email': self.partner_email,
'name': self.partner_name,
'phonenumber': self.partner_phone,
},
'customizations': {
'title': self.company_id.name,
'logo': urls.url_join(base_url, f'web/image/res.company/{self.company_id.id}/logo'),
},
'payment_options': const.PAYMENT_METHODS_MAPPING.get(
self.payment_method_code, self.payment_method_code
),
}
payment_link_data = self.provider_id._flutterwave_make_request('payments', payload=payload)
# Extract the payment link URL and embed it in the redirect form.
rendering_values = {
'api_url': payment_link_data['data']['link'],
}
return rendering_values
def _send_payment_request(self):
""" Override of payment to send a payment request to Flutterwave.
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 != 'flutterwave':
return
# Prepare the payment request to Flutterwave.
if not self.token_id:
raise UserError("Flutterwave: " + _("The transaction is not linked to a token."))
first_name, last_name = payment_utils.split_partner_name(self.partner_name)
data = {
'token': self.token_id.provider_ref,
'email': self.token_id.flutterwave_customer_email,
'amount': self.amount,
'currency': self.currency_id.name,
'country': self.company_id.country_id.code,
'tx_ref': self.reference,
'first_name': first_name,
'last_name': last_name,
'ip': payment_utils.get_customer_ip_address(),
}
# Make the payment request to Flutterwave.
response_content = self.provider_id._flutterwave_make_request(
'tokenized-charges', payload=data
)
# Handle the payment request response.
_logger.info(
"payment request response for transaction with reference %s:\n%s",
self.reference, pprint.pformat(response_content)
)
self._handle_notification_data('flutterwave', response_content['data'])
def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Override of payment to find the transaction based on Flutterwave 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: recordset of `payment.transaction`
:raise ValidationError: If inconsistent data were received.
:raise ValidationError: If the data match no transaction.
"""
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
if provider_code != 'flutterwave' or len(tx) == 1:
return tx
reference = notification_data.get('tx_ref')
if not reference:
raise ValidationError("Flutterwave: " + _("Received data with missing reference."))
tx = self.search([('reference', '=', reference), ('provider_code', '=', 'flutterwave')])
if not tx:
raise ValidationError(
"Flutterwave: " + _("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 Flutterwave data.
Note: self.ensure_one()
:param dict notification_data: The notification data sent by the provider.
:return: None
:raise ValidationError: If inconsistent data were received.
"""
super()._process_notification_data(notification_data)
if self.provider_code != 'flutterwave':
return
# Verify the notification data.
verification_response_content = self.provider_id._flutterwave_make_request(
'transactions/verify_by_reference', payload={'tx_ref': self.reference}, method='GET'
)
verified_data = verification_response_content['data']
# Update the provider reference.
self.provider_reference = verified_data['id']
# Update payment method.
payment_method_type = verified_data.get('payment_type', '')
if payment_method_type == 'card':
payment_method_type = verified_data.get('card', {}).get('type').lower()
payment_method = self.env['payment.method']._get_from_code(
payment_method_type, mapping=const.PAYMENT_METHODS_MAPPING
)
self.payment_method_id = payment_method or self.payment_method_id
# Update the payment state.
payment_status = verified_data['status'].lower()
if payment_status in const.PAYMENT_STATUS_MAPPING['pending']:
self._set_pending()
elif payment_status in const.PAYMENT_STATUS_MAPPING['done']:
self._set_done()
has_token_data = 'token' in verified_data.get('card', {})
if self.tokenize and has_token_data:
self._flutterwave_tokenize_from_notification_data(verified_data)
elif payment_status in const.PAYMENT_STATUS_MAPPING['cancel']:
self._set_canceled()
elif payment_status in const.PAYMENT_STATUS_MAPPING['error']:
self._set_error(_(
"An error occurred during the processing of your payment (status %s). Please try "
"again.", payment_status
))
else:
_logger.warning(
"Received data with invalid payment status (%s) for transaction with reference %s.",
payment_status, self.reference
)
self._set_error("Flutterwave: " + _("Unknown payment status: %s", payment_status))
def _flutterwave_tokenize_from_notification_data(self, notification_data):
""" Create a new token based on the notification data.
Note: self.ensure_one()
:param dict notification_data: The notification 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': notification_data['card']['last_4digits'],
'partner_id': self.partner_id.id,
'provider_ref': notification_data['card']['token'],
'flutterwave_customer_email': notification_data['customer']['email'],
})
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,
},
)
|