[FIX] website_sale: prevent users from modifying paid carts

Bug: Users are able to modify their eCommerce cart after it has been
paid when they fail to return to Odoo through the payment provider's
return route. This prevents the cart from being confirmed.

Steps to reproduce:
1. Install the payment provider Mollie and set it to test mode.
2. Go to the /shop page, add a product to the cart, and select Mollie
   for the payment.
3. On Mollie's hosted payment page, use the card number 4111111111111111
   with the expiry date 03/30 and the secret code 123. Select 'paid' as
   the payment outcome.
4. Confirm the payment but take care not to be redirected to the
   /payment/status route. For examples, close the tab before or comment
   out https://github.com/odoo/odoo/blob/15.0/addons/payment_mollie/controllers/main.py#L38.
5. Go back to /shop/cart and modify the cart.
6. Wait up to 20 minutes for the "payment: post-process transactions"
   cron to try to confirm the cart.
7. Check the cart's chatter: the confirmation failed because the cart's
   and transaction's amounts mismatch.

Explanation: The post-processing of the transaction is responsible for
confirming the cart, but it failed to be triggered because the user did
not visit the /payment/status page. The post-processing cron takes care
of pending post-processings after 10 to 20 minutes, which is long enough
for the user to modify their cart.

Fix: Force creating a new cart as soon as the current one is paid and is
being requested by the website.

task-2995504

closes odoo/odoo#102060

X-original-commit: 5f8674621cbf5919e10e32f29b66ae171fe05993
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Signed-off-by: William Braeckman (wbr) <wbr@odoo.com>
This commit is contained in:
Antoine Vandevenne (anv)
2022-10-04 16:01:08 +02:00
parent 82265c52ca
commit aab9d987f0
5 changed files with 76 additions and 1 deletions
+6
View File
@@ -1524,6 +1524,12 @@ class WebsiteSale(http.Controller):
"""
if sale_order_id is None:
order = request.website.sale_get_order()
if not order and 'sale_last_order_id' in request.session:
# Retrieve the last known order from the session if the session key `sale_order_id`
# was prematurely cleared. This is done to prevent the user from updating their cart
# after payment in case they don't return from payment through this route.
last_order_id = request.session['sale_last_order_id']
order = request.env['sale.order'].sudo().browse(last_order_id).exists()
else:
order = request.env['sale.order'].sudo().browse(sale_order_id)
assert order.id == request.session.get('sale_last_order_id')
+7
View File
@@ -333,6 +333,13 @@ class Website(models.Model):
else:
sale_order_sudo = SaleOrder
# Ignore the current order if a payment has been initiated. We don't want to retrieve the
# cart and allow the user to update it when the payment is about to confirm it.
if sale_order_sudo and sale_order_sudo.get_portal_last_transaction().state in (
'pending', 'authorized', 'done'
):
sale_order_sudo = None
if not (sale_order_sudo or force_create):
# Do not create a SO record unless needed
if request.session.get('sale_order_id'):
+1
View File
@@ -7,6 +7,7 @@ from . import test_sale_process
from . import test_sitemap
from . import test_website_sale_add_to_cart_snippet
from . import test_website_sale_cart_abandoned
from . import test_website_sale_cart_payment
from . import test_website_sale_cart_popover
from . import test_website_sale_cart_recovery
from . import test_website_sale_cart
@@ -0,0 +1,54 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.models import Command
from odoo.tests.common import tagged
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.website.tools import MockRequest
@tagged('post_install', '-at_install')
class WebsiteSaleCartPayment(PaymentCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env['website'].get_current_website()
with MockRequest(cls.env, website=cls.website):
cls.order = cls.website.sale_get_order(force_create=True) # Create the cart to retrieve
cls.tx = cls.env['payment.transaction'].create({
'amount': cls.amount,
'currency_id': cls.currency.id,
'provider_id': cls.provider.id,
'reference': cls.reference,
'operation': 'online_redirect',
'partner_id': cls.partner.id,
})
cls.order.write({'transaction_ids': [Command.set([cls.tx.id])]})
def test_unpaid_orders_can_be_retrieved(self):
""" Test that fetching sales orders linked to a payment transaction in the states 'draft',
'cancel', or 'error' returns the orders. """
for unpaid_order_tx_state in ('draft', 'cancel', 'error'):
self.tx.state = unpaid_order_tx_state
with MockRequest(self.env, website=self.website, sale_order_id=self.order.id):
self.assertEqual(
self.website.sale_get_order(),
self.order,
msg=f"The transaction state '{unpaid_order_tx_state}' should not prevent "
f"retrieving the linked order.",
)
def test_paid_orders_cannot_be_retrieved(self):
""" Test that fetching sales orders linked to a payment transaction in the states 'pending',
'authorized', or 'done' returns an empty recordset to prevent updating the paid orders. """
self.tx.provider_id.support_manual_capture = True
for paid_order_tx_state in ('pending', 'authorized', 'done'):
self.tx.state = paid_order_tx_state
with MockRequest(self.env, website=self.website, sale_order_id=self.order.id):
self.assertFalse(
self.website.sale_get_order(),
msg=f"The transaction state '{paid_order_tx_state}' should prevent retrieving "
f"the linked order.",
)
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.exceptions import UserError
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.website_sale_delivery.controllers.main import WebsiteSaleDelivery
@@ -19,7 +22,11 @@ class TestWebsiteSaleDeliveryController(PaymentCommon):
order = self.website.sale_get_order(force_create=True)
order.transaction_ids = self._create_transaction(flow='redirect', state='pending')
with self.assertRaises(UserError):
self.Controller.update_eshop_carrier(carrier_id=1)
with patch(
'odoo.addons.website_sale.models.website.Website.sale_get_order',
return_value=order,
): # Patch to retrieve the order even if it is linked to a pending transaction.
self.Controller.update_eshop_carrier(carrier_id=1)
# test that changing the carrier while there is a draft transaction doesn't raise an error
def test_controller_change_carrier_when_draft_transaction(self):