From cf52e381bd8a08fcbf390ae11051da1d2957731c Mon Sep 17 00:00:00 2001 From: "Anita (anko)" Date: Thu, 20 Apr 2023 16:26:12 +0200 Subject: [PATCH] [IMP] payment: post-process transactions immediately Instead of monitoring a list of transactions to post-process in the session, we now track only the last processed transaction, thus allowing to drop the 10 min delay of the post-processing cron. task-3125913 Part-of: odoo/odoo#119473 Co-authored-by: mano-odoo --- addons/payment/controllers/portal.py | 7 +- addons/payment/controllers/post_processing.py | 125 ++++------- addons/payment/models/payment_provider.py | 2 +- addons/payment/models/payment_transaction.py | 17 +- .../payment/static/src/js/post_processing.js | 188 +++++++--------- .../static/src/scss/payment_templates.scss | 11 +- .../src/xml/payment_post_processing.xml | 209 ++++++------------ .../views/payment_portal_templates.xml | 90 ++++---- addons/payment/views/payment_templates.xml | 26 ++- .../payment_custom/models/payment_provider.py | 12 +- .../static/src/js/post_processing.js | 35 ++- .../views/payment_custom_templates.xml | 65 +++++- .../controllers/payment_portal.py | 4 - addons/website_sale/controllers/main.py | 6 - 14 files changed, 342 insertions(+), 455 deletions(-) diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index ef79c634d91..d88c6639f5d 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -317,8 +317,8 @@ class PaymentPortal(portal.CustomerPortal): else: tx_sudo._log_sent_message() - # Monitor the transaction to make it available in the portal - PaymentPostProcessing.monitor_transactions(tx_sudo) + # Monitor the transaction to make it available in the portal. + PaymentPostProcessing.monitor_transaction(tx_sudo) return tx_sudo @@ -361,9 +361,6 @@ class PaymentPortal(portal.CustomerPortal): ): raise werkzeug.exceptions.NotFound() # Don't leak information about ids. - # Stop monitoring the transaction now that it reached a final state. - PaymentPostProcessing.remove_transactions(tx_sudo) - # Display the payment confirmation page to the user return request.render('payment.confirm', qcontext={'tx': tx_sudo}) else: diff --git a/addons/payment/controllers/post_processing.py b/addons/payment/controllers/post_processing.py index b004742ca74..5025ef2ff63 100644 --- a/addons/payment/controllers/post_processing.py +++ b/addons/payment/controllers/post_processing.py @@ -1,11 +1,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -from datetime import timedelta import psycopg2 -from odoo import fields, http +from odoo import http from odoo.http import request _logger = logging.getLogger(__name__) @@ -22,7 +21,7 @@ class PaymentPostProcessing(http.Controller): their post-processing. """ - MONITORED_TX_IDS_KEY = '__payment_monitored_tx_ids__' + MONITORED_TX_ID_KEY = '__payment_monitored_tx_id__' @http.route('/payment/status', type='http', auth='public', website=True, sitemap=False) def display_status(self, **kwargs): @@ -36,104 +35,54 @@ class PaymentPostProcessing(http.Controller): @http.route('/payment/status/poll', type='json', auth='public') def poll_status(self, **_kwargs): - """ Fetch the transactions to display on the status page and finalize their post-processing. + """ Fetch the transaction to display on the status page and finalize its post-processing. - :return: The post-processing values of the transactions + :return: The post-processing values of the transaction. :rtype: dict """ - # Retrieve recent user's transactions from the session - limit_date = fields.Datetime.now() - timedelta(days=1) - monitored_txs = request.env['payment.transaction'].sudo().search([ - ('id', 'in', self.get_monitored_transaction_ids()), - ('last_state_change', '>=', limit_date) - ]) - if not monitored_txs: # The transaction was not correctly created - return { - 'success': False, - 'error': 'no_tx_found', - } + # Retrieve the last user's transaction from the session. + monitored_tx = request.env['payment.transaction'].sudo().browse( + self.get_monitored_transaction_id() + ).exists() + if not monitored_tx: # The session might have expired, or the tx has never existed. + raise Exception('tx_not_found') - # Build the list of display values with the display message and post-processing values - display_values_list = [] - for tx in monitored_txs: - display_message = None - if tx.state == 'pending': - display_message = tx.provider_id.pending_msg - elif tx.state == 'done': - display_message = tx.provider_id.done_msg - elif tx.state == 'cancel': - display_message = tx.provider_id.cancel_msg - display_values_list.append({ - 'display_message': display_message, - **tx._get_post_processing_values(), - }) + # Finalize the post-processing of the transaction before redirecting the user to the landing + # route and its document. + if monitored_tx.state == 'done' and not monitored_tx.is_post_processed: + try: + monitored_tx._finalize_post_processing() + except psycopg2.OperationalError: # The database cursor could not be committed. + request.env.cr.rollback() # Rollback and try later. + raise Exception('retry') + except Exception as e: + request.env.cr.rollback() + _logger.exception( + "Encountered an error while post-processing transaction with id %s:\n%s", + monitored_tx.id, e + ) + raise - # Stop monitoring already post-processed transactions - post_processed_txs = monitored_txs.filtered('is_post_processed') - self.remove_transactions(post_processed_txs) - - # Finalize post-processing of transactions before displaying them to the user - txs_to_post_process = (monitored_txs - post_processed_txs).filtered( - lambda t: t.state == 'done' - ) - success, error = True, None - try: - txs_to_post_process._finalize_post_processing() - except psycopg2.OperationalError: # A collision of accounting sequences occurred - request.env.cr.rollback() # Rollback and try later - success = False - error = 'tx_process_retry' - except Exception as e: - request.env.cr.rollback() - success = False - error = str(e) - _logger.exception( - "encountered an error while post-processing transactions with ids %s:\n%s", - ', '.join([str(tx_id) for tx_id in txs_to_post_process.ids]), e - ) - - return { - 'success': success, - 'error': error, - 'display_values_list': display_values_list, - } + # Return the post-processing values to display the transaction summary to the customer. + return monitored_tx._get_post_processing_values() @classmethod - def monitor_transactions(cls, transactions): - """ Add the ids of the provided transactions to the list of monitored transaction ids. + def monitor_transaction(cls, transaction): + """ Make the provided transaction id monitored. - :param recordset transactions: The transactions to monitor, as a `payment.transaction` - recordset + :param payment.transaction transaction: The transaction to monitor. :return: None """ - if transactions: - monitored_tx_ids = request.session.get(cls.MONITORED_TX_IDS_KEY, []) - request.session[cls.MONITORED_TX_IDS_KEY] = list( - set(monitored_tx_ids).union(transactions.ids) - ) + request.session[cls.MONITORED_TX_ID_KEY] = transaction.id @classmethod - def get_monitored_transaction_ids(cls): - """ Return the ids of transactions being monitored. + def get_monitored_transaction_id(cls): + """ Return the id of transaction being monitored. - Only the ids and not the recordset itself is returned to allow the caller browsing the - recordset with sudo privileges, and using the ids in a custom query. + Only the id and not the recordset itself is returned to allow the caller browsing the + recordset with sudo privileges, and using the id in a custom query. - :return: The ids of transactions being monitored + :return: The id of transactions being monitored :rtype: list """ - return request.session.get(cls.MONITORED_TX_IDS_KEY, []) - - @classmethod - def remove_transactions(cls, transactions): - """ Remove the ids of the provided transactions from the list of monitored transaction ids. - - :param recordset transactions: The transactions to remove, as a `payment.transaction` - recordset - :return: None - """ - if transactions: - monitored_tx_ids = request.session.get(cls.MONITORED_TX_IDS_KEY, []) - request.session[cls.MONITORED_TX_IDS_KEY] = [ - tx_id for tx_id in monitored_tx_ids if tx_id not in transactions.ids - ] + return request.session.get(cls.MONITORED_TX_ID_KEY) diff --git a/addons/payment/models/payment_provider.py b/addons/payment/models/payment_provider.py index 84b71416d0b..c57f71bd7e8 100644 --- a/addons/payment/models/payment_provider.py +++ b/addons/payment/models/payment_provider.py @@ -142,7 +142,7 @@ class PaymentProvider(models.Model): done_msg = fields.Html( string="Done Message", help="The message displayed if the order is successfully done after the payment process", - default=lambda self: _("Your payment has been successfully processed. Thank you!"), + default=lambda self: _("Your payment has been successfully processed."), translate=True) cancel_msg = fields.Html( string="Canceled Message", diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index 511af71815f..293a21fd98a 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -911,9 +911,10 @@ class PaymentTransaction(models.Model): The returned dict contains the following entries: - `provider_code`: The code of the provider. + - `provider_name`: The name of the provider. - `reference`: The reference of the transaction. - `amount`: The rounded amount of the transaction. - - `currency_id`: The currency of the transaction, as a `res.currency` id. + - `currency_code`: The currency of the transaction, as a `res.currency` id. - `state`: The transaction state: `draft`, `pending`, `authorized`, `done`, `cancel`, or `error`. - `state_message`: The information message about the state. @@ -929,15 +930,23 @@ class PaymentTransaction(models.Model): """ self.ensure_one() + display_message = None + if self.state == 'pending': + display_message = self.provider_id.pending_msg + elif self.state == 'done': + display_message = self.provider_id.done_msg + elif self.state == 'cancel': + display_message = self.provider_id.cancel_msg post_processing_values = { 'provider_code': self.provider_code, + 'provider_name': self.provider_id.name, 'reference': self.reference, 'amount': self.amount, 'currency_code': self.currency_id.name, 'state': self.state, 'state_message': self.state_message, + 'display_message': display_message, 'operation': self.operation, - 'is_post_processed': self.is_post_processed, 'landing_route': self.landing_route, } _logger.debug( @@ -953,8 +962,6 @@ class PaymentTransaction(models.Model): """ txs_to_post_process = self if not txs_to_post_process: - # Let the client post-process transactions so that they remain available in the portal - client_handling_limit_date = datetime.now() - relativedelta.relativedelta(minutes=10) # Don't try forever to post-process a transaction that doesn't go through. Set the limit # to 4 days because some providers (PayPal) need that much for the payment verification. retry_limit_date = datetime.now() - relativedelta.relativedelta(days=4) @@ -962,8 +969,6 @@ class PaymentTransaction(models.Model): txs_to_post_process = self.search([ ('state', '=', 'done'), ('is_post_processed', '=', False), - '|', ('last_state_change', '<=', client_handling_limit_date), - ('source_transaction_id', '!=', False), ('last_state_change', '>=', retry_limit_date), ]) for tx in txs_to_post_process: diff --git a/addons/payment/static/src/js/post_processing.js b/addons/payment/static/src/js/post_processing.js index 5d5a8e2cff3..c32908ac287 100644 --- a/addons/payment/static/src/js/post_processing.js +++ b/addons/payment/static/src/js/post_processing.js @@ -1,132 +1,96 @@ /** @odoo-module **/ - import publicWidget from "@web/legacy/js/public/public_widget"; - import { renderToElement } from "@web/core/utils/render"; - import { _t } from "@web/core/l10n/translation"; +import publicWidget from "@web/legacy/js/public/public_widget"; +import { renderToElement } from "@web/core/utils/render"; +import { markup } from "@odoo/owl"; +import { _t } from "@web/core/l10n/translation"; - import { markup } from "@odoo/owl"; +publicWidget.registry.PaymentPostProcessing = publicWidget.Widget.extend({ + selector: 'div[name="o_payment_status"]', - publicWidget.registry.PaymentPostProcessing = publicWidget.Widget.extend({ - selector: 'div[name="o_payment_status"]', + timeout: 0, + pollCount: 0, - _pollCount: 0, + async start() { + this.call('ui', 'block', { + 'message': _t("We are processing your payment. Please wait."), + }); + this._poll(); + return this._super.apply(this, arguments); + }, - start: function() { - this.displayLoading(); - this.poll(); - return this._super.apply(this, arguments); - }, - /* Methods */ - startPolling: function () { - var timeout = 3000; - // - if(this._pollCount >= 10 && this._pollCount < 20) { - timeout = 10000; - } - else if(this._pollCount >= 20) { - timeout = 30000; - } - // - setTimeout(this.poll.bind(this), timeout); - this._pollCount ++; - }, - poll: function () { - var self = this; + _poll() { + this._updateTimeout(); + setTimeout(() => { + // Fetch the post-processing values from the server. + const self = this; this._rpc({ route: '/payment/status/poll', params: { 'csrf_token': odoo.csrf_token, } - }).then(function(data) { - if(data.success === true) { - self.processPolledData(data.display_values_list); - } - else { - switch(data.error) { - case "tx_process_retry": - break; - case "no_tx_found": - self.displayContent("payment.no_tx_found", {}); - break; - default: // if an exception is raised - self.displayContent("payment.exception", {exception_msg: data.error}); - break; - } - } - self.startPolling(); + }).then(postProcessingValues => { + let { state, display_message, landing_route } = postProcessingValues; - }).guardedCatch(function() { - self.displayContent("payment.rpc_error", {}); - self.startPolling(); - }); - }, - processPolledData: function (display_values_list) { - var render_values = { - 'tx_draft': [], - 'tx_pending': [], - 'tx_authorized': [], - 'tx_done': [], - 'tx_cancel': [], - 'tx_error': [], - }; + // Display the transaction details before redirection to show something ASAP. + if (display_message) { + postProcessingValues.display_message = markup(display_message); + } + this._renderTemplate('payment.transactionDetails', postProcessingValues); - // group the transaction according to their state - display_values_list.forEach(function (display_values) { - var key = 'tx_' + display_values.state; - if(key in render_values) { - if (display_values["display_message"]) { - display_values.display_message = markup(display_values.display_message) + // Redirect the user to the landing route if the transaction reached a final state. + if (self._getFinalStates(postProcessingValues['provider_code']).includes(state)) { + window.location = landing_route; + } else { + self._poll(); + } + }).guardedCatch(error => { + error.event.preventDefault(); + if (error.message.data) { // Server error. + switch (error.message.data.message) { + case 'retry': + self._poll(); + break; + case 'tx_not_found': + self._renderTemplate('payment.tx_not_found'); + break; + default: + self._renderTemplate( + 'payment.exception', { error_message: error.message.data.message } + ); + break; } - render_values[key].push(display_values); + } else { // RPC error (server unreachable). + self._renderTemplate('payment.rpc_error'); + self._poll(); } }); + }, this.timeout); + }, - function countTxInState(states) { - var nbTx = 0; - for (var prop in render_values) { - if (states.indexOf(prop) > -1 && render_values.hasOwnProperty(prop)) { - nbTx += render_values[prop].length; - } - } - return nbTx; - } + _getFinalStates(providerCode) { + return ['authorized', 'done']; + }, - /* - * When the server sends the list of monitored transactions, it tries to post-process - * all the successful ones. If it succeeds or if the post-process has already been made, - * the transaction is removed from the list of monitored transactions and won't be - * included in the next response. We assume that successful and post-process - * transactions should always prevail on others, regardless of their number or state. - */ - if (render_values['tx_done'].length === 1 && - render_values['tx_done'][0].is_post_processed) { - window.location = render_values['tx_done'][0].landing_route; - return; - } - // If there are multiple transactions monitored, display them all to the customer. If - // there is only one transaction monitored, redirect directly the customer to the - // landing route. - if(countTxInState(['tx_done', 'tx_error', 'tx_pending', 'tx_authorized']) === 1) { - // We don't want to redirect customers to the landing page when they have a pending - // transaction. The successful transactions are dealt with before. - var tx = render_values['tx_authorized'][0] || render_values['tx_error'][0]; - if (tx) { - window.location = tx.landing_route; - return; - } - } + _updateTimeout() { + if (this.pollCount >= 1 && this.pollCount < 10) { + this.timeout = 3000; + } + if (this.pollCount >= 10 && this.pollCount < 20) { + this.timeout = 10000; + } + else if (this.pollCount >= 20) { + this.timeout = 30000; + } + this.pollCount++; + }, - this.displayContent("payment.display_tx_list", render_values); - }, - displayContent: function (xmlid, render_values) { - this.call("ui", "unblock"); - this.$el.find('div[name="o_payment_status_content"]').empty().append(renderToElement(xmlid, render_values)); - }, - displayLoading: function () { - this.call("ui", "block", { - 'message': _t("We are processing your payment, please wait ..."), - }); - }, - }); + _renderTemplate(xmlid, display_values={}) { + this.call('ui', 'unblock'); + const statusContainer = document.querySelector('div[name="o_payment_status_content"]'); + statusContainer.innerHTML = renderToElement(xmlid, display_values).innerHTML; + }, - export default publicWidget.registry.PaymentPostProcessing; +}); + +export default publicWidget.registry.PaymentPostProcessing; diff --git a/addons/payment/static/src/scss/payment_templates.scss b/addons/payment/static/src/scss/payment_templates.scss index ed602ff2d66..9e0598328ac 100644 --- a/addons/payment/static/src/scss/payment_templates.scss +++ b/addons/payment/static/src/scss/payment_templates.scss @@ -1,3 +1,12 @@ -div#o_payment_status_alert > p { +div[name="o_payment_status_alert"] div > p { margin-bottom: 0; } + +.o_payment_details .col:not(:last-child) { + border-bottom: $border-width solid $border-color; + + @include media-breakpoint-up(md) { + border-right: $border-width solid $border-color; + border-bottom: none; + } +} diff --git a/addons/payment/static/src/xml/payment_post_processing.xml b/addons/payment/static/src/xml/payment_post_processing.xml index 4e07b3373d8..b7e2801dac2 100644 --- a/addons/payment/static/src/xml/payment_post_processing.xml +++ b/addons/payment/static/src/xml/payment_post_processing.xml @@ -1,150 +1,72 @@ - - -
- - - -
-

