Compare commits

..

2 commits

Author SHA1 Message Date
GitHub Copilot
9484f8c349 [FIX] website_sale_aplicoop: split cart and cycle stamp in localStorage
965c5c2 stored the cart as {cutoff_date, items: {...}} in
eskaera_<id>_cart. That broke every other reader of the same key:

- checkout_labels.js iterates Object.keys() expecting product IDs and
  rendered "cutoff_date" / "items" as ghost rows → users on the
  checkout page saw their cart as empty.
- home_delivery.js read/wrote the cart in place; the in-place mutation
  destroyed the wrapper.
- _saveOrderDraft serialised the same object straight to the server,
  POSTing "cutoff_date" and "items" as productIds.

Split the schema: eskaera_<id>_cart keeps the plain {productId: {...}}
shape every other JS file already relies on; the cycle marker moves to
eskaera_<id>_cart_cycle. _loadCart migrates browsers still holding the
v18.0.1.10.0 wrapped value on the next read.

Also make eviction strictly opt-in: drop the cart only when we know
the current cycle AND the stored cycle disagrees. Missing data on
either side (check-status didn't run, XHR failed) → preserve. This
restores the checkout page where check-status isn't called.

Version bump triggers the in-tab auto-reload added in 6e6d1e5 for
clients already on that build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 15:37:55 +02:00
GitHub Copilot
6e6d1e5256 [IMP] website_sale_aplicoop: signal frontend to reload on deploy
When we ship a JS-only fix, users with a long-lived tab keep running
the old code until they hard-refresh — there is no clean way to push
new code to an already-loaded page. Now /eskaera/check-status returns
the module's installed_version as client_version, and the eskaera
page embeds the same version in data-build-version on the cart
container. The JS captures the page's build version at init; on every
check-status response it compares them and triggers window.location.
reload() on mismatch. A sessionStorage timestamp guards against
reload loops if the versions stay disagreeing (cached HTML upstream).

The version bump in __manifest__.py also invalidates the asset bundle
URL hash, so even users without this signalling path get fresh JS on
their next navigation.

This is forward-looking: clients on the old JS (no check) still need
one manual refresh today, but every future deploy will auto-recover.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 15:31:37 +02:00
4 changed files with 116 additions and 23 deletions

View file

@ -3,7 +3,7 @@
{ # noqa: B018
"name": "Website Sale - Aplicoop",
"version": "18.0.1.10.0",
"version": "18.0.1.10.2",
"category": "Website/Sale",
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
"author": "Odoo Community Association (OCA), Criptomart",

View file

@ -641,6 +641,16 @@ class AplicoopWebsiteSale(WebsiteSale):
def _decode_json_body(self):
return _utils._decode_json_body(self, request)
def _get_installed_module_version(self):
"""Return the installed version of this addon, used as a build-id
signal so the frontend can detect a deploy and force a reload."""
module = (
request.env["ir.module.module"]
.sudo()
.search([("name", "=", "website_sale_aplicoop")], limit=1)
)
return module.installed_version or ""
def _build_group_order_unavailable_response(self, group_order, status=403):
"""Delegate building of unavailable response to utils helper."""
self._request = request
@ -827,6 +837,7 @@ class AplicoopWebsiteSale(WebsiteSale):
"current_page": page,
"has_next": has_next,
"total_products": total_products,
"module_version": self._get_installed_module_version(),
},
)
@ -1370,6 +1381,7 @@ class AplicoopWebsiteSale(WebsiteSale):
),
"cutoff_passed": cutoff_passed,
"cutoff_date": cutoff_date_str,
"client_version": self._get_installed_module_version(),
}
return request.make_response(
json.dumps(response_data),

View file

@ -45,6 +45,13 @@
console.log("Initializing cart for order:", this.orderId);
// Capture the build version embedded in the rendered HTML. The
// server returns its installed version in /eskaera/check-status,
// and if the two disagree we know a deploy happened and the
// current page is running stale JS — force a one-shot reload.
this._buildVersion = orderIdElement.getAttribute("data-build-version") || null;
console.log("[groupOrderShop] Build version:", this._buildVersion);
// Attach event listeners FIRST (doesn't depend on translations)
this._attachEventListeners();
console.log("[groupOrderShop] Event listeners attached");
@ -435,7 +442,18 @@
},
_loadCart: function () {
// Storage layout:
// eskaera_<id>_cart → items as plain {productId: {...}}
// (keeps the contract used by
// checkout_labels.js, home_delivery.js
// and _saveOrderDraft).
// eskaera_<id>_cart_cycle → "YYYY-MM-DD" the cart was saved at.
// Eviction only fires when we KNOW the current cycle and it
// disagrees with the stored one. If either side is missing
// (e.g. checkout page that doesn't call check-status, or XHR
// failure), we preserve the user's cart.
var cartKey = "eskaera_" + this.orderId + "_cart";
var cycleKey = "eskaera_" + this.orderId + "_cart_cycle";
var raw = localStorage.getItem(cartKey);
this.cart = {};
if (!raw) {
@ -448,27 +466,32 @@
} catch (e) {
console.warn("Cart load: invalid JSON in localStorage, dropping", e);
localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
return;
}
// Legacy / corrupt shape (raw items object without cycle stamp) → drop.
if (
!parsed ||
typeof parsed !== "object" ||
!parsed.items ||
!("cutoff_date" in parsed)
) {
console.log(
"Cart load: legacy/unstamped value, dropping localStorage[" + cartKey + "]"
);
if (!parsed || typeof parsed !== "object") {
localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
return;
}
// Cycle mismatch (or no current cycle known) → drop. This is the
// primary defence against stale carts surviving cycle rollover.
if (!this._currentCutoffDate || parsed.cutoff_date !== this._currentCutoffDate) {
// Migration: v18.0.1.10.0 wrapped the cart as {cutoff_date, items}
// in the same key, which broke checkout_labels.js / home_delivery.js
// / _saveOrderDraft (they iterated Object.keys treating them as
// productIds). Unwrap once; subsequent saves use the canonical
// split-key layout below.
var items;
var storedCycle;
if (parsed.items && "cutoff_date" in parsed) {
items = parsed.items;
storedCycle = parsed.cutoff_date;
} else {
items = parsed;
storedCycle = localStorage.getItem(cycleKey);
}
if (this._currentCutoffDate && storedCycle && storedCycle !== this._currentCutoffDate) {
console.log(
"Cart load: cycle mismatch (stored=" +
parsed.cutoff_date +
storedCycle +
", current=" +
this._currentCutoffDate +
"), dropping localStorage[" +
@ -476,14 +499,20 @@
"]"
);
localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
return;
}
this.cart = parsed.items;
this.cart = items;
// Re-canonicalize storage so other readers see the plain shape.
localStorage.setItem(cartKey, JSON.stringify(items));
if (storedCycle) {
localStorage.setItem(cycleKey, storedCycle);
}
console.log(
"Cart loaded from localStorage[" +
cartKey +
"] (cycle " +
parsed.cutoff_date +
(storedCycle || "unknown") +
"):",
this.cart
);
@ -491,11 +520,11 @@
_saveCart: function () {
var cartKey = "eskaera_" + this.orderId + "_cart";
var payload = JSON.stringify({
cutoff_date: this._currentCutoffDate || null,
items: this.cart,
});
localStorage.setItem(cartKey, payload);
var cycleKey = "eskaera_" + this.orderId + "_cart_cycle";
localStorage.setItem(cartKey, JSON.stringify(this.cart));
if (this._currentCutoffDate) {
localStorage.setItem(cycleKey, this._currentCutoffDate);
}
console.log(
"Cart saved to localStorage[" +
cartKey +
@ -508,11 +537,42 @@
_clearCurrentOrderCartSilently: function () {
var cartKey = "eskaera_" + this.orderId + "_cart";
var cycleKey = "eskaera_" + this.orderId + "_cart_cycle";
localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
this.cart = {};
console.log("[groupOrderShop] Cart cleared silently for order:", this.orderId);
},
// Avoid a reload loop if for any reason (HTML cached upstream, etc.)
// the build versions keep disagreeing after a reload.
_shouldReloadForNewBuild: function () {
try {
var raw = sessionStorage.getItem("eskaera_build_reload_at");
if (!raw) {
return true;
}
var last = parseInt(raw, 10);
if (isNaN(last)) {
return true;
}
// If we already reloaded in the last 30s, don't try again.
return Date.now() - last > 30000;
} catch (e) {
return true;
}
},
_markReloadAttempt: function () {
try {
sessionStorage.setItem("eskaera_build_reload_at", String(Date.now()));
} catch (e) {
// sessionStorage unavailable — fail open; worst case we get a
// single extra reload, never a loop because the next render
// will carry the new version.
}
},
_isClosedOrderResponse: function (xhr) {
if (!xhr) {
return false;
@ -555,6 +615,27 @@
}
try {
var data = JSON.parse(xhr.responseText || "{}");
// Deploy-detection: server tells us the installed module
// version; if it differs from the version embedded in
// the HTML, we are running stale JS — reload once.
if (
data &&
data.client_version &&
self._buildVersion &&
data.client_version !== self._buildVersion &&
self._shouldReloadForNewBuild()
) {
console.warn(
"[groupOrderShop] Build mismatch (page=" +
self._buildVersion +
", server=" +
data.client_version +
"), reloading"
);
self._markReloadAttempt();
window.location.reload();
return;
}
// Capture current cycle marker so _loadCart can evict a
// localStorage cart that belongs to a previous cycle.
self._currentCutoffDate = data && data.cutoff_date ? data.cutoff_date : null;

View file

@ -337,7 +337,7 @@
</button>
</div>
</div>
<div class="card-body cart-body-lg" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
<div class="card-body cart-body-lg" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" t-attf-data-build-version="{{ module_version }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
<p class="text-muted">This order's cart is empty</p>
</div>
<div class="card-footer bg-white d-flex justify-content-between align-items-center gap-2">