diff --git a/addons/payment/data/payment_acquirer_data.xml b/addons/payment/data/payment_acquirer_data.xml index 92853d90bad..09fecf7a146 100644 --- a/addons/payment/data/payment_acquirer_data.xml +++ b/addons/payment/data/payment_acquirer_data.xml @@ -168,6 +168,27 @@ ])]"/> + + Mercado Pago + Credit Card (powered by Mercado Pago) + + + + + + + Mollie diff --git a/addons/payment/data/payment_icon_data.xml b/addons/payment/data/payment_icon_data.xml index 88c7ed3dc26..9f777e7b424 100644 --- a/addons/payment/data/payment_icon_data.xml +++ b/addons/payment/data/payment_icon_data.xml @@ -163,4 +163,16 @@ + + 280 + BBVA Bancomer + + + + + 280 + CitiBanamex + + + diff --git a/addons/payment/static/img/bbva-bancomer.png b/addons/payment/static/img/bbva-bancomer.png new file mode 100644 index 00000000000..5f455b6b3bb Binary files /dev/null and b/addons/payment/static/img/bbva-bancomer.png differ diff --git a/addons/payment/static/img/citibanamex.png b/addons/payment/static/img/citibanamex.png new file mode 100644 index 00000000000..166a4f8d307 Binary files /dev/null and b/addons/payment/static/img/citibanamex.png differ diff --git a/addons/payment_mercado_pago/README.md b/addons/payment_mercado_pago/README.md new file mode 100644 index 00000000000..acff702d5d4 --- /dev/null +++ b/addons/payment_mercado_pago/README.md @@ -0,0 +1,41 @@ +# Mercado Pago + +## Implementation details + +### Supported features + +- Payment with redirection flow +- Several payment methods such as credit cards, debit cards, and + [others](https://www.mercadopago.com.mx/developers/en/docs/checkout-api/payment-methods/other-payment-methods). +- [Webhook](https://www.mercadopago.com.mx/developers/en/docs/notifications/webhooks/webhooks) + notifications. + +### Not implemented features + +- [Manual capture](https://www.mercadopago.com.mx/developers/en/docs/checkout-api/payment-management/capture-authorized-payment). +- [Partial refunds](https://www.mercadopago.com.mx/developers/en/docs/checkout-api/payment-management/cancellations-and-refunds). + +### API and gateway + +We choose to integrate with a combination of the +[Checkout Pro](https://www.mercadopago.com.mx/developers/en/docs/checkout-pro/landing) and +[Checkout API](https://www.mercadopago.com.mx/developers/en/docs/checkout-api/landing) solutions: +The payment with redirection flow is initiated by sending a client HTTP request with a form-encoded +payload like Checkout Pro's JavaScript SDK does under the hood. The remaining API calls are made +according to the Checkout API's documentation. It was not possible to integrate with Checkout Pro +only as it only allows redirecting customers to the payment page, nor with the Checkout API only as +it requires building a custom payment form to accept direct payments from the merchant's website. + +The other gateways were ruled out. See the task's dev notes for the details on the other gateways. + +The API implemented by this module is not versioned. + +## Merge details + +The first version of the module was specified in task +[2704764](https://www.odoo.com/web#id=2704764&model=project.task) and merged with PR +odoo/odoo#83957 in `saas-15.5`. + +## Testing instructions + +https://www.mercadopago.com.mx/developers/en/docs/checkout-api/integration-test/test-cards diff --git a/addons/payment_mercado_pago/__init__.py b/addons/payment_mercado_pago/__init__.py new file mode 100644 index 00000000000..982a588f8cf --- /dev/null +++ b/addons/payment_mercado_pago/__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, 'mercado_pago') diff --git a/addons/payment_mercado_pago/__manifest__.py b/addons/payment_mercado_pago/__manifest__.py new file mode 100644 index 00000000000..700fe52967d --- /dev/null +++ b/addons/payment_mercado_pago/__manifest__.py @@ -0,0 +1,20 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Payment Acquirer: Mercado Pago", + 'version': '1.0', + 'category': 'Accounting/Payment Acquirers', + 'sequence': 350, + 'summary': "An online payments provider covering several countries, currencies and payment " + "methods from Latin America.", + 'depends': ['payment'], + 'data': [ + 'views/payment_mercado_pago_templates.xml', + 'views/payment_views.xml', + + 'data/payment_acquirer_data.xml', # Depends on views/payment_mercado_pago_templates.xml + ], + 'application': True, + 'uninstall_hook': 'uninstall_hook', + 'license': 'LGPL-3', +} diff --git a/addons/payment_mercado_pago/const.py b/addons/payment_mercado_pago/const.py new file mode 100644 index 00000000000..c56a238d50d --- /dev/null +++ b/addons/payment_mercado_pago/const.py @@ -0,0 +1,34 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# Currency codes of the currencies supported by Mercado Pago in ISO 4217 format. +# See https://api.mercadopago.com/currencies. +SUPPORTED_CURRENCIES = [ + 'ARS', # Argentinian Peso + 'BRL', # Real + 'CLP', # Chilean Peso + 'CLF', # Fomento Unity + 'MXN', # Mexican Peso + 'COP', # Colombian Peso + 'CRC', # Colon + 'CUC', # Cuban Convertible Peso + 'CUP', # Cuban Peso + 'DOP', # Dominican Peso + 'GTQ', # Guatemalan Quetzal + 'HNL', # Lempira + 'NIO', # Cordoba + 'PAB', # Balboa + 'PEN', # Sol + 'PYG', # Guarani + 'USD', # US Dollars + 'UYU', # Uruguayan Peso + 'VEF', # Strong Bolivar + 'VES', # Sovereign Bolivar +] + +# Mapping of transaction states to Mercado Pago payment statuses. +# See https://www.mercadopago.com.mx/developers/en/reference/payments/_payments_id/get. +TRANSACTION_STATUS_MAPPING = { + 'pending': ('pending', 'in_process', 'in_mediation'), + 'done': ('approved', 'refunded'), + 'canceled': ('cancelled', 'null'), +} diff --git a/addons/payment_mercado_pago/controllers/__init__.py b/addons/payment_mercado_pago/controllers/__init__.py new file mode 100644 index 00000000000..80ee4da1c5e --- /dev/null +++ b/addons/payment_mercado_pago/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_mercado_pago/controllers/main.py b/addons/payment_mercado_pago/controllers/main.py new file mode 100644 index 00000000000..0cbf884954a --- /dev/null +++ b/addons/payment_mercado_pago/controllers/main.py @@ -0,0 +1,64 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import pprint + +from odoo import http +from odoo.exceptions import ValidationError +from odoo.http import request + + +_logger = logging.getLogger(__name__) + + +class MercadoPagoController(http.Controller): + _return_url = '/payment/mercado_pago/return' + _webhook_url = '/payment/mercado_pago/webhook' + + @http.route(_return_url, type='http', methods=['GET'], auth='public') + def mercado_pago_return_from_checkout(self, **data): + """ Process the notification data sent by Mercado Pago after redirection from checkout. + + :param dict data: The notification data. + """ + # Handle the notification data. + _logger.info("Handling redirection from Mercado Pago with data:\n%s", pprint.pformat(data)) + if data.get('payment_id') != 'null': + request.env['payment.transaction'].sudo()._handle_notification_data( + 'mercado_pago', data + ) + else: # The customer cancelled the payment by clicking on the return button. + pass # Don't try to process this case because the payment id was not provided. + + # Redirect the user to the status page. + return request.redirect('/payment/status') + + @http.route( + f'{_webhook_url}/', type='http', auth='public', methods=['POST'], csrf=False + ) + def mercado_pago_webhook(self, reference, **_kwargs): + """ Process the notification data sent by Mercado Pago to the webhook. + + :param str reference: The transaction reference embedded in the webhook URL. + :param dict _kwargs: The extra query parameters. + :return: An empty string to acknowledge the notification. + :rtype: str + """ + data = request.get_json_data() + _logger.info("Notification received from Mercado Pago with data:\n%s", pprint.pformat(data)) + + # Mercado Pago sends two types of asynchronous notifications: webhook notifications and + # IPNs which are very similar to webhook notifications but are sent later and contain less + # information. Therefore, we filter the notifications we receive based on the 'action' + # (type of event) key as it is not populated for IPNs, and we don't want to process the + # other types of events. + if data.get('action') in ('payment.created', 'payment.updated'): + # Handle the notification data. + try: + payment_id = data.get('data', {}).get('id') + request.env['payment.transaction'].sudo()._handle_notification_data( + 'mercado_pago', {'external_reference': reference, 'payment_id': payment_id} + ) # Use 'external_reference' as the reference key like in the redirect data. + except ValidationError: # Acknowledge the notification to avoid getting spammed. + _logger.exception("Unable to handle the notification data; skipping to acknowledge") + return '' # Acknowledge the notification. diff --git a/addons/payment_mercado_pago/data/payment_acquirer_data.xml b/addons/payment_mercado_pago/data/payment_acquirer_data.xml new file mode 100644 index 00000000000..53fae97a5e2 --- /dev/null +++ b/addons/payment_mercado_pago/data/payment_acquirer_data.xml @@ -0,0 +1,15 @@ + + + + + mercado_pago + + + + + Mercado Pago + mercado_pago + inbound + + + diff --git a/addons/payment_mercado_pago/models/__init__.py b/addons/payment_mercado_pago/models/__init__.py new file mode 100644 index 00000000000..28c8703fcc1 --- /dev/null +++ b/addons/payment_mercado_pago/models/__init__.py @@ -0,0 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_payment_method +from . import payment_acquirer +from . import payment_transaction diff --git a/addons/payment_mercado_pago/models/account_payment_method.py b/addons/payment_mercado_pago/models/account_payment_method.py new file mode 100644 index 00000000000..1abd3a5e9e9 --- /dev/null +++ b/addons/payment_mercado_pago/models/account_payment_method.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# 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['mercado_pago'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]} + return res diff --git a/addons/payment_mercado_pago/models/payment_acquirer.py b/addons/payment_mercado_pago/models/payment_acquirer.py new file mode 100644 index 00000000000..06af0ab1dcb --- /dev/null +++ b/addons/payment_mercado_pago/models/payment_acquirer.py @@ -0,0 +1,96 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import pprint + +import requests +from werkzeug import urls + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from odoo.addons.payment_mercado_pago.const import SUPPORTED_CURRENCIES + + +_logger = logging.getLogger(__name__) + + +class PaymentAcquirer(models.Model): + _inherit = 'payment.acquirer' + + provider = fields.Selection( + selection_add=[('mercado_pago', "Mercado Pago")], ondelete={'mercado_pago': 'set default'} + ) + mercado_pago_access_token = fields.Char( + string="Mercado Pago Access Token", + required_if_provider='mercado_pago', + groups='base.group_system', + ) + + # === BUSINESS METHODS === # + + @api.model + def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs): + """ Override of `payment` to unlist Mercado Pago acquirers for unsupported currencies. """ + acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs) + + currency = self.env['res.currency'].browse(currency_id).exists() + if currency and currency.name not in SUPPORTED_CURRENCIES: + acquirers = acquirers.filtered(lambda a: a.provider != 'mercado_pago') + + return acquirers + + def _mercado_pago_make_request(self, endpoint, payload=None, method='POST'): + """ Make a request to Mercado Pago 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 = urls.url_join('https://api.mercadopago.com', endpoint) + headers = {'Authorization': f'Bearer {self.mercado_pago_access_token}'} + 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), + ) + response_content = response.json() + error_code = response_content.get('error') + error_message = response_content.get('message') + raise ValidationError("Mercado Pago: " + _( + "The communication with the API failed. Mercado Pago gave us the following " + "information: '%s' (code %s)", error_message, error_code + )) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + _logger.exception("Unable to reach endpoint at %s", url) + raise ValidationError( + "Mercado Pago: " + _("Could not establish the connection to the API.") + ) + return response.json() + + def _get_default_payment_method_id(self): + self.ensure_one() + + if self.provider != 'mercado_pago': + return super()._get_default_payment_method_id() + return self.env.ref('payment_mercado_pago.payment_method_mercado_pago').id + + def _neutralize(self): + super()._neutralize() + + self._neutralize_fields('mercado_pago', [ + 'mercado_pago_access_token', + ]) diff --git a/addons/payment_mercado_pago/models/payment_transaction.py b/addons/payment_mercado_pago/models/payment_transaction.py new file mode 100644 index 00000000000..fb988e2be7b --- /dev/null +++ b/addons/payment_mercado_pago/models/payment_transaction.py @@ -0,0 +1,163 @@ +# 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 ValidationError + +from odoo.addons.payment_mercado_pago.const import TRANSACTION_STATUS_MAPPING +from odoo.addons.payment_mercado_pago.controllers.main import MercadoPagoController + + +_logger = logging.getLogger(__name__) + + +class PaymentTransaction(models.Model): + _inherit = 'payment.transaction' + + def _get_specific_rendering_values(self, processing_values): + """ Override of `payment` to return Mercado Pago-specific rendering values. + + Note: self.ensure_one() from `_get_rendering_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 != 'mercado_pago': + return res + + # Initiate the payment and retrieve the payment link data. + payload = self._mercado_pago_prepare_preference_request_payload() + _logger.info( + "Sending '/checkout/preferences' request for link creation:\n%s", + pprint.pformat(payload), + ) + api_url = self.acquirer_id._mercado_pago_make_request( + '/checkout/preferences', payload=payload + )['init_point' if self.acquirer_id.state == 'enabled' else 'sandbox_init_point'] + + # Extract the payment link URL and embed it in the redirect form. + rendering_values = { + 'api_url': api_url, + } + return rendering_values + + def _mercado_pago_prepare_preference_request_payload(self): + """ Create the payload for the preference request based on the transaction values. + + :return: The request payload. + :rtype: dict + """ + base_url = self.acquirer_id.get_base_url() + return_url = urls.url_join(base_url, MercadoPagoController._return_url) + webhook_url = urls.url_join( + base_url, f'{MercadoPagoController._webhook_url}/{self.reference}' + ) # Append the reference to identify the transaction from the webhook notification data. + return { + 'auto_return': 'all', + 'back_urls': { + 'success': return_url, + 'pending': return_url, + 'failure': return_url, + }, + 'external_reference': self.reference, + 'items': [{ + 'title': self.reference, + 'quantity': 1, + 'currency_id': self.currency_id.name, + 'unit_price': self.amount, + }], + 'notification_url': webhook_url, + 'payer': { + 'name': self.partner_name, + 'email': self.partner_email, + 'phone': { + 'number': self.partner_phone, + }, + 'address': { + 'zip_code': self.partner_zip, + 'street_name': self.partner_address, + }, + }, + 'payment_methods': { + 'installments': 1, # Prevent MP from proposing several installments for a payment. + }, + } + + def _get_tx_from_notification_data(self, provider, notification_data): + """ Override of `payment` to find the transaction based on Mercado Pago 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 != 'mercado_pago' or len(tx) == 1: + return tx + + reference = notification_data.get('external_reference') + if not reference: + raise ValidationError("Mercado Pago: " + _("Received data with missing reference.")) + + tx = self.search([('reference', '=', reference), ('provider', '=', 'mercado_pago')]) + if not tx: + raise ValidationError( + "Mercado Pago: " + _("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 Mercado Pago data. + + Note: self.ensure_one() from `_process_notification_data` + + :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 != 'mercado_pago': + return + + payment_id = notification_data.get('payment_id') + if not payment_id: + raise ValidationError("Mercado Pago: " + _("Received data with missing payment id.")) + if self.operation != 'refund': + self.acquirer_reference = payment_id + + # Verify the notification data. + verified_payment_data = self.acquirer_id._mercado_pago_make_request( + f'/v1/payments/{self.acquirer_reference}', method='GET' + ) + + payment_status = verified_payment_data.get('status') + if not payment_status: + raise ValidationError("Mercado Pago: " + _("Received data with missing status.")) + + if payment_status in TRANSACTION_STATUS_MAPPING['pending']: + self._set_pending() + elif payment_status in TRANSACTION_STATUS_MAPPING['done']: + self._set_done() + + # Immediately post-process the transaction if it is a refund, as the post-processing + # will not be triggered by a customer browsing the transaction from the portal. + if self.operation == 'refund': + self.env.ref('payment.cron_post_process_payment_tx')._trigger() + elif payment_status in TRANSACTION_STATUS_MAPPING['canceled']: + self._set_canceled() + else: # Classify unsupported payment status as the `error` tx state. + _logger.warning( + "Received data for transaction with reference %s with invalid payment status: %s", + self.reference, payment_status + ) + self._set_error( + "Mercado Pago: " + _("Received data with invalid status: %s", payment_status) + ) diff --git a/addons/payment_mercado_pago/static/description/icon.png b/addons/payment_mercado_pago/static/description/icon.png new file mode 100644 index 00000000000..b5a9b4b4dc1 Binary files /dev/null and b/addons/payment_mercado_pago/static/description/icon.png differ diff --git a/addons/payment_mercado_pago/static/description/icon.svg b/addons/payment_mercado_pago/static/description/icon.svg new file mode 100644 index 00000000000..702eac660ef --- /dev/null +++ b/addons/payment_mercado_pago/static/description/icon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/payment_mercado_pago/tests/__init__.py b/addons/payment_mercado_pago/tests/__init__.py new file mode 100644 index 00000000000..190a3263b4b --- /dev/null +++ b/addons/payment_mercado_pago/tests/__init__.py @@ -0,0 +1,6 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common +from . import test_payment_acquirer +from . import test_processing_flows +from . import test_payment_transaction diff --git a/addons/payment_mercado_pago/tests/common.py b/addons/payment_mercado_pago/tests/common.py new file mode 100644 index 00000000000..528ccde43ed --- /dev/null +++ b/addons/payment_mercado_pago/tests/common.py @@ -0,0 +1,28 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.payment.tests.common import PaymentCommon + + +class MercadoPagoCommon(PaymentCommon): + + MP_PAYMENT_ID = '1234567890' + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.acquirer = cls._prepare_acquirer('mercado_pago', update_values={ + 'mercado_pago_access_token': 'TEST-4850554046279901-TEST-TEST', + }) + cls.payment_id = '123456' + cls.redirect_notification_data = { + 'external_reference': cls.reference, + 'payment_id': cls.payment_id, + } + cls.webhook_notification_data = { + 'action': 'payment.created', + 'data': {'id': cls.payment_id}, + } + cls.verification_data = { + 'status': 'approved', + } diff --git a/addons/payment_mercado_pago/tests/test_payment_acquirer.py b/addons/payment_mercado_pago/tests/test_payment_acquirer.py new file mode 100644 index 00000000000..957443633ee --- /dev/null +++ b/addons/payment_mercado_pago/tests/test_payment_acquirer.py @@ -0,0 +1,22 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged + +from odoo.addons.payment_mercado_pago.tests.common import MercadoPagoCommon + + +@tagged('post_install', '-at_install') +class TestPaymentAcquirer(MercadoPagoCommon): + + def test_incompatible_with_unsupported_currencies(self): + """ Test that Mercado Pago acquirers are filtered out from compatible acquirers when the + currency is not supported. """ + compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers( + self.company_id, self.partner.id, self.amount, currency_id=self.env.ref('base.AFN').id + ) + self.assertNotIn(self.acquirer, compatible_acquirers) + + def test_neutralize(self): + """ Test that the sensitive fields of the acquirer are correctly neutralized. """ + self.env['payment.acquirer']._neutralize() + self.assertFalse(self.acquirer.mercado_pago_access_token) diff --git a/addons/payment_mercado_pago/tests/test_payment_transaction.py b/addons/payment_mercado_pago/tests/test_payment_transaction.py new file mode 100644 index 00000000000..f8132789e92 --- /dev/null +++ b/addons/payment_mercado_pago/tests/test_payment_transaction.py @@ -0,0 +1,69 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from odoo.tests import tagged +from odoo.tools import mute_logger + +from odoo.addons.payment.tests.http_common import PaymentHttpCommon +from odoo.addons.payment_mercado_pago.tests.common import MercadoPagoCommon + + +@tagged('post_install', '-at_install') +class TestPaymentTransaction(MercadoPagoCommon, PaymentHttpCommon): + + def test_no_item_missing_from_preference_request_payload(self): + """ Test that the request values are conform to the transaction fields. """ + tx = self._create_transaction(flow='redirect') + request_payload = tx._mercado_pago_prepare_preference_request_payload() + self.maxDiff = 10000 # Allow comparing large dicts. + return_url = self._build_url('/payment/mercado_pago/return') + webhook_url = self._build_url('/payment/mercado_pago/webhook') + self.assertDictEqual(request_payload, { + 'auto_return': 'all', + 'back_urls': { + 'failure': return_url, + 'pending': return_url, + 'success': return_url, + }, + 'external_reference': tx.reference, + 'items': [{ + 'currency_id': tx.currency_id.name, + 'quantity': 1, + 'title': tx.reference, + 'unit_price': tx.amount, + }], + 'notification_url': f'{webhook_url}/{tx.reference}', + 'payer': { + 'address': {'street_name': tx.partner_address, 'zip_code': tx.partner_zip}, + 'email': tx.partner_email, + 'name': tx.partner_name, + 'phone': {'number': tx.partner_phone}, + }, + 'payment_methods': {'installments': 1}, + }) + + @mute_logger('odoo.addons.payment.models.payment_transaction') + def test_no_input_missing_from_redirect_form(self): + """ Test that the `api_url` key is not omitted from the rendering values. """ + tx = self._create_transaction(flow='redirect') + with patch( + 'odoo.addons.payment_mercado_pago.models.payment_transaction.PaymentTransaction' + '._get_specific_rendering_values', return_value={'api_url': 'https://dummy.com'} + ): + processing_values = tx._get_processing_values() + form_info = self._extract_values_from_html_form(processing_values['redirect_form_html']) + self.assertEqual(form_info['action'], 'https://dummy.com') + self.assertEqual(form_info['method'], 'post') + self.assertDictEqual(form_info['inputs'], {}) + + def test_processing_notification_data_confirms_transaction(self): + """ Test that the transaction state is set to 'done' when the notification data indicate a + successful payment. """ + tx = self._create_transaction(flow='redirect') + with patch( + 'odoo.addons.payment_mercado_pago.models.payment_acquirer.PaymentAcquirer' + '._mercado_pago_make_request', return_value=self.verification_data + ): + tx._process_notification_data(self.redirect_notification_data) + self.assertEqual(tx.state, 'done') diff --git a/addons/payment_mercado_pago/tests/test_processing_flows.py b/addons/payment_mercado_pago/tests/test_processing_flows.py new file mode 100644 index 00000000000..ae309f493f3 --- /dev/null +++ b/addons/payment_mercado_pago/tests/test_processing_flows.py @@ -0,0 +1,40 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from odoo.tests import tagged +from odoo.tools import mute_logger + +from odoo.addons.payment.tests.http_common import PaymentHttpCommon +from odoo.addons.payment_mercado_pago.controllers.main import MercadoPagoController +from odoo.addons.payment_mercado_pago.tests.common import MercadoPagoCommon + + +@tagged('post_install', '-at_install') +class TestProcessingFlows(MercadoPagoCommon, PaymentHttpCommon): + + @mute_logger('odoo.addons.payment_mercado_pago.controllers.main') + def test_redirect_notification_triggers_processing(self): + """ Test that receiving a redirect notification triggers the processing of the notification + data. """ + self._create_transaction(flow='redirect') + url = self._build_url(MercadoPagoController._return_url) + with patch( + 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' + '._handle_notification_data' + ) as handle_notification_data_mock: + self._make_http_get_request(url, params=self.redirect_notification_data) + self.assertEqual(handle_notification_data_mock.call_count, 1) + + @mute_logger('odoo.addons.payment_mercado_pago.controllers.main') + def test_webhook_notification_triggers_processing(self): + """ Test that receiving a valid webhook notification triggers the processing of the + notification data. """ + tx = self._create_transaction(flow='redirect') + url = self._build_url(f'{MercadoPagoController._webhook_url}/{tx.reference}') + with patch( + 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' + '._handle_notification_data' + ) as handle_notification_data_mock: + self._make_json_request(url, data=self.webhook_notification_data) + self.assertEqual(handle_notification_data_mock.call_count, 1) diff --git a/addons/payment_mercado_pago/views/payment_mercado_pago_templates.xml b/addons/payment_mercado_pago/views/payment_mercado_pago_templates.xml new file mode 100644 index 00000000000..6fa6b6b0a0c --- /dev/null +++ b/addons/payment_mercado_pago/views/payment_mercado_pago_templates.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/addons/payment_mercado_pago/views/payment_views.xml b/addons/payment_mercado_pago/views/payment_views.xml new file mode 100644 index 00000000000..4f371c66ac8 --- /dev/null +++ b/addons/payment_mercado_pago/views/payment_views.xml @@ -0,0 +1,20 @@ + + + + + Mercado Pago Acquirer Form + payment.acquirer + + + + + + + + + + +