Operations in progress

-
- - - -

- - -

- - - - Your payment is being processed, please wait... - - - Saving your payment method, please wait... - - - - - Your payment has been processed.
- Click here to be redirected to the confirmation page. -
- - Your payment method has been saved.
- Click here to be redirected to the confirmation page. -
-
-
-
-
- - - -

- - -

- - - - - - - Your payment is in pending state.
- You will be notified when the payment is fully confirmed.
- Click here to be redirected to the confirmation page. -
-
-
-
- - - -

- - -

- - - - - - - Your payment has been received but need to be confirmed manually.
- You will be notified when the payment is confirmed. -
-
-
-
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ +
+ + Skip +
- - - -
-

Canceled operations

- + +
+
+ + +
- -
+ +

We are not able to find your payment, but don't worry.

You should receive an email confirming your payment in a few minutes.

If the payment hasn't been confirmed you can contact us.

@@ -152,16 +74,15 @@ -
-

Server error: Unable to contact the Odoo server.

-

Please wait ...

+
+

Unable to contact the server. Please wait.

-
+

Internal server error

-
+
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml index af6e6afeb4d..f018819aaf1 100644 --- a/addons/payment/views/payment_portal_templates.xml +++ b/addons/payment/views/payment_portal_templates.xml @@ -103,9 +103,9 @@ -
+
+ class="col-12 col-lg-8">
@@ -126,53 +126,49 @@
- -
-
-
+
+
+ +
+
-
- - -
-
-
- - -
-
-
- - -
-
-
-
- Processed by -
- -
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -186,7 +182,7 @@ -->
-