From 94bddadf4e2339e9c5ba997d36a6c78af4aa44f9 Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Thu, 17 Feb 2022 14:14:57 +0000 Subject: [PATCH] [ADD] payment_flutterwave: integrate with "Flutterwave standard" gateway See README for technical details. task-2759117 Part-of: odoo/odoo#85514 --- addons/payment/data/payment_acquirer_data.xml | 30 +++ addons/payment/data/payment_icon_data.xml | 25 +++ addons/payment/static/img/airtel-money.png | Bin 0 -> 2916 bytes .../static/img/barter-by-flutterwave.png | Bin 0 -> 24156 bytes addons/payment/static/img/m-pesa.png | Bin 0 -> 3222 bytes .../payment/static/img/mtn-mobile-money.png | Bin 0 -> 5074 bytes addons/payment/tests/common.py | 1 + addons/payment/tests/utils.py | 9 +- addons/payment_flutterwave/README.md | 31 +++ addons/payment_flutterwave/__init__.py | 10 + addons/payment_flutterwave/__manifest__.py | 20 ++ addons/payment_flutterwave/const.py | 47 +++++ .../controllers/__init__.py | 3 + .../payment_flutterwave/controllers/main.py | 81 +++++++ .../data/payment_acquirer_data.xml | 16 ++ addons/payment_flutterwave/models/__init__.py | 6 + .../models/account_payment_method.py | 13 ++ .../models/payment_acquirer.py | 115 ++++++++++ .../models/payment_token.py | 11 + .../models/payment_transaction.py | 199 ++++++++++++++++++ .../static/description/icon.png | Bin 0 -> 7503 bytes .../static/description/icon.svg | 80 +++++++ .../static/src/img/flutterwave-logo.png | Bin 0 -> 3458 bytes addons/payment_flutterwave/tests/__init__.py | 6 + addons/payment_flutterwave/tests/common.py | 43 ++++ .../tests/test_payment_acquirer.py | 27 +++ .../tests/test_payment_transaction.py | 61 ++++++ .../tests/test_processing_flows.py | 83 ++++++++ .../views/payment_flutterwave_templates.xml | 8 + .../views/payment_views.xml | 28 +++ 30 files changed, 950 insertions(+), 3 deletions(-) create mode 100644 addons/payment/static/img/airtel-money.png create mode 100644 addons/payment/static/img/barter-by-flutterwave.png create mode 100644 addons/payment/static/img/m-pesa.png create mode 100644 addons/payment/static/img/mtn-mobile-money.png create mode 100644 addons/payment_flutterwave/README.md create mode 100644 addons/payment_flutterwave/__init__.py create mode 100644 addons/payment_flutterwave/__manifest__.py create mode 100644 addons/payment_flutterwave/const.py create mode 100644 addons/payment_flutterwave/controllers/__init__.py create mode 100644 addons/payment_flutterwave/controllers/main.py create mode 100644 addons/payment_flutterwave/data/payment_acquirer_data.xml create mode 100644 addons/payment_flutterwave/models/__init__.py create mode 100644 addons/payment_flutterwave/models/account_payment_method.py create mode 100644 addons/payment_flutterwave/models/payment_acquirer.py create mode 100644 addons/payment_flutterwave/models/payment_token.py create mode 100644 addons/payment_flutterwave/models/payment_transaction.py create mode 100644 addons/payment_flutterwave/static/description/icon.png create mode 100644 addons/payment_flutterwave/static/description/icon.svg create mode 100644 addons/payment_flutterwave/static/src/img/flutterwave-logo.png create mode 100644 addons/payment_flutterwave/tests/__init__.py create mode 100644 addons/payment_flutterwave/tests/common.py create mode 100644 addons/payment_flutterwave/tests/test_payment_acquirer.py create mode 100644 addons/payment_flutterwave/tests/test_payment_transaction.py create mode 100644 addons/payment_flutterwave/tests/test_processing_flows.py create mode 100644 addons/payment_flutterwave/views/payment_flutterwave_templates.xml create mode 100644 addons/payment_flutterwave/views/payment_views.xml diff --git a/addons/payment/data/payment_acquirer_data.xml b/addons/payment/data/payment_acquirer_data.xml index 4d8bc0cbea1..307310a0ca6 100644 --- a/addons/payment/data/payment_acquirer_data.xml +++ b/addons/payment/data/payment_acquirer_data.xml @@ -115,6 +115,36 @@ ])]"/> + + Flutterwave + + + +

