[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
|
raise ValueError(f"Invalid JSON: {str(e)}") from e
|
||||||
return data
|
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):
|
def _find_recent_draft_order(self, partner_id, order_id):
|
||||||
"""Find most recent draft sale.order for partner and group_order in current week.
|
"""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
|
"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(
|
@http.route(
|
||||||
["/eskaera/save-cart"],
|
["/eskaera/save-cart"],
|
||||||
type="http",
|
type="http",
|
||||||
|
|
@ -2068,6 +2143,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group_order.state != "open":
|
||||||
|
return self._build_group_order_unavailable_response(group_order)
|
||||||
|
|
||||||
current_user = request.env.user
|
current_user = request.env.user
|
||||||
if not current_user.partner_id:
|
if not current_user.partner_id:
|
||||||
return request.make_response(
|
return request.make_response(
|
||||||
|
|
@ -2188,6 +2266,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group_order.state != "open":
|
||||||
|
return self._build_group_order_unavailable_response(group_order)
|
||||||
|
|
||||||
current_user = request.env.user
|
current_user = request.env.user
|
||||||
if not current_user.partner_id:
|
if not current_user.partner_id:
|
||||||
return request.make_response(
|
return request.make_response(
|
||||||
|
|
@ -2343,6 +2424,9 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
status=400,
|
status=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group_order.state != "open":
|
||||||
|
return self._build_group_order_unavailable_response(group_order)
|
||||||
|
|
||||||
current_user = request.env.user
|
current_user = request.env.user
|
||||||
|
|
||||||
# Validate that the user has a partner_id
|
# Validate that the user has a partner_id
|
||||||
|
|
|
||||||
|
|
@ -56,33 +56,35 @@
|
||||||
console.log("[groupOrderShop] Translations loaded from server");
|
console.log("[groupOrderShop] Translations loaded from server");
|
||||||
self.labels = i18nManager.getAll();
|
self.labels = i18nManager.getAll();
|
||||||
|
|
||||||
self._loadCart();
|
self._checkGroupOrderStatus(function () {
|
||||||
self._checkConfirmationMessage();
|
self._loadCart();
|
||||||
self._initializeTooltips();
|
self._checkConfirmationMessage();
|
||||||
|
self._initializeTooltips();
|
||||||
|
|
||||||
// Update display if there is cart-items-container
|
// Update display if there is cart-items-container
|
||||||
if (cartContainer) {
|
if (cartContainer) {
|
||||||
self._updateCartDisplay();
|
self._updateCartDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're loading from history
|
// Check if we're loading from history
|
||||||
var storageKey = "load_from_history_" + self.orderId;
|
var storageKey = "load_from_history_" + self.orderId;
|
||||||
if (sessionStorage.getItem(storageKey)) {
|
if (sessionStorage.getItem(storageKey)) {
|
||||||
// Load items from historical order
|
// Load items from historical order
|
||||||
self._loadFromHistory();
|
self._loadFromHistory();
|
||||||
} else {
|
} else {
|
||||||
// Auto-load draft order on page load (silent mode)
|
// Auto-load draft order on page load (silent mode)
|
||||||
self._autoLoadDraftOnInit();
|
self._autoLoadDraftOnInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit event when fully initialized
|
// Emit event when fully initialized
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new CustomEvent("groupOrderShopReady", {
|
new CustomEvent("groupOrderShopReady", {
|
||||||
detail: { labels: self.labels },
|
detail: { labels: self.labels },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[groupOrderShop] ✓ Initialization complete");
|
console.log("[groupOrderShop] ✓ Initialization complete");
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.error("[groupOrderShop] Failed to initialize translations:", error);
|
console.error("[groupOrderShop] Failed to initialize translations:", error);
|
||||||
|
|
@ -385,6 +387,74 @@
|
||||||
console.log("Verification - immediately read back:", localStorage.getItem(cartKey));
|
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) {
|
_showNotification: function (message, type, duration) {
|
||||||
// type: 'success', 'error', 'warning', 'info'
|
// type: 'success', 'error', 'warning', 'info'
|
||||||
// duration: milliseconds (default 8000 = 8 seconds)
|
// duration: milliseconds (default 8000 = 8 seconds)
|
||||||
|
|
@ -1406,6 +1476,11 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (self._isClosedOrderResponse(xhr)) {
|
||||||
|
self._clearCurrentOrderCartSilently();
|
||||||
|
self._updateCartDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var errorData = JSON.parse(xhr.responseText);
|
var errorData = JSON.parse(xhr.responseText);
|
||||||
self._showNotification(
|
self._showNotification(
|
||||||
|
|
@ -1525,6 +1600,11 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (self._isClosedOrderResponse(xhr)) {
|
||||||
|
self._clearCurrentOrderCartSilently();
|
||||||
|
self._updateCartDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var errorData = JSON.parse(xhr.responseText);
|
var errorData = JSON.parse(xhr.responseText);
|
||||||
self._showNotification(
|
self._showNotification(
|
||||||
|
|
@ -1603,6 +1683,11 @@
|
||||||
self._showNotification("Error processing response", "danger");
|
self._showNotification("Error processing response", "danger");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (self._isClosedOrderResponse(xhr)) {
|
||||||
|
self._clearCurrentOrderCartSilently();
|
||||||
|
self._updateCartDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var errorData = JSON.parse(xhr.responseText);
|
var errorData = JSON.parse(xhr.responseText);
|
||||||
console.error("HTTP error:", xhr.status, errorData);
|
console.error("HTTP error:", xhr.status, errorData);
|
||||||
|
|
@ -1712,6 +1797,11 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (self._isClosedOrderResponse(xhr)) {
|
||||||
|
self._clearCurrentOrderCartSilently();
|
||||||
|
self._updateCartDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var errorData = JSON.parse(xhr.responseText);
|
var errorData = JSON.parse(xhr.responseText);
|
||||||
console.error("HTTP error:", xhr.status, errorData);
|
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_pricing_with_pricelist # noqa: F401
|
||||||
from . import test_portal_sale_order_creation # noqa: F401
|
from . import test_portal_sale_order_creation # noqa: F401
|
||||||
from . import test_cron_picking_batch # 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