diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/payment/controllers/portal.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/payment/controllers/portal.py')
| -rw-r--r-- | addons/payment/controllers/portal.py | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py new file mode 100644 index 00000000..a0140390 --- /dev/null +++ b/addons/payment/controllers/portal.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hashlib +import hmac +import logging +from unicodedata import normalize +import psycopg2 +import werkzeug + +from odoo import http, _ +from odoo.http import request +from odoo.osv import expression +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, consteq, ustr +from odoo.tools.float_utils import float_repr +from datetime import datetime, timedelta + + +_logger = logging.getLogger(__name__) + +class PaymentProcessing(http.Controller): + + @staticmethod + def remove_payment_transaction(transactions): + tx_ids_list = request.session.get("__payment_tx_ids__", []) + if transactions: + for tx in transactions: + if tx.id in tx_ids_list: + tx_ids_list.remove(tx.id) + else: + return False + request.session["__payment_tx_ids__"] = tx_ids_list + return True + + @staticmethod + def add_payment_transaction(transactions): + if not transactions: + return False + tx_ids_list = set(request.session.get("__payment_tx_ids__", [])) | set(transactions.ids) + request.session["__payment_tx_ids__"] = list(tx_ids_list) + return True + + @staticmethod + def get_payment_transaction_ids(): + # return the ids and not the recordset, since we might need to + # sudo the browse to access all the record + # I prefer to let the controller chose when to access to payment.transaction using sudo + return request.session.get("__payment_tx_ids__", []) + + @http.route(['/payment/process'], type="http", auth="public", website=True, sitemap=False) + def payment_status_page(self, **kwargs): + # When the customer is redirect to this website page, + # we retrieve the payment transaction list from his session + tx_ids_list = self.get_payment_transaction_ids() + payment_transaction_ids = request.env['payment.transaction'].sudo().browse(tx_ids_list).exists() + + render_ctx = { + 'payment_tx_ids': payment_transaction_ids.ids, + } + return request.render("payment.payment_process_page", render_ctx) + + @http.route(['/payment/process/poll'], type="json", auth="public") + def payment_status_poll(self): + # retrieve the transactions + tx_ids_list = self.get_payment_transaction_ids() + + payment_transaction_ids = request.env['payment.transaction'].sudo().search([ + ('id', 'in', list(tx_ids_list)), + ('date', '>=', (datetime.now() - timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), + ]) + if not payment_transaction_ids: + return { + 'success': False, + 'error': 'no_tx_found', + } + + processed_tx = payment_transaction_ids.filtered('is_processed') + self.remove_payment_transaction(processed_tx) + + # create the returned dictionnary + result = { + 'success': True, + 'transactions': [], + } + # populate the returned dictionnary with the transactions data + for tx in payment_transaction_ids: + message_to_display = tx.acquirer_id[tx.state + '_msg'] if tx.state in ['done', 'pending', 'cancel'] else None + tx_info = { + 'reference': tx.reference, + 'state': tx.state, + 'return_url': tx.return_url, + 'is_processed': tx.is_processed, + 'state_message': tx.state_message, + 'message_to_display': message_to_display, + 'amount': tx.amount, + 'currency': tx.currency_id.name, + 'acquirer_provider': tx.acquirer_id.provider, + } + tx_info.update(tx._get_processing_info()) + result['transactions'].append(tx_info) + + tx_to_process = payment_transaction_ids.filtered(lambda x: x.state == 'done' and x.is_processed is False) + try: + tx_to_process._post_process_after_done() + except psycopg2.OperationalError as e: + request.env.cr.rollback() + result['success'] = False + result['error'] = "tx_process_retry" + except Exception as e: + request.env.cr.rollback() + result['success'] = False + result['error'] = str(e) + _logger.exception("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e)) + + return result + +class WebsitePayment(http.Controller): + @staticmethod + def _get_acquirers_compatible_with_current_user(acquirers): + # s2s mode will always generate a token, which we don't want for public users + valid_flows = ['form'] if request.env.user._is_public() else ['form', 's2s'] + return [acq for acq in acquirers if acq.payment_flow in valid_flows] + + @http.route(['/my/payment_method'], type='http', auth="user", website=True) + def payment_method(self, **kwargs): + acquirers = list(request.env['payment.acquirer'].search([ + ('state', 'in', ['enabled', 'test']), ('registration_view_template_id', '!=', False), + ('payment_flow', '=', 's2s'), ('company_id', '=', request.env.company.id) + ])) + partner = request.env.user.partner_id + payment_tokens = partner.payment_token_ids + payment_tokens |= partner.commercial_partner_id.sudo().payment_token_ids + return_url = request.params.get('redirect', '/my/payment_method') + values = { + 'pms': payment_tokens, + 'acquirers': acquirers, + 'error_message': [kwargs['error']] if kwargs.get('error') else False, + 'return_url': return_url, + 'bootstrap_formatting': True, + 'partner_id': partner.id + } + return request.render("payment.pay_methods", values) + + @http.route(['/website_payment/pay'], type='http', auth='public', website=True, sitemap=False) + def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, partner_id=False, access_token=None, **kw): + """ + Generic payment page allowing public and logged in users to pay an arbitrary amount. + + In the case of a public user access, we need to ensure that the payment is made anonymously - e.g. it should not be + possible to pay for a specific partner simply by setting the partner_id GET param to a random id. In the case where + a partner_id is set, we do an access_token check based on the payment.link.wizard model (since links for specific + partners should be created from there and there only). Also noteworthy is the filtering of s2s payment methods - + we don't want to create payment tokens for public users. + + In the case of a logged in user, then we let access rights and security rules do their job. + """ + env = request.env + user = env.user.sudo() + reference = normalize('NFKD', reference).encode('ascii','ignore').decode('utf-8') + if partner_id and not access_token: + raise werkzeug.exceptions.NotFound + if partner_id and access_token: + token_ok = request.env['payment.link.wizard'].check_token(access_token, int(partner_id), float(amount), int(currency_id)) + if not token_ok: + raise werkzeug.exceptions.NotFound + + invoice_id = kw.get('invoice_id') + + # Default values + values = { + 'amount': 0.0, + 'currency': user.company_id.currency_id, + } + + # Check sale order + if order_id: + try: + order_id = int(order_id) + if partner_id: + # `sudo` needed if the user is not connected. + # A public user woudn't be able to read the sale order. + # With `partner_id`, an access_token should be validated, preventing a data breach. + order = env['sale.order'].sudo().browse(order_id) + else: + order = env['sale.order'].browse(order_id) + values.update({ + 'currency': order.currency_id, + 'amount': order.amount_total, + 'order_id': order_id + }) + except: + order_id = None + + if invoice_id: + try: + values['invoice_id'] = int(invoice_id) + except ValueError: + invoice_id = None + + # Check currency + if currency_id: + try: + currency_id = int(currency_id) + values['currency'] = env['res.currency'].browse(currency_id) + except: + pass + + # Check amount + if amount: + try: + amount = float(amount) + values['amount'] = amount + except: + pass + + # Check reference + reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {} + values['reference'] = env['payment.transaction']._compute_reference(values=reference_values, prefix=reference) + + # Check acquirer + acquirers = None + if order_id and order: + cid = order.company_id.id + elif kw.get('company_id'): + try: + cid = int(kw.get('company_id')) + except: + cid = user.company_id.id + else: + cid = user.company_id.id + + # Check partner + if not user._is_public(): + # NOTE: this means that if the partner was set in the GET param, it gets overwritten here + # This is something we want, since security rules are based on the partner - assuming the + # access_token checked out at the start, this should have no impact on the payment itself + # existing besides making reconciliation possibly more difficult (if the payment partner is + # not the same as the invoice partner, for example) + partner_id = user.partner_id.id + elif partner_id: + partner_id = int(partner_id) + + values.update({ + 'partner_id': partner_id, + 'bootstrap_formatting': True, + 'error_msg': kw.get('error_msg') + }) + + acquirer_domain = ['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', cid)] + if partner_id: + partner = request.env['res.partner'].browse([partner_id]) + acquirer_domain = expression.AND([ + acquirer_domain, + ['|', ('country_ids', '=', False), ('country_ids', 'in', [partner.sudo().country_id.id])] + ]) + if acquirer_id: + acquirers = env['payment.acquirer'].browse(int(acquirer_id)) + if order_id: + acquirers = env['payment.acquirer'].search(acquirer_domain) + if not acquirers: + acquirers = env['payment.acquirer'].search(acquirer_domain) + + values['acquirers'] = self._get_acquirers_compatible_with_current_user(acquirers) + if partner_id: + values['pms'] = request.env['payment.token'].search([ + ('acquirer_id', 'in', acquirers.ids), + ('partner_id', 'child_of', partner.commercial_partner_id.id) + ]) + else: + values['pms'] = [] + + + return request.render('payment.pay', values) + + @http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>', + '/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>', + '/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>/<int:partner_id>'], type='json', auth='public') + def transaction(self, acquirer_id, reference, amount, currency_id, partner_id=False, **kwargs): + acquirer = request.env['payment.acquirer'].browse(acquirer_id) + order_id = kwargs.get('order_id') + invoice_id = kwargs.get('invoice_id') + + reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {} + reference = request.env['payment.transaction']._compute_reference(values=reference_values, prefix=reference) + + values = { + 'acquirer_id': int(acquirer_id), + 'reference': reference, + 'amount': float(amount), + 'currency_id': int(currency_id), + 'partner_id': partner_id, + 'type': 'form_save' if acquirer.save_token != 'none' and partner_id else 'form', + } + + if order_id: + values['sale_order_ids'] = [(6, 0, [order_id])] + elif invoice_id: + values['invoice_ids'] = [(6, 0, [invoice_id])] + + reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {} + reference_values.update(acquirer_id=int(acquirer_id)) + values['reference'] = request.env['payment.transaction']._compute_reference(values=reference_values, prefix=reference) + tx = request.env['payment.transaction'].sudo().with_context(lang=None).create(values) + secret = request.env['ir.config_parameter'].sudo().get_param('database.secret') + token_str = '%s%s%s' % (tx.id, tx.reference, float_repr(tx.amount, precision_digits=tx.currency_id.decimal_places)) + token = hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest() + tx.return_url = '/website_payment/confirm?tx_id=%d&access_token=%s' % (tx.id, token) + + PaymentProcessing.add_payment_transaction(tx) + + render_values = { + 'partner_id': partner_id, + 'type': tx.type, + } + + return acquirer.sudo().render(tx.reference, float(amount), int(currency_id), values=render_values) + + @http.route(['/website_payment/token/<string:reference>/<string:amount>/<string:currency_id>', + '/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>', + '/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>/<int:partner_id>'], type='http', auth='public', website=True) + def payment_token(self, pm_id, reference, amount, currency_id, partner_id=False, return_url=None, **kwargs): + token = request.env['payment.token'].browse(int(pm_id)) + order_id = kwargs.get('order_id') + invoice_id = kwargs.get('invoice_id') + + if not token: + return request.redirect('/website_payment/pay?error_msg=%s' % _('Cannot setup the payment.')) + + values = { + 'acquirer_id': token.acquirer_id.id, + 'reference': reference, + 'amount': float(amount), + 'currency_id': int(currency_id), + 'partner_id': int(partner_id), + 'payment_token_id': int(pm_id), + 'type': 'server2server', + 'return_url': return_url, + } + + if order_id: + values['sale_order_ids'] = [(6, 0, [int(order_id)])] + if invoice_id: + values['invoice_ids'] = [(6, 0, [int(invoice_id)])] + + tx = request.env['payment.transaction'].sudo().with_context(lang=None).create(values) + PaymentProcessing.add_payment_transaction(tx) + + try: + tx.s2s_do_transaction() + secret = request.env['ir.config_parameter'].sudo().get_param('database.secret') + token_str = '%s%s%s' % (tx.id, tx.reference, float_repr(tx.amount, precision_digits=tx.currency_id.decimal_places)) + token = hmac.new(secret.encode('utf-8'), token_str.encode('utf-8'), hashlib.sha256).hexdigest() + tx.return_url = return_url or '/website_payment/confirm?tx_id=%d&access_token=%s' % (tx.id, token) + except Exception as e: + _logger.exception(e) + return request.redirect('/payment/process') + + @http.route(['/website_payment/confirm'], type='http', auth='public', website=True, sitemap=False) + def confirm(self, **kw): + tx_id = int(kw.get('tx_id', 0)) + access_token = kw.get('access_token') + if tx_id: + if access_token: + tx = request.env['payment.transaction'].sudo().browse(tx_id) + secret = request.env['ir.config_parameter'].sudo().get_param('database.secret') + valid_token_str = '%s%s%s' % (tx.id, tx.reference, float_repr(tx.amount, precision_digits=tx.currency_id.decimal_places)) + valid_token = hmac.new(secret.encode('utf-8'), valid_token_str.encode('utf-8'), hashlib.sha256).hexdigest() + if not consteq(ustr(valid_token), access_token): + raise werkzeug.exceptions.NotFound + else: + tx = request.env['payment.transaction'].browse(tx_id) + if tx.state in ['done', 'authorized']: + status = 'success' + message = tx.acquirer_id.done_msg + elif tx.state == 'pending': + status = 'warning' + message = tx.acquirer_id.pending_msg + else: + status = 'danger' + message = tx.state_message or _('An error occured during the processing of this payment') + PaymentProcessing.remove_payment_transaction(tx) + return request.render('payment.confirm', {'tx': tx, 'status': status, 'message': message}) + else: + return request.redirect('/my/home') |