+ A payment gateway from Flutterwave to accept online payments. +

+
    +
  • Online Payments
  • +
  • Payment Status Tracking
  • +
  • Subscriptions
  • +
  • Save Cards
  • +
+
+ + + +
+ Mollie diff --git a/addons/payment/data/payment_icon_data.xml b/addons/payment/data/payment_icon_data.xml index 452d8a4e64f..97c44aa6b42 100644 --- a/addons/payment/data/payment_icon_data.xml +++ b/addons/payment/data/payment_icon_data.xml @@ -1,5 +1,6 @@ + 10 VISA @@ -126,4 +127,28 @@ + + 220 + M-Pesa + + + + + 230 + Airtel Money + + + + + 240 + MTN Mobile Money + + + + + 250 + Barter by Flutterwave + + + diff --git a/addons/payment/static/img/airtel-money.png b/addons/payment/static/img/airtel-money.png new file mode 100644 index 0000000000000000000000000000000000000000..d000b513e61e2846f7ccd3efd53d86e47868168d GIT binary patch literal 2916 zcmYjS_d6AgAH_8;8Q09pCM0{4uDwTAA!HL$cG=fXxNjGAGm^^4O7^JU;tGX~%#4sd zE;pk$`Fwky_m}T^o^#G~{(y5%x|M|?106RV85tRav5}tjAMg3YOeEru5?Bc#WMmYp zR%W*P@CRI6P*nwwAH%(SaPcDi{taYgKurxNCtzRz-K^h>5}QFi=s! z@iF}R1z`y{fPC-Bb$jHDy|3F+EaB{-8 zZ}9OW(9po)A!KF(B_$jjfPw;i{|>aYfJTFv88|pVN(!*E11~QK3&Zj<wg!uf5ETVkSuiyPY;3T;4*B^I843IQz|Rk#K7pbl{P+PXDu6

Z0=l~J^(!aQZZqlz_cG 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 literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/barter-by-flutterwave.png b/addons/payment/static/img/barter-by-flutterwave.png new file mode 100644 index 0000000000000000000000000000000000000000..5db09322cc8a02f294a668e0224eebafc6b54e1d GIT binary patch literal 24156 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 literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/m-pesa.png b/addons/payment/static/img/m-pesa.png new file mode 100644 index 0000000000000000000000000000000000000000..7d208c7c844704b80bb6afe72515a590f71cb587 GIT binary patch literal 3222 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 literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/mtn-mobile-money.png b/addons/payment/static/img/mtn-mobile-money.png new file mode 100644 index 0000000000000000000000000000000000000000..2bc9ec29fb91c8c9dd84ba9d329936c4a426a93c GIT binary patch literal 5074 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 literal 0 HcmV?d00001 diff --git a/addons/payment/tests/common.py b/addons/payment/tests/common.py index 167920721c3..2f673b50dda 100644 --- a/addons/payment/tests/common.py +++ b/addons/payment/tests/common.py @@ -96,6 +96,7 @@ class PaymentCommon(PaymentTestUtils): cls.acquirer = cls.dummy_acquirer cls.amount = 1111.11 cls.company = cls.env.company + cls.company_id = cls.company.id cls.currency = cls.currency_euro cls.partner = cls.default_partner cls.reference = "Test Transaction" diff --git a/addons/payment/tests/utils.py b/addons/payment/tests/utils.py index 31544ce634e..6b3c71fa4bb 100644 --- a/addons/payment/tests/utils.py +++ b/addons/payment/tests/utils.py @@ -40,10 +40,13 @@ class PaymentTestUtils(AccountTestInvoicingCommon): :rtype: dict[str:str] """ html_tree = objectify.fromstring(html_form) - action = html_tree.get('action') - inputs = {form_input.get('name'): form_input.get('value') for form_input in html_tree.input} + if hasattr(html_tree, 'input'): + inputs = {input_.get('name'): input_.get('value') for input_ in html_tree.input} + else: + inputs = {} return { - 'action': action, + 'action': html_tree.get('action'), + 'method': html_tree.get('method'), 'inputs': inputs, } diff --git a/addons/payment_flutterwave/README.md b/addons/payment_flutterwave/README.md new file mode 100644 index 00000000000..82d7b9cb226 --- /dev/null +++ b/addons/payment_flutterwave/README.md @@ -0,0 +1,31 @@ +# Flutterwave + +## Implementation details + +### Supported features + +- Payment with redirection flow +- [Tokenization](https://developer.flutterwave.com/reference/endpoints/tokenized-charge/) +- Several payment methods such as credit cards, M-Pesa, and + [others](https://developer.flutterwave.com/docs/collecting-payments/payment-methods/). +- [Webhook](https://developer.flutterwave.com/docs/integration-guides/webhooks/). + +In addition, Flutterwave also allows to implement refunds and pre-authorizations. + +### API and gateway + +We choose to integrate with +[Flutterwave standard](https://developer.flutterwave.com/docs/collecting-payments/standard/) as it +is the gateway that covers the best our needs, out of the three that Flutterwave offers as of +May 2022. See the task's dev notes for the details on the other gateways. + +The version of the API implemented by this module is v3. + +## Merge details + +The first version of the module was specified in task +[2759117](https://www.odoo.com/web#id=2759117&model=project.task) and merged with PR +odoo/odoo#84820 in `saas-15.4`. + +## Testing instructions + diff --git a/addons/payment_flutterwave/__init__.py b/addons/payment_flutterwave/__init__.py new file mode 100644 index 00000000000..14fae20949e --- /dev/null +++ b/addons/payment_flutterwave/__init__.py @@ -0,0 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models + +from odoo.addons.payment import reset_payment_acquirer + + +def uninstall_hook(cr, registry): + reset_payment_acquirer(cr, registry, 'flutterwave') diff --git a/addons/payment_flutterwave/__manifest__.py b/addons/payment_flutterwave/__manifest__.py new file mode 100644 index 00000000000..3d090b12194 --- /dev/null +++ b/addons/payment_flutterwave/__manifest__.py @@ -0,0 +1,20 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Payment Acquirer: Flutterwave", + 'version': '2.0', + 'category': 'Accounting/Payment Acquirers', + 'sequence': 360, + 'summary': "A Nigerian online payments provider covering several African countries and payment " + "methods.", + 'depends': ['payment'], + 'data': [ + 'views/payment_views.xml', + 'views/payment_flutterwave_templates.xml', + + 'data/payment_acquirer_data.xml', + ], + 'application': True, + 'uninstall_hook': 'uninstall_hook', + 'license': 'LGPL-3', +} diff --git a/addons/payment_flutterwave/const.py b/addons/payment_flutterwave/const.py new file mode 100644 index 00000000000..4c57e2c01ea --- /dev/null +++ b/addons/payment_flutterwave/const.py @@ -0,0 +1,47 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# The currencies supported by Flutterwave, in ISO 4217 format. +# See https://support.flutterwave.com/en/articles/3632719-accepted-currencies. +SUPPORTED_CURRENCIES = [ + 'ARS', + 'BRL', + 'GBP', + 'CAD', + 'CVE', + 'CLP', + 'COP', + 'CDF', + 'EGP', + 'EUR', + 'GMD', + 'GHS', + 'GNF', + 'KES', + 'LRD', + 'MWK', + 'MXN', + 'MAD', + 'MZN', + 'NGN', + 'SOL', + 'RWF', + 'SLL', + 'STD', + 'ZAR', + 'TZS', + 'UGX', + 'USD', + 'XAF', + 'XOF', + 'ZMK', + 'ZMW', +] + + +# Mapping of transaction states to Flutterwave payment statuses. +PAYMENT_STATUS_MAPPING = { + 'pending': ['pending auth'], + 'done': ['successful'], + 'cancel': ['cancelled'], + 'error': ['failed'], +} diff --git a/addons/payment_flutterwave/controllers/__init__.py b/addons/payment_flutterwave/controllers/__init__.py new file mode 100644 index 00000000000..80ee4da1c5e --- /dev/null +++ b/addons/payment_flutterwave/controllers/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/payment_flutterwave/controllers/main.py b/addons/payment_flutterwave/controllers/main.py new file mode 100644 index 00000000000..8da739791c9 --- /dev/null +++ b/addons/payment_flutterwave/controllers/main.py @@ -0,0 +1,81 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hmac +import logging +import pprint + +from werkzeug.exceptions import Forbidden + +from odoo import http +from odoo.exceptions import ValidationError +from odoo.http import request + + +_logger = logging.getLogger(__name__) + + +class FlutterwaveController(http.Controller): + _return_url = '/payment/flutterwave/return' + _webhook_url = '/payment/flutterwave/webhook' + + @http.route(_return_url, type='http', methods=['GET'], auth='public') + def flutterwave_return_from_checkout(self, **data): + """ Process the notification data sent by Flutterwave after redirection from checkout. + + :param dict data: The notification data. + """ + # Handle the notification data. + if data.get('status') != 'cancelled': + request.env['payment.transaction'].sudo()._handle_notification_data('flutterwave', data) + else: # The customer cancelled the payment by clicking on the close button. + pass # Don't try to process this case because the transaction id was not provided. + + # Redirect the user to the status page. + return request.redirect('/payment/status') + + @http.route(_webhook_url, type='http', methods=['POST'], auth='public', csrf=False) + def flutterwave_webhook(self): + """ Process the notification data sent by Flutterwave to the webhook. + + :return: An empty string to acknowledge the notification. + :rtype: str + """ + data = request.get_json_data() + _logger.info("Notification received from Flutterwave with data:\n%s", pprint.pformat(data)) + + if data['event'] == 'charge.completed': + try: + # Check the origin of the notification. + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( + 'flutterwave', data['data'] + ) + signature = request.httprequest.headers.get('verif-hash') + self._verify_notification_signature(signature, tx_sudo) + + # Handle the notification data. + notification_data = data['data'] + tx_sudo._handle_notification_data('flutterwave', notification_data) + except ValidationError: # Acknowledge the notification to avoid getting spammed. + _logger.exception("Unable to handle the notification data; skipping to acknowledge") + return request.make_json_response('') + + @staticmethod + def _verify_notification_signature(received_signature, tx_sudo): + """ Check that the received signature matches the expected one. + + :param dict received_signature: The signature received with the notification data. + :param recordset tx_sudo: The sudoed transaction referenced by the notification data, as a + `payment.transaction` record. + :return: None + :raise Forbidden: If the signatures don't match. + """ + # Check for the received signature. + if not received_signature: + _logger.warning("Received notification with missing signature.") + raise Forbidden() + + # Compare the received signature with the expected signature. + expected_signature = tx_sudo.acquirer_id.flutterwave_webhook_secret + if not hmac.compare_digest(received_signature, expected_signature): + _logger.warning("Received notification with invalid signature.") + raise Forbidden() diff --git a/addons/payment_flutterwave/data/payment_acquirer_data.xml b/addons/payment_flutterwave/data/payment_acquirer_data.xml new file mode 100644 index 00000000000..fc23febe681 --- /dev/null +++ b/addons/payment_flutterwave/data/payment_acquirer_data.xml @@ -0,0 +1,16 @@ + + + + + flutterwave + + True + + + + Flutterwave + flutterwave + inbound + + + diff --git a/addons/payment_flutterwave/models/__init__.py b/addons/payment_flutterwave/models/__init__.py new file mode 100644 index 00000000000..2303108a7c9 --- /dev/null +++ b/addons/payment_flutterwave/models/__init__.py @@ -0,0 +1,6 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_payment_method +from . import payment_acquirer +from . import payment_token +from . import payment_transaction diff --git a/addons/payment_flutterwave/models/account_payment_method.py b/addons/payment_flutterwave/models/account_payment_method.py new file mode 100644 index 00000000000..a641b6e5215 --- /dev/null +++ b/addons/payment_flutterwave/models/account_payment_method.py @@ -0,0 +1,13 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class AccountPaymentMethod(models.Model): + _inherit = 'account.payment.method' + + @api.model + def _get_payment_method_information(self): + res = super()._get_payment_method_information() + res['flutterwave'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]} + return res diff --git a/addons/payment_flutterwave/models/payment_acquirer.py b/addons/payment_flutterwave/models/payment_acquirer.py new file mode 100644 index 00000000000..fc3ffc48143 --- /dev/null +++ b/addons/payment_flutterwave/models/payment_acquirer.py @@ -0,0 +1,115 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import pprint + +import requests +from werkzeug.urls import url_join + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from odoo.addons.payment_flutterwave.const import SUPPORTED_CURRENCIES + + +_logger = logging.getLogger(__name__) + + +class PaymentAcquirer(models.Model): + _inherit = 'payment.acquirer' + + provider = fields.Selection( + selection_add=[('flutterwave', "Flutterwave")], ondelete={'flutterwave': 'set default'} + ) + flutterwave_public_key = fields.Char( + string="Flutterwave Public Key", + help="The key solely used to identify the account with Flutterwave.", + required_if_provider='flutterwave', + ) + flutterwave_secret_key = fields.Char( + string="Flutterwave Secret Key", + required_if_provider='flutterwave', + groups='base.group_system', + ) + flutterwave_webhook_secret = fields.Char( + string="Flutterwave Webhook Secret", + required_if_provider='flutterwave', + groups='base.group_system', + ) + + #=== COMPUTE METHODS ===# + + def _compute_feature_support_fields(self): + """ Override of `payment` to enable additional features. """ + super()._compute_feature_support_fields() + self.filtered(lambda acq: acq.provider == 'flutterwave').update({ + 'support_tokenization': True, + }) + + # === BUSINESS METHODS ===# + + @api.model + def _get_compatible_acquirers(self, *args, currency_id=None, is_validation=False, **kwargs): + """ Override of payment to filter out Flutterwave acquirers for unsupported currencies or + for validation operations. """ + acquirers = super()._get_compatible_acquirers( + *args, currency_id=currency_id, is_validation=is_validation, **kwargs + ) + + currency = self.env['res.currency'].browse(currency_id).exists() + if (currency and currency.name not in SUPPORTED_CURRENCIES) or is_validation: + acquirers = acquirers.filtered(lambda a: a.provider != 'flutterwave') + + return acquirers + + def _flutterwave_make_request(self, endpoint, payload=None, method='POST'): + """ Make a request to Flutterwave API at the specified endpoint. + + Note: self.ensure_one() + + :param str endpoint: The endpoint to be reached by the request. + :param dict payload: The payload of the request. + :param str method: The HTTP method of the request. + :return The JSON-formatted content of the response. + :rtype: dict + :raise ValidationError: If an HTTP error occurs. + """ + self.ensure_one() + + url = url_join('https://api.flutterwave.com/v3/', endpoint) + headers = {'Authorization': f'Bearer {self.flutterwave_secret_key}'} + try: + if method == 'GET': + response = requests.get(url, params=payload, headers=headers, timeout=10) + else: + response = requests.post(url, json=payload, headers=headers, timeout=10) + try: + response.raise_for_status() + except requests.exceptions.HTTPError: + _logger.exception( + "Invalid API request at %s with data:\n%s", url, pprint.pformat(payload), + ) + raise ValidationError("Flutterwave: " + _( + "The communication with the API failed. Flutterwave gave us the following " + "information: '%s'", response.json().get('message', '') + )) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + _logger.exception("Unable to reach endpoint at %s", url) + raise ValidationError( + "Flutterwave: " + _("Could not establish the connection to the API.") + ) + return response.json() + + def _get_default_payment_method_id(self): + self.ensure_one() + if self.provider != 'flutterwave': + return super()._get_default_payment_method_id() + return self.env.ref('payment_flutterwave.payment_method_flutterwave').id + + def _neutralize(self): + super()._neutralize() + self._neutralize_fields('flutterwave', [ + 'flutterwave_public_key', + 'flutterwave_secret_key', + 'flutterwave_webhook_secret', + ]) diff --git a/addons/payment_flutterwave/models/payment_token.py b/addons/payment_flutterwave/models/payment_token.py new file mode 100644 index 00000000000..d03d1e32d20 --- /dev/null +++ b/addons/payment_flutterwave/models/payment_token.py @@ -0,0 +1,11 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class PaymentToken(models.Model): + _inherit = 'payment.token' + + flutterwave_customer_email = fields.Char( + help="The email of the customer at the time the token was created.", readonly=True + ) diff --git a/addons/payment_flutterwave/models/payment_transaction.py b/addons/payment_flutterwave/models/payment_transaction.py new file mode 100644 index 00000000000..ec2625d4b0c --- /dev/null +++ b/addons/payment_flutterwave/models/payment_transaction.py @@ -0,0 +1,199 @@ +# 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.const import PAYMENT_STATUS_MAPPING +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 acquirer-specific processing values. + :rtype: dict + """ + res = super()._get_specific_rendering_values(processing_values) + if self.provider != 'flutterwave': + return res + + # Initiate the payment and retrieve the payment link data. + base_url = self.acquirer_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_link_data = self.acquirer_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 != '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.acquirer_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.acquirer_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, notification_data): + """ Override of payment to find the transaction based on Flutterwave data. + + :param str provider: The provider of the acquirer 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, notification_data) + if provider != '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', '=', '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 != 'flutterwave': + return + + # Verify the notification data. + verification_response_content = self.acquirer_id._flutterwave_make_request( + 'transactions/verify_by_reference', payload={'tx_ref': self.reference}, method='GET' + ) + verified_data = verification_response_content['data'] + + # Process the verified notification data. + self.acquirer_reference = verified_data['id'] + payment_status = verified_data['status'].lower() + if payment_status in PAYMENT_STATUS_MAPPING['pending']: + self._set_pending() + elif payment_status in 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 PAYMENT_STATUS_MAPPING['cancel']: + self._set_canceled() + elif payment_status in 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({ + 'acquirer_id': self.acquirer_id.id, + 'name': payment_utils.build_token_name(notification_data['card']['last_4digits']), + 'partner_id': self.partner_id.id, + 'acquirer_ref': notification_data['card']['token'], + 'flutterwave_customer_email': notification_data['customer']['email'], + 'verified': True, # The payment is confirmed, so the payment method is valid. + }) + 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, + }, + ) diff --git a/addons/payment_flutterwave/static/description/icon.png b/addons/payment_flutterwave/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e4b7fdaea458227f964884bbd8a6b45c77f05589 GIT binary patch literal 7503 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 Z_<#0moh%+(?CJmj002ovPDHLkV1oF%NL~N{ literal 0 HcmV?d00001 diff --git a/addons/payment_flutterwave/static/description/icon.svg b/addons/payment_flutterwave/static/description/icon.svg new file mode 100644 index 00000000000..39fa2cfe167 --- /dev/null +++ b/addons/payment_flutterwave/static/description/icon.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/payment_flutterwave/static/src/img/flutterwave-logo.png b/addons/payment_flutterwave/static/src/img/flutterwave-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..63ba8deb6070c351f39762c4c29f6b9f7b3ad85e GIT binary patch literal 3458 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 + + + + + diff --git a/addons/payment_flutterwave/views/payment_views.xml b/addons/payment_flutterwave/views/payment_views.xml new file mode 100644 index 00000000000..a7e06ce333b --- /dev/null +++ b/addons/payment_flutterwave/views/payment_views.xml @@ -0,0 +1,28 @@ + + + + + Flutterwave Acquirer Form + payment.acquirer + + + + + + + + + + + + +