1import { 2 CART_REFRESH_EVENT, 3 addPlanToCart, 4 buildCartSnapshotWithProducts, 5 createEmptyCartSnapshot, 6 emitCartRefresh, 7 fetchCurrentCart, 8 getCartFromRefreshEvent, 9 getCartItemsCount, 10 getCartProducts, 11 savePlanCartPeriod, 12} from '../common/cart-client.js'; 13import { getCurrencyFromRoot, getRootData } from '../../client-utils.js'; 14import { showNotificationModal } from '../common/notification-modal.js'; 15import { addToCartEvent } from '../common/data-layer-events.js'; 16import { normalizePlanKey } from '../domain/plans-shared.js'; 17import { getSearchPlanCatalog } from './search-plan-catalog.js'; 18import { 19 buildSelectedPlanCartState, 20 buildStateFromCartProduct, 21 findSelectedPlanCartProduct, 22 readStoredSelectedPlanCart, 23 writeStoredSelectedPlanCart, 24} from './search-plan-cart-state.js'; 25import { 26 getSelectedSearchPlanId, 27 getSelectedSearchPlanPeriod, 28 setSelectedSearchPlanPeriod, 29 subscribeSelectedSearchPlanId, 30 subscribeSelectedSearchPlanPeriod, 31} from './search-plan-selection.js'; 32 33const cartSnapsh
33otSubscribers = new Set(); 34 35let searchPlanCartSnapshot = null; 36let currentSelectedPlanCart = null; 37let hydratePromise = null; 38let refreshEventSuppressed = false; 39let selectedPlanPeriodSyncInProgress = false; 40let cartSyncInitialized = false; 41 42const toPositiveNumber = (value) => { 43 const numeric = Number(value); 44 return Number.isFinite(numeric) && numeric > 0 ? numeric : null; 45}; 46 47const coerceCartSnapshot = (cart) => (cart && typeof cart === 'object' ? cart : createEmptyCartSnapshot()); 48 49const notifyCartSnapshotSubscribers = () => { 50 cartSnapshotSubscribers.forEach((listener) => { 51 listener(searchPlanCartSnapshot); 52 }); 53}; 54 55const setCurrentSelectedPlanCart = (value) => { 56 currentSelectedPlanCart = value || null; 57 writeStoredSelectedPlanCart(currentSelectedPlanCart); 58 return currentSelectedPlanCart; 59}; 60 61const getSelectedSearchPlanCart = () => currentSelectedPlanCart; 62 63const getSearchPlanCartSnapshot = () => searchPlanCartSnapshot; 64 65const subscribeSearchPlanCartSnapshot = (listener, { emitCurrent = false } = {}) => { 66 if (typeof listener !== 'function') { 67 return () => {}; 68 } 69 70 cartSnapshotSubscribers.add(listener); 71 if (emitCurrent) { 72 listener(searchPlanCartSnapshot); 73 } 74 75 return () => { 76 cartSnapshotSubscribers.delete(listener); 77 }; 78}; 79 80const emitSharedCartRefresh = () => { 81 refreshEventSuppressed = true; 82 try { 83 emitCartRefresh({ 84 cart: searchPlanCartSnapshot, 85 count: getCartItemsCount(searchPlanCartSnapshot), 86 item: currentSelectedPlanCart, 87 }); 88 } finally { 89 refreshEventSuppressed = false; 90 } 91}; 92 93const resolveSelectedPlanCartFromSnapshot = ({ preferredProductId = null, syncSelectedPeriod = false } = {}) => { 94 const selectedPlanId = getSelectedSearchPlanId(); 95 if (!selectedPlanId) { 96 setCurrentSelectedPlanCart(null); 97 return null; 98 } 99 100 const storedState = readStoredSelectedPlanCart(); 101 const { planKeysById } = getSearchPlanCatalog(); 102 const selectedPlanName = planKeysById.get(String(selectedPlanId)) || ''; 103 const matchedProduct = findSelectedPlanCartProduct(getCartProducts(searchPlanCartSnapshot), selectedPlanId, { 104 preferredProductId: preferredProductId ?? storedState?.id ?? null, 105 planName: selectedPlanName, 106 }); 107 const nextState = matchedProduct ? buildStateFromCartProduct(matchedProduct, selectedPlanId) : null; 108 109 setCurrentSelectedPlanCart(nextState); 110 111 if (syncSelectedPeriod && nextState?.orderPeriod && getSelectedSearchPlanPeriod() !== Number(nextState.orderPeriod)) { 112 setSelectedSearchPlanPeriod(nextState.orderPeriod); 113 } 114 115 return currentSelectedPlanCart; 116}; 117 118const commitSearchPlanCartSnapshot = ( 119 nextCart, 120 { emitRefresh = true, preferredProductId = null, syncSelectedPeriod = false } = {}, 121) => { 122 searchPlanCartSnapshot = coerceCartSnapshot(nextCart); 123 resolveSelectedPlanCartFromSnapshot({ 124 preferredProductId, 125 syncSelectedPeriod, 126 }); 127 notifyCartSnapshotSubscribers(); 128 129 if (emitRefresh) { 130 emitSharedCartRefresh(); 131 } 132 133 return searchPlanCartSnapshot; 134}; 135 136const findCartProductById = (productId) => { 137 const safeProductId = toPositiveNumber(productId); 138 if (!safeProductId) { 139 return null; 140 } 141 return getCartProducts(searchPlanCartSnapshot).find((product) => Number(product?.id) === safeProductId) || null; 142}; 143 144const mergeCartProductIntoSnapshot = (product, { appendIfMissing = true } = {}) => { 145 if (!product || typeof product !== 'object') { 146 return getCartProducts(searchPlanCartSnapshot); 147 } 148 149 const nextProductId = Number(product?.id); 150 const nextProducts = getCartProducts(searchPlanCartSnapshot).slice(); 151 const existingIndex = nextProducts.findIndex((entry) => Number(entry?.id) === nextProductId); 152 153 if (existingIndex >= 0) { 154 nextProducts[existingIndex] = product; 155 return nextProducts; 156 } 157 158 if (appendIfMissing) { 159 nextProducts.push(product); 160 } 161 162 return nextProducts; 163}; 164 165const syncSelectedPlanPeriodToCart = async () => { 166 if (selectedPlanPeriodSyncInProgress) { 167 return; 168 } 169 170 selectedPlanPeriodSyncInProgress = true; 171 172 try { 173 await whenSearchPlanCartReady(); 174 while (true) { 175 const selectedPlanId = getSelectedSearchPlanId(); 176 const desiredOrderPeriod = getSelectedSearchPlanPeriod(); 177 const selectedPlanCart = getSelectedSearchPlanCart(); 178 179 if (!selectedPlanId || !selectedPlanCart || selectedPlanCart.planId !== selectedPlanId) { 180 break; 181 } 182
183 const currentOrderPeriod = Number(selectedPlanCart.orderPeriod) || 1; 184 if (desiredOrderPeriod === currentOrderPeriod) { 185 break; 186 } 187 188 const selectedCartProductId = toPositiveNumber(selectedPlanCart.id); 189 const selectedCartConfigId = toPositiveNumber(selectedPlanCart?.shopping_cart_product_configuration?.id); 190 if (!selectedCartProductId || !selectedCartConfigId || !findCartProductById(selectedCartProductId)) { 191 resolveSelectedPlanCartFromSnapshot(); 192 break; 193 } 194 195 try { 196 const cart = await savePlanCartPeriod(selectedCartConfigId, desiredOrderPeriod); 197 commitSearchPlanCartSnapshot(cart, { preferredProductId: selectedCartProductId }); 198 const confirmedProduct = getCartProducts(cart).find( 199 (product) => Number(product?.shopping_cart_product_configuration?.id) === selectedCartConfigId, 200 ); 201 if ( 202 confirmedProduct && 203 Number(confirmedProduct.shopping_cart_product_configuration.product_configuration?.order_period) !== 204 desiredOrderPeriod 205 ) { 206 throw new Error('The selected plan period was not confirmed by the cart'); 207 } 208 } catch (error) { 209 console.error('Failed to update selected plan period:', error); 210 // Keep the last confirmed period and stop. A new user selection can retry. 211 resolveSelectedPlanCartFromSnapshot({ syncSelectedPeriod: true }); 212 showNotificationModal({ mode: 'error' }); 213 break; 214 } 215 } 216 } finally { 217 selectedPlanPeriodSyncInProgress = false; 218 } 219}; 220 221const addSelectedSearchPlanToCart = async (planId, { orderPeriod = getSelectedSearchPlanPeriod() } = {}) => { 222 await whenSearchPlanCartReady(); 223 224 const normalizedPlanId = String(planId || '').trim(); 225 const safeOrderPeriod = Number.isFinite(Number(orderPeriod)) && Number(orderPeriod) > 0 ? Number(orderPeriod) : 1; 226 if (!normalizedPlanId) { 227 return null; 228 } 229 230 const { plansById } = getSearchPlanCatalog(); 231 const nextPlanEntry = plansById.get(normalizedPlanId); 232 const nextPlanName = normalizePlanKey(nextPlanEntry?.plan?.name); 233 if (!nextPlanEntry || !nextPlanName) { 234 return null; 235 } 236 237 const cartIndex = getCartItemsCount(searchPlanCartSnapshot); 238 const response = await addPlanToCart(nextPlanName, safeOrderPeriod); 239 addToCartEvent({ 240 price: response?.price, 241 name: nextPlanName, 242 index: cartIndex, 243 currency: getCurrencyFromRoot(getRootData()) || 'EUR', 244 }); 245 const confirmedState = 246 buildSelectedPlanCartState({ 247 response, 248 planId: normalizedPlanId, 249 planName: nextPlanName, 250 orderPeriod: safeOrderPeriod, 251 price: response?.price, 252 priceWithoutDiscount: response?.price_without_discount, 253 }) || buildStateFromCartProduct(response, normalizedPlanId); 254 255 commitSearchPlanCartSnapshot( 256 buildCartSnapshotWithProducts(mergeCartProductIntoSnapshot(response), searchPlanCartSnapshot), 257 { 258 preferredProductId: getSelectedSearchPlanId() === normalizedPlanId ? (confirmedState?.id ?? null) : null, 259 }, 260 ); 261 262 if (getSelectedSearchPlanId() === normalizedPlanId && getSelectedSearchPlanPeriod() !== safeOrderPeriod) { 263 setSelectedSearchPlanPeriod(safeOrderPeriod); 264 } 265 266 return confirmedState; 267}; 268 269const whenSearchPlanCartReady = async () => { 270 if (!hydratePromise) { 271 return searchPlanCartSnapshot; 272 } 273 274 await hydratePromise; 275 return searchPlanCartSnapshot; 276}; 277 278const initSearchPlanCartSync = () => { 279 if (cartSyncInitialized || typeof document === 'undefined') { 280 return hydratePromise; 281 } 282 cartSyncInitialized = true; 283 284 document.addEventListener(CART_REFRESH_EVENT, (event) => { 285 if (refreshEventSuppressed) { 286 return; 287 } 288 289 const cart = getCartFromRefreshEvent(event); 290 if (!cart) { 291 return; 292 } 293 294 commitSearchPlanCartSnapshot(cart, { 295 emitRefresh: false, 296 syncSelectedPeriod: !selectedPlanPeriodSyncInProgress, 297 }); 298 }); 299 300 subscribeSelectedSearchPlanId(() => { 301 resolveSelectedPlanCartFromSnapshot({ 302 syncSelectedPeriod: true, 303 }); 304 }); 305 306 subscribeSelectedSearchPlanPeriod(() => { 307 void syncSelectedPlanPeriodToCart(); 308 }); 309 310 hydratePromise = fetchCurrentCart() 311 .then((cart) => { 312 commitSearchPlanCartSnapshot(cart, { 313 syncSelectedPeriod: true, 314 }); 315 return searchPlanCartSnapshot; 316 }) 317 .catch((error) => { 318 console.error('Failed to hydrate selected plan cart state:', error); 319 commitSearchPlanCartSnapshot(createEmptyCartSnapshot(), { 320 syncSelectedPeriod: false, 321 }); 322 return searchPlanCartSnapshot; 323 }); 324 325 return hydratePromise; 326}; 327 328if (typeof document !== 'undefined') { 329 initSearchPlanCartSync(); 330} 331 332export { 333 initSearchPlanCartSync,
334 whenSearchPlanCartReady, 335 getSearchPlanCartSnapshot, 336 subscribeSearchPlanCartSnapshot, 337 getSelectedSearchPlanCart, 338 addSelectedSearchPlanToCart, 339};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.