[FIX] website_sale_aplicoop: invalidate stale carts on closed group orders
This commit is contained in:
parent
89c008441e
commit
c345699bf4
4 changed files with 329 additions and 23 deletions
|
|
@ -1372,6 +1372,21 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
raise ValueError(f"Invalid JSON: {str(e)}") from e
|
||||
return data
|
||||
|
||||
def _build_group_order_unavailable_response(self, group_order, status=403):
|
||||
"""Build a consistent JSON response when a group order is not open."""
|
||||
return request.make_response(
|
||||
json.dumps(
|
||||
{
|
||||
"error": request.env._("Order is not available"),
|
||||
"code": "group_order_not_open",
|
||||
"order_state": group_order.state if group_order else False,
|
||||
"action": "clear_cart",
|
||||
}
|
||||
),
|
||||
[("Content-Type", "application/json")],
|
||||
status=status,
|
||||
)
|
||||
|
||||
def _find_recent_draft_order(self, partner_id, order_id):
|
||||
"""Find most recent draft sale.order for partner and group_order in current week.
|
||||
|
||||
|
|
@ -2018,6 +2033,66 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
"website_sale_aplicoop.eskaera_checkout", template_context
|
||||
)
|
||||
|
||||
@http.route(
|
||||
["/eskaera/check-status"],
|
||||
type="http",
|
||||
auth="user",
|
||||
website=True,
|
||||
methods=["POST"],
|
||||
csrf=False,
|
||||
)
|
||||
def check_group_order_status(self, **post):
|
||||
"""Return status information for a group.order.
|
||||
|
||||
Intended for frontend proactive cart invalidation when the order
|
||||
changed state between visits.
|
||||
"""
|
||||
try:
|
||||
data = self._decode_json_body()
|
||||
except ValueError as e:
|
||||
return request.make_response(
|
||||
json.dumps({"error": str(e)}),
|
||||
[("Content-Type", "application/json")],
|
||||
status=400,
|
||||
)
|
||||
|
||||
order_id = data.get("order_id")
|
||||
if not order_id:
|
||||
return request.make_response(
|
||||
json.dumps({"error": "order_id is required"}),
|
||||
[("Content-Type", "application/json")],
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
order_id = int(order_id)
|
||||
except (TypeError, ValueError):
|
||||
return request.make_response(
|
||||
json.dumps({"error": f"Invalid order_id format: {order_id}"}),
|
||||
[("Content-Type", "application/json")],
|
||||
status=400,
|
||||
)
|
||||
|
||||
group_order = request.env["group.order"].sudo().browse(order_id)
|
||||
if not group_order.exists():
|
||||
return request.make_response(
|
||||
json.dumps({"error": f"Order {order_id} not found"}),
|
||||
[("Content-Type", "application/json")],
|
||||
status=404,
|
||||
)
|
||||
|
||||
response_data = {
|
||||
"success": True,
|
||||
"order_id": group_order.id,
|
||||
"state": group_order.state,
|
||||
"is_open": group_order.state == "open",
|
||||
"action": "clear_cart" if group_order.state != "open" else "none",
|
||||
}
|
||||
return request.make_response(
|
||||
json.dumps(response_data),
|
||||
[("Content-Type", "application/json")],
|
||||
)
|
||||
|
||||
@http.route(
|
||||
["/eskaera/save-cart"],
|
||||
type="http",
|
||||
|
|
@ -2068,6 +2143,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
status=400,
|
||||
)
|
||||
|
||||
if group_order.state != "open":
|
||||
return self._build_group_order_unavailable_response(group_order)
|
||||
|
||||
current_user = request.env.user
|
||||
if not current_user.partner_id:
|
||||
return request.make_response(
|
||||
|
|
@ -2188,6 +2266,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
status=400,
|
||||
)
|
||||
|
||||
if group_order.state != "open":
|
||||
return self._build_group_order_unavailable_response(group_order)
|
||||
|
||||
current_user = request.env.user
|
||||
if not current_user.partner_id:
|
||||
return request.make_response(
|
||||
|
|
@ -2343,6 +2424,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
status=400,
|
||||
)
|
||||
|
||||
if group_order.state != "open":
|
||||
return self._build_group_order_unavailable_response(group_order)
|
||||
|
||||
current_user = request.env.user
|
||||
|
||||
# Validate that the user has a partner_id
|
||||
|
|
|
|||
|
|
@ -56,33 +56,35 @@
|
|||
console.log("[groupOrderShop] Translations loaded from server");
|
||||
self.labels = i18nManager.getAll();
|
||||
|
||||
self._loadCart();
|
||||
self._checkConfirmationMessage();
|
||||
self._initializeTooltips();
|
||||
self._checkGroupOrderStatus(function () {
|
||||
self._loadCart();
|
||||
self._checkConfirmationMessage();
|
||||
self._initializeTooltips();
|
||||
|
||||
// Update display if there is cart-items-container
|
||||
if (cartContainer) {
|
||||
self._updateCartDisplay();
|
||||
}
|
||||
// Update display if there is cart-items-container
|
||||
if (cartContainer) {
|
||||
self._updateCartDisplay();
|
||||
}
|
||||
|
||||
// Check if we're loading from history
|
||||
var storageKey = "load_from_history_" + self.orderId;
|
||||
if (sessionStorage.getItem(storageKey)) {
|
||||
// Load items from historical order
|
||||
self._loadFromHistory();
|
||||
} else {
|
||||
// Auto-load draft order on page load (silent mode)
|
||||
self._autoLoadDraftOnInit();
|
||||
}
|
||||
// Check if we're loading from history
|
||||
var storageKey = "load_from_history_" + self.orderId;
|
||||
if (sessionStorage.getItem(storageKey)) {
|
||||
// Load items from historical order
|
||||
self._loadFromHistory();
|
||||
} else {
|
||||
// Auto-load draft order on page load (silent mode)
|
||||
self._autoLoadDraftOnInit();
|
||||
}
|
||||
|
||||
// Emit event when fully initialized
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("groupOrderShopReady", {
|
||||
detail: { labels: self.labels },
|
||||
})
|
||||
);
|
||||
// Emit event when fully initialized
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("groupOrderShopReady", {
|
||||
detail: { labels: self.labels },
|
||||
})
|
||||
);
|
||||
|
||||
console.log("[groupOrderShop] ✓ Initialization complete");
|
||||
console.log("[groupOrderShop] ✓ Initialization complete");
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error("[groupOrderShop] Failed to initialize translations:", error);
|
||||
|
|
@ -385,6 +387,74 @@
|
|||
console.log("Verification - immediately read back:", localStorage.getItem(cartKey));
|
||||
},
|
||||
|
||||
_clearCurrentOrderCartSilently: function () {
|
||||
var cartKey = "eskaera_" + this.orderId + "_cart";
|
||||
localStorage.removeItem(cartKey);
|
||||
this.cart = {};
|
||||
console.log("[groupOrderShop] Cart cleared silently for order:", this.orderId);
|
||||
},
|
||||
|
||||
_isClosedOrderResponse: function (xhr) {
|
||||
if (!xhr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xhr.status !== 400 && xhr.status !== 403) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText || "{}");
|
||||
return (
|
||||
errorData.code === "group_order_not_open" ||
|
||||
errorData.action === "clear_cart" ||
|
||||
errorData.order_state === "closed" ||
|
||||
errorData.order_state === "cancelled"
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_checkGroupOrderStatus: function (callback) {
|
||||
var self = this;
|
||||
var done = function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/eskaera/check-status", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
|
||||
xhr.onload = function () {
|
||||
if (xhr.status !== 200) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var data = JSON.parse(xhr.responseText || "{}");
|
||||
if (data && data.is_open === false) {
|
||||
self._clearCurrentOrderCartSilently();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[groupOrderShop] check-status parse error", e);
|
||||
}
|
||||
done();
|
||||
};
|
||||
|
||||
xhr.onerror = function () {
|
||||
done();
|
||||
};
|
||||
|
||||
xhr.send(
|
||||
JSON.stringify({
|
||||
order_id: this.orderId,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_showNotification: function (message, type, duration) {
|
||||
// type: 'success', 'error', 'warning', 'info'
|
||||
// duration: milliseconds (default 8000 = 8 seconds)
|
||||
|
|
@ -1406,6 +1476,11 @@
|
|||
);
|
||||
}
|
||||
} else {
|
||||
if (self._isClosedOrderResponse(xhr)) {
|
||||
self._clearCurrentOrderCartSilently();
|
||||
self._updateCartDisplay();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
self._showNotification(
|
||||
|
|
@ -1525,6 +1600,11 @@
|
|||
);
|
||||
}
|
||||
} else {
|
||||
if (self._isClosedOrderResponse(xhr)) {
|
||||
self._clearCurrentOrderCartSilently();
|
||||
self._updateCartDisplay();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
self._showNotification(
|
||||
|
|
@ -1603,6 +1683,11 @@
|
|||
self._showNotification("Error processing response", "danger");
|
||||
}
|
||||
} else {
|
||||
if (self._isClosedOrderResponse(xhr)) {
|
||||
self._clearCurrentOrderCartSilently();
|
||||
self._updateCartDisplay();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
console.error("HTTP error:", xhr.status, errorData);
|
||||
|
|
@ -1712,6 +1797,11 @@
|
|||
);
|
||||
}
|
||||
} else {
|
||||
if (self._isClosedOrderResponse(xhr)) {
|
||||
self._clearCurrentOrderCartSilently();
|
||||
self._updateCartDisplay();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var errorData = JSON.parse(xhr.responseText);
|
||||
console.error("HTTP error:", xhr.status, errorData);
|
||||
|
|
|
|||
|
|
@ -13,3 +13,4 @@ from . import test_date_calculations # noqa: F401
|
|||
from . import test_pricing_with_pricelist # noqa: F401
|
||||
from . import test_portal_sale_order_creation # noqa: F401
|
||||
from . import test_cron_picking_batch # noqa: F401
|
||||
from . import test_group_order_status_endpoint # noqa: F401
|
||||
|
|
|
|||
131
website_sale_aplicoop/tests/test_group_order_status_endpoint.py
Normal file
131
website_sale_aplicoop/tests/test_group_order_status_endpoint.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from ..controllers.website_sale import AplicoopWebsiteSale
|
||||
|
||||
|
||||
class TestGroupOrderStatusEndpoint(TransactionCase):
|
||||
"""Tests for status endpoint and closed-order safeguards in cart endpoints."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.controller = AplicoopWebsiteSale()
|
||||
|
||||
self.group = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Status Group",
|
||||
"is_company": True,
|
||||
"is_group": True,
|
||||
"email": "status-group@test.com",
|
||||
}
|
||||
)
|
||||
|
||||
self.group_order = self.env["group.order"].create(
|
||||
{
|
||||
"name": "Status Test Order",
|
||||
"group_ids": [(6, 0, [self.group.id])],
|
||||
"type": "regular",
|
||||
"period": "weekly",
|
||||
"pickup_day": "3",
|
||||
"cutoff_day": "1",
|
||||
"start_date": fields.Date.today(),
|
||||
"end_date": fields.Date.today() + timedelta(days=7),
|
||||
}
|
||||
)
|
||||
self.group_order.action_open()
|
||||
|
||||
self.product = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Status Endpoint Product",
|
||||
"type": "consu",
|
||||
"list_price": 12.0,
|
||||
}
|
||||
)
|
||||
|
||||
def _build_request_mock(self, payload):
|
||||
"""Build a request mock with JSON payload and make_response support."""
|
||||
|
||||
def _make_response(data, headers=None, status=200):
|
||||
raw = data.encode("utf-8") if isinstance(data, str) else data
|
||||
return SimpleNamespace(data=raw, status=status, headers=headers or [])
|
||||
|
||||
return SimpleNamespace(
|
||||
env=self.env,
|
||||
httprequest=SimpleNamespace(
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
),
|
||||
make_response=_make_response,
|
||||
)
|
||||
|
||||
def test_check_group_order_status_open(self):
|
||||
"""Endpoint returns is_open=True for open group order."""
|
||||
request_mock = self._build_request_mock({"order_id": self.group_order.id})
|
||||
|
||||
with patch(
|
||||
"odoo.addons.website_sale_aplicoop.controllers.website_sale.request",
|
||||
request_mock,
|
||||
):
|
||||
response = self.controller.check_group_order_status.__wrapped__(
|
||||
self.controller
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode("utf-8"))
|
||||
self.assertTrue(data["success"])
|
||||
self.assertTrue(data["is_open"])
|
||||
self.assertEqual(data["state"], "open")
|
||||
self.assertEqual(data["action"], "none")
|
||||
|
||||
def test_check_group_order_status_closed(self):
|
||||
"""Endpoint returns clear_cart action for non-open group order."""
|
||||
self.group_order.action_close()
|
||||
request_mock = self._build_request_mock({"order_id": self.group_order.id})
|
||||
|
||||
with patch(
|
||||
"odoo.addons.website_sale_aplicoop.controllers.website_sale.request",
|
||||
request_mock,
|
||||
):
|
||||
response = self.controller.check_group_order_status.__wrapped__(
|
||||
self.controller
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode("utf-8"))
|
||||
self.assertTrue(data["success"])
|
||||
self.assertFalse(data["is_open"])
|
||||
self.assertEqual(data["state"], "closed")
|
||||
self.assertEqual(data["action"], "clear_cart")
|
||||
|
||||
def test_save_order_endpoint_rejects_closed_group_order(self):
|
||||
"""/eskaera/save-order must reject closed group order with clear_cart action."""
|
||||
self.group_order.action_close()
|
||||
|
||||
payload = {
|
||||
"order_id": self.group_order.id,
|
||||
"items": [
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"quantity": 1,
|
||||
"product_price": 12.0,
|
||||
}
|
||||
],
|
||||
}
|
||||
request_mock = self._build_request_mock(payload)
|
||||
|
||||
with patch(
|
||||
"odoo.addons.website_sale_aplicoop.controllers.website_sale.request",
|
||||
request_mock,
|
||||
):
|
||||
response = self.controller.save_eskaera_draft.__wrapped__(self.controller)
|
||||
|
||||
data = json.loads(response.data.decode("utf-8"))
|
||||
self.assertEqual(response.status, 403)
|
||||
self.assertEqual(data.get("code"), "group_order_not_open")
|
||||
self.assertEqual(data.get("order_state"), "closed")
|
||||
self.assertEqual(data.get("action"), "clear_cart")
|
||||
Loading…
Add table
Add a link
Reference in a new issue