diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index d3925e0..b6019c7 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -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 diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 3f76c3c..ad1e696 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -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); diff --git a/website_sale_aplicoop/tests/__init__.py b/website_sale_aplicoop/tests/__init__.py index 5734cf1..2effc65 100644 --- a/website_sale_aplicoop/tests/__init__.py +++ b/website_sale_aplicoop/tests/__init__.py @@ -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 diff --git a/website_sale_aplicoop/tests/test_group_order_status_endpoint.py b/website_sale_aplicoop/tests/test_group_order_status_endpoint.py new file mode 100644 index 0000000..f97111f --- /dev/null +++ b/website_sale_aplicoop/tests/test_group_order_status_endpoint.py @@ -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")