[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>
This commit is contained in:
GitHub Copilot 2026-06-06 15:37:55 +02:00
parent 6e6d1e5256
commit 9484f8c349
2 changed files with 46 additions and 22 deletions

View file

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

View file

@ -442,7 +442,18 @@
}, },
_loadCart: function () { _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 cartKey = "eskaera_" + this.orderId + "_cart";
var cycleKey = "eskaera_" + this.orderId + "_cart_cycle";
var raw = localStorage.getItem(cartKey); var raw = localStorage.getItem(cartKey);
this.cart = {}; this.cart = {};
if (!raw) { if (!raw) {
@ -455,27 +466,32 @@
} catch (e) { } catch (e) {
console.warn("Cart load: invalid JSON in localStorage, dropping", e); console.warn("Cart load: invalid JSON in localStorage, dropping", e);
localStorage.removeItem(cartKey); localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
return; return;
} }
// Legacy / corrupt shape (raw items object without cycle stamp) → drop. if (!parsed || typeof parsed !== "object") {
if (
!parsed ||
typeof parsed !== "object" ||
!parsed.items ||
!("cutoff_date" in parsed)
) {
console.log(
"Cart load: legacy/unstamped value, dropping localStorage[" + cartKey + "]"
);
localStorage.removeItem(cartKey); localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
return; return;
} }
// Cycle mismatch (or no current cycle known) → drop. This is the // Migration: v18.0.1.10.0 wrapped the cart as {cutoff_date, items}
// primary defence against stale carts surviving cycle rollover. // in the same key, which broke checkout_labels.js / home_delivery.js
if (!this._currentCutoffDate || parsed.cutoff_date !== this._currentCutoffDate) { // / _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( console.log(
"Cart load: cycle mismatch (stored=" + "Cart load: cycle mismatch (stored=" +
parsed.cutoff_date + storedCycle +
", current=" + ", current=" +
this._currentCutoffDate + this._currentCutoffDate +
"), dropping localStorage[" + "), dropping localStorage[" +
@ -483,14 +499,20 @@
"]" "]"
); );
localStorage.removeItem(cartKey); localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
return; 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( console.log(
"Cart loaded from localStorage[" + "Cart loaded from localStorage[" +
cartKey + cartKey +
"] (cycle " + "] (cycle " +
parsed.cutoff_date + (storedCycle || "unknown") +
"):", "):",
this.cart this.cart
); );
@ -498,11 +520,11 @@
_saveCart: function () { _saveCart: function () {
var cartKey = "eskaera_" + this.orderId + "_cart"; var cartKey = "eskaera_" + this.orderId + "_cart";
var payload = JSON.stringify({ var cycleKey = "eskaera_" + this.orderId + "_cart_cycle";
cutoff_date: this._currentCutoffDate || null, localStorage.setItem(cartKey, JSON.stringify(this.cart));
items: this.cart, if (this._currentCutoffDate) {
}); localStorage.setItem(cycleKey, this._currentCutoffDate);
localStorage.setItem(cartKey, payload); }
console.log( console.log(
"Cart saved to localStorage[" + "Cart saved to localStorage[" +
cartKey + cartKey +
@ -515,7 +537,9 @@
_clearCurrentOrderCartSilently: function () { _clearCurrentOrderCartSilently: function () {
var cartKey = "eskaera_" + this.orderId + "_cart"; var cartKey = "eskaera_" + this.orderId + "_cart";
var cycleKey = "eskaera_" + this.orderId + "_cart_cycle";
localStorage.removeItem(cartKey); localStorage.removeItem(cartKey);
localStorage.removeItem(cycleKey);
this.cart = {}; this.cart = {};
console.log("[groupOrderShop] Cart cleared silently for order:", this.orderId); console.log("[groupOrderShop] Cart cleared silently for order:", this.orderId);
}, },