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
+
+
+
+
+
+
+
+
+
+
+