From 15058b6739d75b3fe3a203c0d5ddbfdd2cce91ce Mon Sep 17 00:00:00 2001 From: Horacio Tellez Date: Tue, 30 Aug 2022 10:15:09 +0000 Subject: [PATCH] [ADD] payment_mercado_pago: integrate with Mercado Pago Mercado Pago is the largest online payment platform in Latin America, supports more countries, currencies and payment methods in the region than other PSPs, and is more accessible to small businesses. This commit integrates Odoo with a combination of the Checkout Pro and Checkout API solutions of Mercado Pago to implement payments with redirection to the provider's payment page. Task - 2704764 Part-of: odoo/odoo#83957 Co-authored-by: Antoine Vandevenne (anv) --- addons/payment/data/payment_acquirer_data.xml | 21 +++ addons/payment/data/payment_icon_data.xml | 12 ++ addons/payment/static/img/bbva-bancomer.png | Bin 0 -> 6417 bytes addons/payment/static/img/citibanamex.png | Bin 0 -> 2497 bytes addons/payment_mercado_pago/README.md | 41 +++++ addons/payment_mercado_pago/__init__.py | 10 ++ addons/payment_mercado_pago/__manifest__.py | 20 +++ addons/payment_mercado_pago/const.py | 34 ++++ .../controllers/__init__.py | 3 + .../payment_mercado_pago/controllers/main.py | 64 +++++++ .../data/payment_acquirer_data.xml | 15 ++ .../payment_mercado_pago/models/__init__.py | 5 + .../models/account_payment_method.py | 14 ++ .../models/payment_acquirer.py | 96 +++++++++++ .../models/payment_transaction.py | 163 ++++++++++++++++++ .../static/description/icon.png | Bin 0 -> 16443 bytes .../static/description/icon.svg | 26 +++ addons/payment_mercado_pago/tests/__init__.py | 6 + addons/payment_mercado_pago/tests/common.py | 28 +++ .../tests/test_payment_acquirer.py | 22 +++ .../tests/test_payment_transaction.py | 69 ++++++++ .../tests/test_processing_flows.py | 40 +++++ .../views/payment_mercado_pago_templates.xml | 10 ++ .../views/payment_views.xml | 20 +++ 24 files changed, 719 insertions(+) create mode 100644 addons/payment/static/img/bbva-bancomer.png create mode 100644 addons/payment/static/img/citibanamex.png create mode 100644 addons/payment_mercado_pago/README.md create mode 100644 addons/payment_mercado_pago/__init__.py create mode 100644 addons/payment_mercado_pago/__manifest__.py create mode 100644 addons/payment_mercado_pago/const.py create mode 100644 addons/payment_mercado_pago/controllers/__init__.py create mode 100644 addons/payment_mercado_pago/controllers/main.py create mode 100644 addons/payment_mercado_pago/data/payment_acquirer_data.xml create mode 100644 addons/payment_mercado_pago/models/__init__.py create mode 100644 addons/payment_mercado_pago/models/account_payment_method.py create mode 100644 addons/payment_mercado_pago/models/payment_acquirer.py create mode 100644 addons/payment_mercado_pago/models/payment_transaction.py create mode 100644 addons/payment_mercado_pago/static/description/icon.png create mode 100644 addons/payment_mercado_pago/static/description/icon.svg create mode 100644 addons/payment_mercado_pago/tests/__init__.py create mode 100644 addons/payment_mercado_pago/tests/common.py create mode 100644 addons/payment_mercado_pago/tests/test_payment_acquirer.py create mode 100644 addons/payment_mercado_pago/tests/test_payment_transaction.py create mode 100644 addons/payment_mercado_pago/tests/test_processing_flows.py create mode 100644 addons/payment_mercado_pago/views/payment_mercado_pago_templates.xml create mode 100644 addons/payment_mercado_pago/views/payment_views.xml 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 0000000000000000000000000000000000000000..5f455b6b3bbf923b6c3cd7e9c22e21bd3c332f0a GIT binary patch literal 6417 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 O8BJ9Yq);Vi8UBA%FZdh) literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/citibanamex.png b/addons/payment/static/img/citibanamex.png new file mode 100644 index 0000000000000000000000000000000000000000..166a4f8d3075a2287bf9c2ba0b07059eceaf5471 GIT binary patch literal 2497 zcma)7c{tSD8~+Yt88nI_`@W2Qq>^>ajA3k9CNW7wwk(r9TrMVSsiaW0m`hO_>$UH? z71u3lC~=XHB|BNt{WdPYdb+>++~+yxJ)iTwpZD{g_nbdYyt$dd9yWe9008#j4Y8Ks zdKR20Rz}d$L@XiT0wWpP`T>v&2ft3pbTgF|02l#t6N0`0$pp}k2FDgi!$q$$Fl}$Y zvpDj1TRQ*0co?x!|4cKw@~xlY2ih0#F#os8zZ~WtL%Ysh@U`C`#!mFRW_O(WI{^1% z;*JNq1L)#VY~&9mz9W1aorki5E$b}a$U8Ving;4)x&qsvv+YC|r*$p9gHD=#GG~q56)A&Xl+Z8?yj5U`Pl=S2s z?qVIa0{|vRJXVij(>i1AO0dH3-{L|KhQiz>#ZX*LcUc(^i8glN6V_gup6^XnIz_%yqnO&UQrYNuyPA_z zr@MOU{n5d&)wiLK0JfOBHoEO9YHkT1kwD}?PK}UfN4Q&;`g%)(;+<5_$Re|exJdS9 zr{h9x`M<6}if%c6_NEQf&Qp(5erkJITd`qBqPc7=ZF-y<&oT{?R(Bvpi>psNYL(4a z2#vPhh!n|eDY_~jjQBZQPq%=t#}X^%{`2#u4$sFdh-V|tX~7?+6_m4Jx0)M*4VIBz z{asmWtt)}!TPzuZ&mEo#P^7fL}$D#xo?W*JX?{hC7GWj zG$4JGdx|>A8|x+M(kbOd(YsRv&=_s}mt^`A&Uo<#DxMylyOlz*<{!$VK5j8l-P<57 zQ1NPbnoVQ01-;_|GoPj9Fl4ylX8m%${afH_cyN1_strR*1Jo4 zNY@rQ9-n0KCGc1up9D^BTn@Pa$gMh5G0VZ2j8p=9B?w$6C5K(uH{AH zF|B?S=8mVlp9WVJFZnfPXJ*bm@@;B%u!Gbf#N#gi@}$zLs(}9v&K<4J#_ntVtTM>W zs~1M@NHUbchn2aQ9D>GhT<-8-{5>ZQGUL&?yR2pb%k*b&UZ%a z9$6{5@1(Sj6XjiFN^LK690?1b>o*SNQ9j*WUD4QWoTSL!1U-E1#GzL{z6e!cLfInZ z&xQ2|Di$cJh@_OIqsi5~aBhcZ4yZ$#mNE;y=&@P$jFQ)$@>vayjgx`3k9quiH!I*v za&-|nL@oARAFVz1QJ!+(-5j@KOsf)i;DsDpzPl5{DcXo8J_g8n57cImq zp+IiV#f&`LHfRic>y-y6+(q=$;%d|-;$d{yrRQBY7TV2a1?ATqL}nL3) zBL;Iuy{@4L4DvBrJ^K~R?%`>3b)`MaGalK2Ym`s(_#n}HPkuX;`y?CfnLevssu5vT zu7fZb(9yoDZuw!TK^8l8A0MKdBK4_&Z&Y8ZI$hsNly?y`R6#Aa_mYFdAfq#EvxZlj zZ1=s;xebep%hwu@nI9Q?VK02c`cb+>wZNr1g*gST2o<<&XPy7KIGz%|`Af*=veBBS z`3v_gvV0lqi1*-rmm{c1q@A$h(L$q7^Xo7PwRF}v', 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 0000000000000000000000000000000000000000..b5a9b4b4dc1925dce9142b8f81de98ad35e9a283 GIT binary patch literal 16443 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 Xm2_BqIxGMH002ovPDHLkV1fexf(W{s literal 0 HcmV?d00001 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 + + + + + + + + + + +