From f3d7a97f698046cc185ddd25d808f9d8b850d611 Mon Sep 17 00:00:00 2001 From: nni-odoo Date: Mon, 27 Mar 2023 18:45:44 +0800 Subject: [PATCH] [ADD] payment_xendit: new payment provider for Indonesia and Philippines See the README.md file for more details. 2946329 closes odoo/odoo#141661 Signed-off-by: Antoine Vandevenne (anv) --- .tx/config | 9 + addons/payment/data/payment_method_data.xml | 173 +++++++++++++++++- addons/payment/data/payment_provider_data.xml | 40 +++- addons/payment/static/img/bank_bca.png | Bin 0 -> 8327 bytes addons/payment/static/img/bank_permata.png | Bin 0 -> 5406 bytes addons/payment/static/img/bni.png | Bin 0 -> 3900 bytes addons/payment/static/img/bri.png | Bin 0 -> 7299 bytes addons/payment/static/img/bsi.png | Bin 0 -> 2959 bytes addons/payment/static/img/jeniuspay.png | Bin 0 -> 5772 bytes addons/payment/static/img/shopeepay.png | Bin 0 -> 3488 bytes addons/payment_xendit/README.md | 34 ++++ addons/payment_xendit/__init__.py | 14 ++ addons/payment_xendit/__manifest__.py | 19 ++ addons/payment_xendit/const.py | 38 ++++ addons/payment_xendit/controllers/__init__.py | 3 + addons/payment_xendit/controllers/main.py | 60 ++++++ .../data/payment_provider_data.xml | 9 + addons/payment_xendit/i18n/payment_xendit.pot | 90 +++++++++ addons/payment_xendit/models/__init__.py | 4 + .../payment_xendit/models/payment_provider.py | 79 ++++++++ .../models/payment_transaction.py | 150 +++++++++++++++ .../static/description/icon.png | Bin 0 -> 3296 bytes .../static/description/icon.svg | 1 + addons/payment_xendit/tests/__init__.py | 6 + addons/payment_xendit/tests/common.py | 34 ++++ .../tests/test_payment_provider.py | 16 ++ .../tests/test_payment_transaction.py | 69 +++++++ .../tests/test_processing_flows.py | 75 ++++++++ .../views/payment_provider_views.xml | 26 +++ .../views/payment_xendit_templates.xml | 8 + 30 files changed, 952 insertions(+), 5 deletions(-) create mode 100644 addons/payment/static/img/bank_bca.png create mode 100644 addons/payment/static/img/bank_permata.png create mode 100644 addons/payment/static/img/bni.png create mode 100644 addons/payment/static/img/bri.png create mode 100644 addons/payment/static/img/bsi.png create mode 100644 addons/payment/static/img/jeniuspay.png create mode 100644 addons/payment/static/img/shopeepay.png create mode 100644 addons/payment_xendit/README.md create mode 100644 addons/payment_xendit/__init__.py create mode 100644 addons/payment_xendit/__manifest__.py create mode 100644 addons/payment_xendit/const.py create mode 100644 addons/payment_xendit/controllers/__init__.py create mode 100644 addons/payment_xendit/controllers/main.py create mode 100644 addons/payment_xendit/data/payment_provider_data.xml create mode 100644 addons/payment_xendit/i18n/payment_xendit.pot create mode 100644 addons/payment_xendit/models/__init__.py create mode 100644 addons/payment_xendit/models/payment_provider.py create mode 100644 addons/payment_xendit/models/payment_transaction.py create mode 100644 addons/payment_xendit/static/description/icon.png create mode 100644 addons/payment_xendit/static/description/icon.svg create mode 100644 addons/payment_xendit/tests/__init__.py create mode 100644 addons/payment_xendit/tests/common.py create mode 100644 addons/payment_xendit/tests/test_payment_provider.py create mode 100644 addons/payment_xendit/tests/test_payment_transaction.py create mode 100644 addons/payment_xendit/tests/test_processing_flows.py create mode 100644 addons/payment_xendit/views/payment_provider_views.xml create mode 100644 addons/payment_xendit/views/payment_xendit_templates.xml diff --git a/.tx/config b/.tx/config index 0d7c1745f8d..ea2491f247e 100644 --- a/.tx/config +++ b/.tx/config @@ -1288,6 +1288,15 @@ resource_name = payment_stripe replace_edited_strings = false keep_translations = false +[o:odoo:p:odoo-17:r:payment_xendit] +file_filter = addons/payment_xendit/i18n/.po +source_file = addons/payment_xendit/i18n/payment_xendit.pot +type = PO +minimum_perc = 0 +resource_name = payment_xendit +replace_edited_strings = false +keep_translations = false + [o:odoo:p:odoo-17:r:phone_validation] file_filter = addons/phone_validation/i18n/.po source_file = addons/phone_validation/i18n/phone_validation.pot diff --git a/addons/payment/data/payment_method_data.xml b/addons/payment/data/payment_method_data.xml index 7610bec608b..ffc9817a4a4 100644 --- a/addons/payment/data/payment_method_data.xml +++ b/addons/payment/data/payment_method_data.xml @@ -3,6 +3,26 @@ + + 7Eleven + 7eleven + 1000 + False + False + False + partial + + + + ACH Direct Debit ach_direct_debit @@ -396,6 +416,27 @@ + + BCA + bank_bca + 1000 + False + + False + False + + + + + Bank of Ayudhya bank_of_ayudhya @@ -417,6 +458,27 @@ /> + + Bank Permata + bank_permata + 1000 + False + + False + False + + + + + Bank reference bank_reference @@ -619,7 +681,7 @@ bni 1000 False - + False False @@ -719,12 +781,33 @@ /> + + BRI + bri + 1000 + False + + False + False + + + + + Bank Syariah Indonesia bsi 1000 False - + False False @@ -772,6 +855,46 @@ /> + + Cashalo + cashalo + 1000 + False + False + False + partial + + + + + + Cebuana + cebuana + 1000 + False + False + False + partial + + + + Clearpay clearpay @@ -1121,12 +1244,14 @@ eval="[Command.set([ ref('base.my'), ref('base.sg'), + ref('base.ph'), ])]" /> @@ -1269,6 +1394,27 @@ /> + + JeniusPay + jeniuspay + 1000 + False + + False + False + partial + + + + Jkopay jkopay @@ -2782,6 +2928,29 @@ /> + + ShopeePay + shopeepay + 1000 + False + + False + False + partial + + + + Sofort sofort diff --git a/addons/payment/data/payment_provider_data.xml b/addons/payment/data/payment_provider_data.xml index 4d128dd3444..430daaf70ce 100644 --- a/addons/payment/data/payment_provider_data.xml +++ b/addons/payment/data/payment_provider_data.xml @@ -391,10 +391,44 @@ Wire Transfer 30 - + + + Xendit + + + + + + diff --git a/addons/payment/static/img/bank_bca.png b/addons/payment/static/img/bank_bca.png new file mode 100644 index 0000000000000000000000000000000000000000..a3b0ce343670f1158216beae3794365c749030aa GIT binary patch literal 8327 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

