[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:
@@ -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')
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user