/*! * Nuva Cart — Phase 6B * --------------------------------------------------------------------------- * The cart stores ONLY { productId, quantity } per line. It never stores a * remembered price or name — every read re-resolves those from * NuvaProductCatalogAdmin at call time, so a price never goes stale and a * product that becomes unavailable after being added is caught before * checkout rather than trusted from what the cart "remembers" (Part 43). * * Draft, inactive, unavailable, and (when disabled) placeholder products can * never be added — addItem() re-checks NuvaProductCatalogAdmin. * isProductPurchasable() every time, not just once. * * Browser persistence (localStorage) is a per-viewer convenience only — it * is never treated as authoritative, is wrapped in try/catch, and simply * doesn't survive if unavailable (private browsing, cleared storage, etc). */ (function (root) { 'use strict'; var CART_VERSION = '1.0'; var STORAGE_KEY = 'nuva_cart_items_v1'; var ITEMS = []; // [{ productId, quantity }] function loadFromStorage() { try { if (typeof localStorage === 'undefined') return; var raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; var parsed = JSON.parse(raw); if (Array.isArray(parsed)) { var valid = parsed.filter(function (i) { return i && typeof i.productId === 'string' && typeof i.quantity === 'number'; }); ITEMS.length = 0; Array.prototype.push.apply(ITEMS, valid); } } catch (e) { /* ignore — cart just starts empty */ } } function saveToStorage() { try { if (typeof localStorage === 'undefined') return; localStorage.setItem(STORAGE_KEY, JSON.stringify(ITEMS)); } catch (e) { /* ignore — in-memory cart still works this session */ } } function getAdmin() { var admin = root.NuvaProductCatalogAdmin; if (!admin) throw new Error('NuvaCart requires window.NuvaProductCatalogAdmin (Phase 6B) to be loaded first.'); return admin; } function findLine(productId) { for (var i = 0; i < ITEMS.length; i++) { if (ITEMS[i].productId === productId) return i; } return -1; } function addItem(productId, quantity) { quantity = typeof quantity === 'number' && quantity > 0 ? Math.floor(quantity) : 1; var admin = getAdmin(); if (!admin.isProductPurchasable(productId)) { return { ok: false, errors: ['This product is not currently available to add to your cart.'] }; } var idx = findLine(productId); if (idx === -1) ITEMS.push({ productId: productId, quantity: quantity }); else ITEMS[idx].quantity += quantity; saveToStorage(); return { ok: true, items: getItems() }; } function removeItem(productId) { var idx = findLine(productId); if (idx === -1) return { ok: false, errors: ['Item not in cart.'] }; ITEMS.splice(idx, 1); saveToStorage(); return { ok: true, items: getItems() }; } function increaseQuantity(productId, by) { by = typeof by === 'number' && by > 0 ? Math.floor(by) : 1; var idx = findLine(productId); if (idx === -1) return { ok: false, errors: ['Item not in cart.'] }; ITEMS[idx].quantity += by; saveToStorage(); return { ok: true, items: getItems() }; } function decreaseQuantity(productId, by) { by = typeof by === 'number' && by > 0 ? Math.floor(by) : 1; var idx = findLine(productId); if (idx === -1) return { ok: false, errors: ['Item not in cart.'] }; ITEMS[idx].quantity -= by; if (ITEMS[idx].quantity <= 0) ITEMS.splice(idx, 1); saveToStorage(); return { ok: true, items: getItems() }; } function clearCart() { ITEMS.length = 0; // mutate in place so _internals.ITEMS (a live reference) stays accurate saveToStorage(); return { ok: true, items: [] }; } /** * Re-resolves every stored line against the live catalog. Lines whose * product no longer exists at all are silently dropped (nothing to show). * Lines whose product exists but is no longer purchasable are KEPT but * marked available:false with no price, so checkout can warn the customer * instead of silently charging a stale price. */ function getItems() { var admin = getAdmin(); var resolved = []; var stillValid = []; ITEMS.forEach(function (line) { var product = admin.getProduct(line.productId); if (!product) return; // deleted entirely — drop silently stillValid.push(line); var available = admin.isProductPurchasable(line.productId); var unitPrice = available ? product.price : null; resolved.push({ productId: product.productId, name: product.name, imageUrl: product.imageUrl || '', quantity: line.quantity, unitPrice: unitPrice, currency: product.currency || 'NGN', lineTotal: available ? unitPrice * line.quantity : 0, available: available, isPlaceholder: !!product.isPlaceholder }); }); if (stillValid.length !== ITEMS.length) { ITEMS.length = 0; Array.prototype.push.apply(ITEMS, stillValid); saveToStorage(); } return resolved; } function getSubtotal() { return getItems().reduce(function (sum, item) { return sum + (item.available ? item.lineTotal : 0); }, 0); } function getTotalQuantity() { return getItems().reduce(function (sum, item) { return sum + item.quantity; }, 0); } function getUnavailableItems() { return getItems().filter(function (item) { return !item.available; }); } function isEmpty() { return getItems().length === 0; } loadFromStorage(); var api = { version: CART_VERSION, addItem: addItem, removeItem: removeItem, increaseQuantity: increaseQuantity, decreaseQuantity: decreaseQuantity, clearCart: clearCart, getItems: getItems, getSubtotal: getSubtotal, getTotalQuantity: getTotalQuantity, getUnavailableItems: getUnavailableItems, isEmpty: isEmpty, _internals: { ITEMS: ITEMS } }; root.NuvaCart = api; if (typeof module !== 'undefined' && module.exports) { module.exports = api; } })(typeof window !== 'undefined' ? window : this);