(nqx1kG8AwiI~^4R}0 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 literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/bank_permata.png b/addons/payment/static/img/bank_permata.png new file mode 100644 index 0000000000000000000000000000000000000000..07a1a1c62975ef5398bffaab10a09f2c10ea3e15 GIT binary patch literal 5406 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 literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/bsi.png b/addons/payment/static/img/bsi.png new file mode 100644 index 0000000000000000000000000000000000000000..d7f215c3de18c6c10b77fa48b6c68200dfde10be GIT binary patch literal 2959 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 literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/jeniuspay.png b/addons/payment/static/img/jeniuspay.png new file mode 100644 index 0000000000000000000000000000000000000000..d3977a4f2dc92a733731afd0734cf7e7428f71a7 GIT binary patch literal 5772 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�PNv6nS|`z`>S8nuc4SVk))UBp$|%pk+1sv1T5tPC zNBie2T8s=ng?c`5@0BdkHBt+86Tkk<=i`nIY8RgppLmJ#(h^+&lJ+Cd4^y1p4jMszg0#3OqIR@PDMm(EC7Eze_ z1FF!ssE2I6WPfPwn(*BqOR5$S;f!{yg=Sfj#+vJDruD7njT^S=usw9?w+@?c&G|;} zSDaS|t)wdZ(hSN2ze)%!d>Mywo@Wo+W4ccWu*~0Q&!25`Ai{jb(X!Hv?z|k01%?dN zWGO-{l_6ZD#p5O~1~L&uWX$c#tY1w?SBR6{8K@&(!_QCS3G!Pi>##a(583FpS?Yma zh+wbyX#nwM4dmNzdFtJ-Uk%w&3>M4MTEM#1=E}{e;5Fr-X}!6Hb$e4KXXXU~J+9+5 zB)=E{GRvsDy0qVi{}hN}C>%2;PJ^W9#-{M3u;Y|BTr73Xu=9A4+#=otj5~$hjT2o3 zgBOa>$H30CJw;Q`ilVDUDQ0}By#%ph7sMyI^CE7Gt_WIKu` zOd9&u{xgN!AwSgq*ZFAoQs)HJZm(o1%a*wC2nE^or9@E=JvIVizSnfdzJY&7gIa>{6n6S zwhk8eB=I;%uV0UhTM{o>&dZ!U37nL;{gZOur}6x#m?MY0xC<&(@<(D7+kb+5lvwqo zf1MHQ_!6%#rYc4j_bemWph-W!@cB+*tB|E8w?2~zn(HvUHWv(Tjt8$8of+u+yYnhcQP#CrjN9qM- z&UR&fc)Mj~534EGo!lvn_9UYZ8~1iK@xo_&D^0U2BA6X&|1D$(hddQA~KdMIxRZD6FgE%%3)5< zEcU#~&J;aIa;BV6XK1b{51`P14jcT~dT5KWL`nadu+O-M7yI31IcEbp|+Q0P*x&EKAyZ}Tr2TOG{`>q$ou_NuG)wXzjrN`6>)}NGX z_pNB_J_^D$?+TdrdTF?unYhTD^gp^Y4PLPs<^Xbf$MW_sv3$x=9XvfLF?V9(u%q&Z zKM(^CjIIfFaEn)&5%-DK^y!5iaqd^4gwZK!~?PZL+K zp_mp9QN^x^otivN=W={CqNywCqRQAiMPgKK=Ak_!avd8lYkntTAj`HYfqr#s;U9Y^ zoFJ|Oc*Q{a@Sf-y8vF{vzU7uz#uL~xeev4nHf+>ge|S_SkfNzw3I58lbr3Y`O$0Wb zhCG!-UX<$x(&f}`;JpCvM~|FG(yYK}QoW?MjksO)s<$EqlblILr;#M6EG{@HNSyRF zf`ii|%FI|@)L)SN(`KlZx|55hT}*e{aWMk$$-Oa!I0_tdY48i!?e_m>O#iok&ABDe YuR7O}96Z{%lgD^EnuZz(5IE|80E_PCoB#j- literal 0 HcmV?d00001 diff --git a/addons/payment/static/img/shopeepay.png b/addons/payment/static/img/shopeepay.png new file mode 100644 index 0000000000000000000000000000000000000000..0e4c6bd5e058635917e651af8c5dbd2c24687c7f GIT binary patch literal 3488 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 literal 0 HcmV?d00001 diff --git a/addons/payment_xendit/README.md b/addons/payment_xendit/README.md new file mode 100644 index 00000000000..7ce60571f04 --- /dev/null +++ b/addons/payment_xendit/README.md @@ -0,0 +1,34 @@ +# Xendit + +## Implementation details + +### Supported features + +- [Payment with redirection flow](https://developers.xendit.co/api-reference/#create-invoice) +- Several payment methods such as credit cards, bank transfers, eWallets, pay later, and + [others](https://docs.xendit.co/payment-link/payment-channels). +- [Webhook](https://developers.xendit.co/api-reference/#invoice-callback). + +In addition, Xendit also allows to implement tokenization. + +### API and gateway + +We choose to integrate with the +[Invoices API](https://developer.flutterwave.com/docs/collecting-payments/standard/) as it +is the gateway that covers the best our needs: it is a payment with redirection flow and is +compatible with `payment`'s form-based implementation of that flow. + +The [Payments API](https://developers.xendit.co/api-reference/#payments-api) was considered too, but +it requires implementing a more complex direct payment flow and linking the customer account. + +The version of the API implemented by this module is v2. + +## Merge details + +The first version of the module was specified in task +[2946329](https://www.odoo.com/web#id=2759117&model=project.task) and merged with PR +odoo/odoo#141661 in branch `17.0`. + +## Testing instructions + +https://developers.xendit.co/api-reference/#test-scenarios \ No newline at end of file diff --git a/addons/payment_xendit/__init__.py b/addons/payment_xendit/__init__.py new file mode 100644 index 00000000000..9fa28e8543f --- /dev/null +++ b/addons/payment_xendit/__init__.py @@ -0,0 +1,14 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models + +from odoo.addons.payment import setup_provider, reset_payment_provider + + +def post_init_hook(env): + setup_provider(env, 'xendit') + + +def uninstall_hook(env): + reset_payment_provider(env, 'xendit') diff --git a/addons/payment_xendit/__manifest__.py b/addons/payment_xendit/__manifest__.py new file mode 100644 index 00000000000..b7b259560ce --- /dev/null +++ b/addons/payment_xendit/__manifest__.py @@ -0,0 +1,19 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Payment Provider: Xendit", + 'version': '1.0', + 'category': 'Accounting/Payment Providers', + 'sequence': 350, + 'summary': "A payment provider for Indonesian and the Philippines.", + 'depends': ['payment'], + 'data': [ + 'views/payment_provider_views.xml', + 'views/payment_xendit_templates.xml', + + 'data/payment_provider_data.xml', # Depends on payment_xendit_templates.xml + ], + 'post_init_hook': 'post_init_hook', + 'uninstall_hook': 'uninstall_hook', + 'license': 'LGPL-3', +} diff --git a/addons/payment_xendit/const.py b/addons/payment_xendit/const.py new file mode 100644 index 00000000000..db7446b8197 --- /dev/null +++ b/addons/payment_xendit/const.py @@ -0,0 +1,38 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# The currencies supported by Xendit, in ISO 4217 format. +SUPPORTED_CURRENCIES = [ + 'IDR', + 'PHP', +] + +# The codes of the payment methods to activate when Xendit is activated. +DEFAULT_PAYMENT_METHODS_CODES = [ + # Primary payment methods. + 'card', + 'dana', + 'ovo', + 'qris', + + # Brand payment methods. + 'visa', + 'mastercard', +] + +# Mapping of payment code to channel code according to Xendit API +PAYMENT_METHODS_MAPPING = { + 'bank_bca': 'BCA', + 'bank_permata': 'PERMATA', + 'bpi': 'DD_BPI', + 'card': 'CREDIT_CARD', + 'maya': 'PAYMAYA', +} + +# Mapping of transaction states to Xendit payment statuses. +PAYMENT_STATUS_MAPPING = { + 'draft': (), + 'pending': ('PENDING'), + 'done': ('SUCCEEDED', 'PAID'), + 'cancel': ('CANCELLED', 'EXPIRED'), + 'error': ('FAILED',) +} diff --git a/addons/payment_xendit/controllers/__init__.py b/addons/payment_xendit/controllers/__init__.py new file mode 100644 index 00000000000..80ee4da1c5e --- /dev/null +++ b/addons/payment_xendit/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_xendit/controllers/main.py b/addons/payment_xendit/controllers/main.py new file mode 100644 index 00000000000..a45d8612b62 --- /dev/null +++ b/addons/payment_xendit/controllers/main.py @@ -0,0 +1,60 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import pprint + +from werkzeug.exceptions import Forbidden + +from odoo import http +from odoo.exceptions import ValidationError +from odoo.http import request +from odoo.tools import consteq + + +_logger = logging.getLogger(__name__) + + +class XenditController(http.Controller): + + _webhook_url = '/payment/xendit/webhook' + + @http.route(_webhook_url, type='http', methods=['POST'], auth='public', csrf=False) + def xendit_webhook(self): + """ Process the notification data sent by Xendit to the webhook. + + :return: The 'accepted' string to acknowledge the notification. + """ + data = request.get_json_data() + _logger.info("Notification received from Xendit with data:\n%s", pprint.pformat(data)) + + try: + # Check the integrity of the notification. + received_token = request.httprequest.headers.get('x-callback-token') + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( + 'xendit', data + ) + self._verify_notification_token(received_token, tx_sudo) + + # Handle the notification data. + tx_sudo._handle_notification_data('xendit', data) + except ValidationError: + _logger.exception("Unable to handle notification data; skipping to acknowledge.") + + return request.make_json_response(['accepted'], status=200) + + def _verify_notification_token(self, received_token, tx_sudo): + """ Check that the received token matches the saved webhook token. + + :param str received_token: The callback token received with the notification data. + :param payment.transaction tx_sudo: The transaction referenced by the notification data. + :return: None + :raise Forbidden: If the tokens don't match. + """ + # Check for the received token. + if not received_token: + _logger.warning("Received notification with missing token.") + raise Forbidden() + + if not consteq(tx_sudo.provider_id.xendit_webhook_token, received_token): + _logger.warning("Received notification with invalid callback token %r.", received_token) + raise Forbidden() diff --git a/addons/payment_xendit/data/payment_provider_data.xml b/addons/payment_xendit/data/payment_provider_data.xml new file mode 100644 index 00000000000..44286ed3b71 --- /dev/null +++ b/addons/payment_xendit/data/payment_provider_data.xml @@ -0,0 +1,9 @@ + + + + + xendit + + + + diff --git a/addons/payment_xendit/i18n/payment_xendit.pot b/addons/payment_xendit/i18n/payment_xendit.pot new file mode 100644 index 00000000000..9200f11d669 --- /dev/null +++ b/addons/payment_xendit/i18n/payment_xendit.pot @@ -0,0 +1,90 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * payment_xendit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-01-08 13:49+0000\n" +"PO-Revision-Date: 2024-01-08 13:49+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: payment_xendit +#. odoo-python +#: code:addons/payment_xendit/models/payment_transaction.py:0 +#, python-format +msgid "" +"An error occurred during the processing of your payment (status %s). Please " +"try again." +msgstr "" + +#. module: payment_xendit +#: model:ir.model.fields,field_description:payment_xendit.field_payment_provider__code +msgid "Code" +msgstr "" + +#. module: payment_xendit +#. odoo-python +#: code:addons/payment_xendit/models/payment_provider.py:0 +#, python-format +msgid "Could not establish the connection to the API." +msgstr "" + +#. module: payment_xendit +#. odoo-python +#: code:addons/payment_xendit/models/payment_transaction.py:0 +#, python-format +msgid "No transaction found matching reference %s." +msgstr "" + +#. module: payment_xendit +#: model:ir.model,name:payment_xendit.model_payment_provider +msgid "Payment Provider" +msgstr "" + +#. module: payment_xendit +#: model:ir.model,name:payment_xendit.model_payment_transaction +msgid "Payment Transaction" +msgstr "" + +#. module: payment_xendit +#. odoo-python +#: code:addons/payment_xendit/models/payment_transaction.py:0 +#, python-format +msgid "Received data with missing reference." +msgstr "" + +#. module: payment_xendit +#: model:ir.model.fields,field_description:payment_xendit.field_payment_provider__xendit_secret_key +msgid "Secret Key" +msgstr "" + +#. module: payment_xendit +#. odoo-python +#: code:addons/payment_xendit/models/payment_provider.py:0 +#, python-format +msgid "" +"The communication with the API failed. Xendit gave us the following " +"information: '%s'" +msgstr "" + +#. module: payment_xendit +#: model:ir.model.fields,help:payment_xendit.field_payment_provider__code +msgid "The technical code of this payment provider." +msgstr "" + +#. module: payment_xendit +#: model:ir.model.fields,field_description:payment_xendit.field_payment_provider__xendit_webhook_token +msgid "Webhook Token" +msgstr "" + +#. module: payment_xendit +#: model:ir.model.fields.selection,name:payment_xendit.selection__payment_provider__code__xendit +msgid "Xendit" +msgstr "" diff --git a/addons/payment_xendit/models/__init__.py b/addons/payment_xendit/models/__init__.py new file mode 100644 index 00000000000..08dfb8af342 --- /dev/null +++ b/addons/payment_xendit/models/__init__.py @@ -0,0 +1,4 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import payment_provider +from . import payment_transaction diff --git a/addons/payment_xendit/models/payment_provider.py b/addons/payment_xendit/models/payment_provider.py new file mode 100644 index 00000000000..a5d9f226753 --- /dev/null +++ b/addons/payment_xendit/models/payment_provider.py @@ -0,0 +1,79 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import pprint + +import requests + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +from odoo.addons.payment_xendit import const + + +_logger = logging.getLogger(__name__) + + +class PaymentProvider(models.Model): + _inherit = 'payment.provider' + + code = fields.Selection( + selection_add=[('xendit', "Xendit")], ondelete={'xendit': 'set default'} + ) + xendit_secret_key = fields.Char( + string="Xendit Secret Key", groups='base.group_system', required_if_provider='xendit' + ) + xendit_webhook_token = fields.Char( + string="Xendit Webhook Token", groups='base.group_system', required_if_provider='xendit' + ) + + # === BUSINESS METHODS ===# + + def _get_supported_currencies(self): + """ Override of `payment` to return the supported currencies. """ + supported_currencies = super()._get_supported_currencies() + if self.code == 'xendit': + supported_currencies = supported_currencies.filtered( + lambda c: c.name in const.SUPPORTED_CURRENCIES + ) + return supported_currencies + + def _get_default_payment_method_codes(self): + """ Override of `payment` to return the default payment method codes. """ + default_codes = super()._get_default_payment_method_codes() + if self.code != 'xendit': + return default_codes + return const.DEFAULT_PAYMENT_METHODS_CODES + + def _xendit_make_request(self, payload=None): + """ Make a request to Xendit API and return the JSON-formatted content of the response. + + Note: self.ensure_one() + + :param dict payload: The payload of the request. + :return The JSON-formatted content of the response. + :rtype: dict + :raise ValidationError: If an HTTP error occurs. + """ + self.ensure_one() + + auth = (self.xendit_secret_key, '') + url = "https://api.xendit.co/v2/invoices" + try: + response = requests.post(url, json=payload, auth=auth, timeout=10) + response.raise_for_status() + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + _logger.exception("Unable to reach endpoint at %s", url) + raise ValidationError("Xendit: " + _("Could not establish the connection to the API.")) + except requests.exceptions.HTTPError as err: + error_message = err.response.json().get('message') + _logger.exception( + "Invalid API request at %s with data:\n%s", url, pprint.pformat(payload) + ) + raise ValidationError( + "Xendit: " + _( + "The communication with the API failed. Xendit gave us the following" + " information: '%s'", error_message + ) + ) + return response.json() diff --git a/addons/payment_xendit/models/payment_transaction.py b/addons/payment_xendit/models/payment_transaction.py new file mode 100644 index 00000000000..dc3ad40a586 --- /dev/null +++ b/addons/payment_xendit/models/payment_transaction.py @@ -0,0 +1,150 @@ +# 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_xendit import const + + +_logger = logging.getLogger(__name__) + + +class PaymentTransaction(models.Model): + _inherit = 'payment.transaction' + + def _get_specific_rendering_values(self, processing_values): + """ Override of `payment` to return Xendit-specific rendering values. + + Note: self.ensure_one() from `_get_processing_values` + + :param dict processing_values: The generic and specific processing values of the transaction + :return: The dict of provider-specific processing values. + :rtype: dict + """ + res = super()._get_specific_rendering_values(processing_values) + if self.provider_code != 'xendit': + return res + + # Initiate the payment and retrieve the invoice data. + payload = self._xendit_prepare_invoice_request_payload() + _logger.info("Sending invoice request for link creation:\n%s", pprint.pformat(payload)) + invoice_data = self.provider_id._xendit_make_request(payload) + _logger.info("Received invoice request response:\n%s", pprint.pformat(invoice_data)) + + # Extract the payment link URL and embed it in the redirect form. + rendering_values = { + 'api_url': invoice_data.get('invoice_url') + } + return rendering_values + + def _xendit_prepare_invoice_request_payload(self): + """ Create the payload for the invoice request based on the transaction values. + + :return: The request payload. + :rtype: dict + """ + base_url = self.provider_id.get_base_url() + redirect_url = urls.url_join(base_url, '/payment/status') + payload = { + 'external_id': self.reference, + 'amount': self.amount, + 'description': self.reference, + 'customer': { + 'given_names': self.partner_name, + }, + 'success_redirect_url': redirect_url, + 'failure_redirect_url': redirect_url, + 'payment_methods': [const.PAYMENT_METHODS_MAPPING.get( + self.payment_method_code, self.payment_method_code.upper()) + ], + 'currency': self.currency_id.name, + } + # Extra payload values that must not be included if empty. + if self.partner_email: + payload['customer']['email'] = self.partner_id.email + if phone := self.partner_id.mobile or self.partner_id.phone: + payload['customer']['mobile_number'] = phone + address_details = {} + if self.partner_city: + address_details['city'] = self.partner_city + if self.partner_country_id.name: + address_details['country'] = self.partner_country_id.name + if self.partner_zip: + address_details['postal_code'] = self.partner_zip + if self.partner_state_id.name: + address_details['state'] = self.partner_state_id.name + if self.partner_address: + address_details['street_line1'] = self.partner_address + if address_details: + payload['customer']['addresses'] = [address_details] + + return payload + + def _get_tx_from_notification_data(self, provider_code, notification_data): + """ Override of `payment` to find the transaction based on the notification data. + + :param str provider_code: The code of the provider that handled the transaction. + :param dict notification_data: The notification data sent by the provider. + :return: The transaction if found. + :rtype: payment.transaction + :raise ValidationError: If inconsistent data were received. + :raise ValidationError: If the data match no transaction. + """ + tx = super()._get_tx_from_notification_data(provider_code, notification_data) + if provider_code != 'xendit' or len(tx) == 1: + return tx + + reference = notification_data.get('external_id') + if not reference: + raise ValidationError("Xendit: " + _("Received data with missing reference.")) + + tx = self.search([('reference', '=', reference), ('provider_code', '=', 'xendit')]) + if not tx: + raise ValidationError( + "Xendit: " + _("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 Xendit 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. + """ + self.ensure_one() + + super()._process_notification_data(notification_data) + if self.provider_code != 'xendit': + return + + # Update the provider reference. + self.provider_reference = notification_data.get('id') + + # Update payment method. + payment_method_code = notification_data.get('payment_method', '') + payment_method = self.env['payment.method']._get_from_code( + payment_method_code, mapping=const.PAYMENT_METHODS_MAPPING + ) + self.payment_method_id = payment_method or self.payment_method_id + + # Update the payment state. + payment_status = notification_data.get('status') + if payment_status in const.PAYMENT_STATUS_MAPPING['pending']: + self._set_pending() + elif payment_status in const.PAYMENT_STATUS_MAPPING['done']: + self._set_done() + elif payment_status in const.PAYMENT_STATUS_MAPPING['cancel']: + self._set_canceled() + elif payment_status in const.PAYMENT_STATUS_MAPPING['error']: + self._set_error(_( + "An error occurred during the processing of your payment (status %s). Please try " + "again." + )) diff --git a/addons/payment_xendit/static/description/icon.png b/addons/payment_xendit/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..199f849265995aa8e3a500346dda5c1cc76be8d4 GIT binary patch literal 3296 zcmV<63?K7}P)`&z%`NO-qt>c_=Tm>y`)Fs!0rj2PD#|lJpl2af%v6fXJ4ol32B=c#hZo0}=(tp8U?{<5HpSs zy$oHzgDybcfyOutPGI%MFApq&CM!X}a66vUbsA;!JrICO!+4T2xOhCf1ZfXBT9U;<139b5Nk_~h{UAt;#QJ%qs+ zH~<6rE&Q;K4&+rB8q1afPz%*Ro5tqDAt%|*{LmqYeZWA@?|i}Z-Vc5Dk!;s+&(ZVF1H4m%P#{-HbB})X#`=dU z{+3Nnx-LKA47Zo?*J|tIAQLV;Z79pAY1KHMAqv)xmp^UpqSRlZ2Q z!^7j@u;EPq3VH|~i-f))_MLWJ&J3cWS5a3bIx~IWI{_B)B4D`Xdfxt}Kgk2UW1Y*H zK~SiZewhQGE-vSI$136V!Gl)h3ku|nL5(q+tawTCI2h0p z$c1M!cA+QfSm^gCia2fxQ^4L!*=M$fRfABP3z83xjIW>3OM=fQaLzP$4jZ3a+;M(N zd*X?_BMG1?3gp@AFC+mwx%xLA9|VDiLTDm8QTkT=9;<1vHG`<=RTRka%$r>Q_`uA} zGVc4+y#28M&wWmWALAe}=5vx}9N%Dk)jL*+f&?NzAtMQk9njb{n*rXMK~(fA(R}yz z3uh;D0{rCmi&Hb8NvmEZYM1AIjfn%;-SsTtz+P4`VO8g{Vh|O*N@cz~2DYJ#h<&C# z@zhSQ$~&wiPI6`Sxz`54V|)TYnh9FdxjHt8s$Lbx(G#*Zlm=69UHu!4yrC;mAy2Pr z!{os4e=D2zrIH}ImsrxdIyOku6BAAS%Gc^=Gyw&Ides$4 zleT)5DRB^C8XQq#U|r|ZG6-GqByVMQeTFHN=7OPKWr|CkOW7c{^eUS)C#~dZu1K#k zMMvj)^5{k7i<6WLk_IP*G}rk03`2q_p$}S>G*i*5Oi|Lg62?!c26!cd1W}-*SD9R& zVM>oYnbOLK*w!CtWy$n(GihT}$K z8=20fG~FvTh@xIKUUVS0tyh`SGTkdpgH?1csX;K!eE_5+P%KJ%RUHa~p)0EN{@ATA z%GyX@1xKaQG97%Gk#J@$UG8?LHeC}mkFJyX8DBx*yWQgC#+GeO>w1@fw- z>7?sGzRF!6E`uM%{n)MlNYpMeAg~FQ^eEIDfo4Qan&=Maa}uAvKWeRSP!tDiPFGc}Y@jv--n^!$S4qd!T`qL2zVB)fbbJW)+-& z9!*!*u|riM%>@^FRXfN#sH?&;#bFofh4y?%9K}-EnbKhVuOB*uyaV;B8gy5$T0`m` z%`{hbk}R1l6QymCEDe@r3>6xcT1bN_&6OA76&T1V&5h*h1&NMElXOON>XZg^&KD%j zLfoX)ig{2VSJbP}&OqZak1ag!5s;gJ8(kQlaZQnuV zeJGIktoW!`G1Y1tW7(#o%0?Hf7WO^Z^)Ts)(yCWU zyhB;9647z>dmNY9u@;Gkc`T*}zuM%O+AWfYM zg!wI{!8tltRVB^!I8(^#)wYFDiKZ$LPUut zl{V(KK3e&g*DGpr#n!a!z{M#X9I{!_`dohx?n#eH@=_h^RU(`jq=BiROy?qFT-x?n z?(-PPwbNV?ao}^@a=*;Cwmb|sgD;3)g6k}TV1hG)5Gp#?TU#H7zihg%-BZ@7p7<+c#;DAr(O@dUfC|&h3ea_#n4KQu8`a?GBSW^hfCMd4>W1p@VB%AI{ zb*>JCI;GW64REekiRjwhqDklSBM(eJIL&wCj|ZTc30Z^UnQOhOq@<*zq@<*zq@<*z eq@?73g?|IU@G=n=Cuv6j0000 diff --git a/addons/payment_xendit/tests/__init__.py b/addons/payment_xendit/tests/__init__.py new file mode 100644 index 00000000000..4a36db00650 --- /dev/null +++ b/addons/payment_xendit/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_provider +from . import test_payment_transaction +from . import test_processing_flows diff --git a/addons/payment_xendit/tests/common.py b/addons/payment_xendit/tests/common.py new file mode 100644 index 00000000000..244153052b0 --- /dev/null +++ b/addons/payment_xendit/tests/common.py @@ -0,0 +1,34 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.payment.tests.common import PaymentCommon + + +class XenditCommon(PaymentCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.xendit = cls._prepare_provider('xendit', update_values={ + 'xendit_secret_key': 'xnd_secret_key', + 'xendit_webhook_token': 'xnd_webhook_token', + }) + cls.provider = cls.xendit + cls.webhook_notification_data = { + 'amount': 1740, + 'status': 'PAID', + 'created': '2023-07-12T09:31:13.111Z', + 'paid_at': '2023-07-12T09:31:22.830Z', + 'updated': '2023-07-12T09:31:23.577Z', + 'user_id': '64118d86854d7d89206e732d', + 'currency': 'IDR', + 'bank_code': 'BNI', + 'description': cls.reference, + 'external_id': cls.reference, + 'paid_amount': 1740, + 'merchant_name': 'Odoo', + 'initial_amount': 1740, + 'payment_method': 'BANK_TRANSFER', + 'payment_channel': 'BNI', + 'payment_destination': '880891384013', + } diff --git a/addons/payment_xendit/tests/test_payment_provider.py b/addons/payment_xendit/tests/test_payment_provider.py new file mode 100644 index 00000000000..a078715618f --- /dev/null +++ b/addons/payment_xendit/tests/test_payment_provider.py @@ -0,0 +1,16 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged + +from odoo.addons.payment_xendit.tests.common import XenditCommon + + +@tagged('post_install', '-at_install') +class TestPaymentProvider(XenditCommon): + def test_incompatible_with_unsupported_currencies(self): + """ Test that Xendit providers are filtered out from compatible providers when the currency + is not supported. """ + compatible_providers = self.env['payment.provider']._get_compatible_providers( + self.company_id, self.partner.id, self.amount, currency_id=self.env.ref('base.AFN').id + ) + self.assertNotIn(self.xendit, compatible_providers) diff --git a/addons/payment_xendit/tests/test_payment_transaction.py b/addons/payment_xendit/tests/test_payment_transaction.py new file mode 100644 index 00000000000..62d6617bc9d --- /dev/null +++ b/addons/payment_xendit/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_xendit.tests.common import XenditCommon + + +@tagged('post_install', '-at_install') +class TestPaymentTransaction(PaymentHttpCommon, XenditCommon): + + def test_no_item_missing_from_invoice_request_payload(self): + """ Test that the invoice request values are conform to the transaction fields. """ + self.maxDiff = 10000 # Allow comparing large dicts. + tx = self._create_transaction(flow='redirect') + request_payload = tx._xendit_prepare_invoice_request_payload() + return_url = self._build_url('/payment/status') + self.assertDictEqual(request_payload, { + 'external_id': tx.reference, + 'amount': tx.amount, + 'description': tx.reference, + 'customer': { + 'given_names': tx.partner_name, + 'email': tx.partner_email, + 'mobile_number': tx.partner_id.phone, + 'addresses': [{ + 'city': tx.partner_city, + 'country': tx.partner_country_id.name, + 'postal_code': tx.partner_zip, + 'street_line1': tx.partner_address, + }], + }, + 'success_redirect_url': return_url, + 'failure_redirect_url': return_url, + 'payment_methods': [self.payment_method_code.upper()], + 'currency': tx.currency_id.name, + }) + + @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('redirect') + with patch( + 'odoo.addons.payment_xendit.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'], 'get') + self.assertDictEqual(form_info['inputs'], {}) + + def test_get_tx_from_notification_data_returns_tx(self): + """ Test that the transaction is found based on the notification data. """ + tx = self._create_transaction('redirect') + tx_found = self.env['payment.transaction']._get_tx_from_notification_data( + 'xendit', self.webhook_notification_data + ) + self.assertEqual(tx, tx_found) + + 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('redirect') + tx._process_notification_data(self.webhook_notification_data) + self.assertEqual(tx.state, 'done') diff --git a/addons/payment_xendit/tests/test_processing_flows.py b/addons/payment_xendit/tests/test_processing_flows.py new file mode 100644 index 00000000000..f535ba5c518 --- /dev/null +++ b/addons/payment_xendit/tests/test_processing_flows.py @@ -0,0 +1,75 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from werkzeug.exceptions import Forbidden + +from odoo.tests import tagged +from odoo.tools import mute_logger + +from odoo.addons.payment.tests.http_common import PaymentHttpCommon +from odoo.addons.payment_xendit.controllers.main import XenditController +from odoo.addons.payment_xendit.tests.common import XenditCommon + + +@tagged('post_install', '-at_install') +class TestProcessingFlow(XenditCommon, PaymentHttpCommon): + + @mute_logger('odoo.addons.payment_xendit.controllers.main') + def test_webhook_notification_triggers_processing(self): + """ Test that receiving a valid webhook notification and signature verified triggers the + processing of the notification data. """ + self._create_transaction('redirect') + url = self._build_url(XenditController._webhook_url) + with patch( + 'odoo.addons.payment_xendit.controllers.main.XenditController' + '._verify_notification_token' + ), 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) + + @mute_logger('odoo.addons.payment_xendit.controllers.main') + def test_webhook_notification_triggers_signature_check(self): + """ Test that receiving a webhook notification triggers a signature check. """ + self._create_transaction('redirect') + url = self._build_url(XenditController._webhook_url) + with patch( + 'odoo.addons.payment_xendit.controllers.main.XenditController.' + '_verify_notification_token' + ) as signature_check_mock: + self._make_json_request(url, data=self.webhook_notification_data) + self.assertEqual(signature_check_mock.call_count, 1) + + def test_accept_webhook_notification_with_valid_signature(self): + """ Test the verification of a webhook notification with a valid signature. """ + tx = self._create_transaction('redirect') + self._assert_does_not_raise( + Forbidden, + XenditController._verify_notification_token, + XenditController, + self.provider.xendit_webhook_token, + tx, + ) + + @mute_logger('odoo.addons.payment_xendit.controllers.main') + def test_reject_notification_with_missing_signature(self): + """ Test the verification of a notification with a missing signature. """ + tx = self._create_transaction('redirect') + self.assertRaises( + Forbidden, + XenditController._verify_notification_token, + XenditController, + None, + tx, + ) + + @mute_logger('odoo.addons.payment_xendit.controllers.main') + def test_reject_notification_with_invalid_signature(self): + """ Test the verification of a notification with an invalid signature. """ + tx = self._create_transaction('redirect') + self.assertRaises( + Forbidden, XenditController._verify_notification_token, XenditController, 'dummy', tx + ) diff --git a/addons/payment_xendit/views/payment_provider_views.xml b/addons/payment_xendit/views/payment_provider_views.xml new file mode 100644 index 00000000000..3500d362b7a --- /dev/null +++ b/addons/payment_xendit/views/payment_provider_views.xml @@ -0,0 +1,26 @@ + + + + + Xendit Provider Form + payment.provider + + + + + + + + + + + + diff --git a/addons/payment_xendit/views/payment_xendit_templates.xml b/addons/payment_xendit/views/payment_xendit_templates.xml new file mode 100644 index 00000000000..096123751d1 --- /dev/null +++ b/addons/payment_xendit/views/payment_xendit_templates.xml @@ -0,0 +1,8 @@ + + + + + +