PageSourceSearch

https://jmwedding2020.com/assets/js/stock-validator-vanilla.js

js jmwedding2020.com collected 2026-10-10 00:28:44 UTC 79,496 bytes, 1,788 lines download raw bytes

1/**
2 * Stock Validation - Vanilla JS Version
3 * 
4 * This script provides client-side stock validation without jQuery dependency
5 * - Works with native JavaScript and fetch API
6 * - Graceful fallback for older browsers
7 * - Same functionality as jQuery version
8 */
9
10window.StockValidator = (function() {
11    'use strict';
12
13    const config = {
14        apiEndpoint: '/api/stock/check.php',
15        modalSelector: '#stock-modal-overlay',  // We create our own modal
16        loadingClass: 'loading-stock',
17        disabledClass: 'stock-disabled',
18        debug: true  // Enable debug logging
19    };
20
21    /**
22     * Cross-browser event listener helper
23     */
24    function addEventListener(element, event, handler) {
25        if (element.addEventListener) {
26            element.addEventListener(event, handler);
27        } else if (element.attachEvent) {
28            element.attachEvent('on' + event, handler);
29        }
30    }
31
32    /**
33     * Get stock information from embedded variant data (instant, no AJAX needed)
34     */
35    function getStockFromEmbeddedData(variantId) {
36        // Check if embedded variant data exists
37        if (!window.__PD_VARIANTS_WITH_BRANCH__ || !Array.isArray(window.__PD_VARIANTS_WITH_BRANCH__)) {
38            return null;
39        }
40        
41        const variants = window.__PD_VARIANTS_WITH_BRANCH__;
42        
43        // SPECIAL CASE: Simple product (no variants) - empty array means simple product
44        if (variants.length === 0) {
45            // For simple products, we cannot determine stock from embedded data
46            // Return null to force API validation
47            return null;
48        }
49        
50        // If no variant specified, check base product stock (all variants)
51        if (!variantId || variantId <= 0) {
52            // For base product, check if ANY variant has stock
53            const isPreorder = window.isPreorderProduct || false;
54            let hasAnyStock = false;
55            let totalStock = 0;
56            
57            if (isPreorder) {
58                // For pre-order, check preorder_remaining
59                hasAnyStock = variants.some(v => {
60                    if ('preorder_remaining' in v && v.preorder_remaining !== null) {
61                        return v.preorder_remaining > 0;
62                    }
63                    return v.stock > 0;
64                });
65                totalStock = variants.reduce((sum, v) => {
66                    if ('preorder_remaining' in v && v.preorder_remaining !== null) {
67                        return sum + (v.preorder_remaining || 0);
68                    }
69                    return sum + (v.stock || 0);
70                }, 0);
71            } else {
72                hasAnyStock = variants.some(v => v.stock > 0);
73                totalStock = variants.reduce((sum, v) => sum + (v.stock || 0), 0);
74            }
75            
76            return {
77                hasStock: hasAnyStock,
78                stock: totalStock,
79                sku: 'Base Product',
80                variant: null
81            };
82        }
83        
84        // Find specific variant
85        const variant = variants.find(v => v.id === variantId);
86        
87        if (!variant) {
88            return {
89                hasStock: false,
90                stock: 0,
91                sku: 'Unknown Variant',
92                variant: null
93            };
94        }
95        
96        // For pre-order products, check preorder_remaining instead of stock
97        const isPreorder = window.isPreorderProduct || false;
98        let hasStock = false;
99        let availableStock = 0;
100        
101        if (isPreorder && 'preorder_remaining' in variant && variant.preorder_remaining !== null) {
102            availableStock = parseInt(variant.preorder_remaining, 10);
103            if (isNaN(availableStock)) availableStock = 0;
104            hasStock = availableStock > 0;
105        } else {
106            availableStock = variant.stock || 0;
107            hasStock = availableStock > 0;
108        }
109        
110        return {
111            hasStock: hasStock,
112            stock: availableStock,
113            sku: variant.sku,
114            variant: variant
115        };
116    }
117
118    /**
119     * Cross-browser AJAX helper
120     */
121    function makeRequest(url, options) {
122        return new Promise((resolve, reject) => {
123            // Try fetch first (modern browsers)
124            if (typeof fetch !== 'undefined') {
125                const fetchOptions = {
126                    method: options.method || 'POST',
127                    headers: {
128                        'Content-Type': 'application/x-www-form-urlencoded',
129                    },
130                    body: options.data ? new URLSearchParams(options.data).toString() : null
131                };
132
133                fetch(url, fetchOptions)
134                    .then(response => {
135                        if (!response.ok) {
136                            throw new Error(`HTTP ${response.status}`);
137                        }
138                        return response.json();
139                    })
140                    .then(resolve)
141                    .catch(reject);
142            } else {
143                // Fallback to XMLHttpRequest
144                const xhr = new XMLHttpRequest();
145                xhr.open(options.method || 'POST', url);
146                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
147                
148                xhr.onload = function() {
149                    if (xhr.status >= 200 && xhr.status < 300) {
150                        try {
151                            resolve(JSON.parse(xhr.responseText));
152                        } catch (e) {
153                            reject(new Error('Invalid JSON response'));
154                        }
155                    } else {
156                        reject(new Error(`HTTP ${xhr.status}`));
157                    }
158                };
159                
160                xhr.onerror = function() {
161                    reject(new Error('Network error'));
162                };
163                
164                const data = options.data ? new URLSearchParams(options.data).toString() : null;
165                xhr.send(data);
166            }
167        });
168    }
169
170    /**
171     * Check stock via AJAX
172     */
173    function checkStock(productId, variantId, qty) {
174        return makeRequest(config.apiEndpoint, {
175            method: 'POST',
176            data: {
177                product_id: productId,
178                variant_id: variantId || 0,
179                qty: qty || 1
180            }
181        });
182    }
183
184    /**
185     * Create and show stock alert modal using modal.css structure
186     */
187    function showStockModal(stockInfo, action = 'add to cart') {
188        let title, message, type, iconClass;
189
190        // Handle both string and object formats
191        if (typeof stockInfo === 'string') {
192            title = 'Stok Tidak Tersedia';
193            message = stockInfo;
194            type = 'warning';
195            iconClass = 'warning';
196        } else if (stockInfo && typeof stockInfo === 'object') {
197            if (!stockInfo.is_active) {
198                title = 'Produk Tidak Tersedia';
199                message = 'Maaf, produk ini sudah tidak aktif atau telah dihapus dari katalog.';
200                type = 'danger';
201                iconClass = 'danger';
202            } else if (!stockInfo.is_in_stock) {
203                title = 'Stok Habis';
204                message = `<strong>${stockInfo.product_name || 'Produk ini'}</strong> sedang habis stok. Silakan pilih varian lain atau tunggu restock.`;
205                if (stockInfo.sku) {
206                    message += `<br><small style="color: #64748b;">SKU: ${stockInfo.sku}</small>`;
207                }
208                type = 'warning';
209                iconClass = 'warning';
210            } else if (!stockInfo.can_add_to_cart) {
211                title = 'Stok Tidak Mencukupi';
212                message = `Stok <strong>${stockInfo.product_name || 'produk'}</strong> tidak mencukupi.<br>`;
213                message += `Tersedia: <strong>${stockInfo.available_stock}</strong>, diminta: <strong>${stockInfo.requested_qty}</strong><br>`;
214                message += `Silakan kurangi jumlah atau pilih varian lain.`;
215                type = 'warning';
216                iconClass = 'warning';
217            } else {
218                title = 'Stok Tidak Tersedia';
219                message = stockInfo.message || 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.';
220                type = 'warning';
221                iconClass = 'warning';
222            }
223        } else {
224            title = 'Stok Tidak Tersedia';
225            message = 'Produk ini tidak dapat ditambahkan ke keranjang saat ini.';
226            type = 'warning';
227            iconClass = 'warning';
228        }
229
230        // Create modal HTML using modal.css structure
231        const modalHTML = `
232            <div class="modal-overlay" id="stock-modal-overlay">
233                <div class="modal-dialog">
234                    <div class="modal-header">
235                        <div class="modal-icon ${iconClass}">
236                            ${getIconSVG(iconClass)}
237                        </div>
238                        <h3 class="modal-title">${title}</h3>
239                    </div>
240                    <div class="modal-body">
241                        <p class="modal-message">${message}</p>
242                        <div class="modal-actions">
243                            <button type="button" class="modal-btn modal-btn-secondary" data-action="close">
244                                Tutup
245                            </button>
246                            <button type="button" class="modal-btn modal-btn-primary" data-action="reload">
247                                Muat Ulang
248                            </button>
249                        </div>
250                    </div>
251                </div>
252            </div>
253        `;
254
255        // Remove existing modal if any
256        const existingModal = document.getElementById('stock-modal-overlay');
257        if (existingModal) {
258            existingModal.remove();
259        }
260
261        // Add modal to body
262        document.body.insertAdjacentHTML('beforeend', modalHTML);
263        
264        // Show modal with animation
265        const modal = document.getElementById('stock-modal-overlay');
266        setTimeout(() => {
267            modal.classList.add('show');
268        }, 10);
269
270        // Add event listeners to modal buttons
271        const closeBtn = modal.querySelector('[data-action="close"]');
272        const reloadBtn = modal.querySelector('[data-action="reload"]');
273        
274        if (closeBtn) {
275            addEventListener(closeBtn, 'click', closeStockModal);
276        }
277        
278        if (reloadBtn) {
279            addEventListener(reloadBtn, 'click', function() {
280                closeStockModal();
281                window.location.reload();
282            });
283        }
284
285        // Close modal when clicking overlay
286        addEventListener(modal, 'click', function(event) {
287            if (event.target === modal) {
288                closeStockModal();
289            }
290        });
291
292        // Auto-close after 8 seconds
293        setTimeout(() => {
294            closeStockModal();
295        }, 8000);
296    }
297
298    /**
299     * Get icon SVG based on type
300     */
301    function getIconSVG(type) {
302        const icons = {
303            danger: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>',
304            warning: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
304<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>',
305            success: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>',
306            info: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>'
307        };
308        return icons[type] || icons.info;
309    }
310
311    /**
312     * Close stock modal
313     */
314    function closeStockModal() {
315        const modal = document.getElementById('stock-modal-overlay');
316        if (modal) {
317            modal.classList.remove('show');
318            setTimeout(() => {
319                modal.remove();
320            }, 200);
321        }
322    }
323
324    // Make closeStockModal globally available
325    if (typeof window !== 'undefined') {
326        window.closeStockModal = closeStockModal;
327    }
328
329    /**
330     * Get element data attribute with fallback
331     */
332    function getDataAttribute(element, attr) {
333        if (element.dataset) {
334            return element.dataset[attr];
335        } else {
336            return element.getAttribute('data-' + attr);
337        }
338    }
339
340    /**
341     * Validate stock before cart action
342     */
343    async function validateBeforeCart(productId, variantId, qty, action = 'add') {
344        try {
345            // SMART VALIDATION: For simple products, check if we already have cached stock info
346            // If buttons are enabled and cached info shows stock available, skip API validation
347            if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart) {
348        
349                return true;
350            }
351            
352            // For variant products or when cached info is not available, do API validation
353            const stockInfo = await checkStock(productId, variantId, qty);
354            
355            if (!stockInfo.ok) {
356                showStockModal({ 
357                    message: stockInfo.error || 'Gagal mengecek stok',
358                    is_active: false 
359                }, action);
360                return false;
361            }
362
363            if (!stockInfo.can_add_to_cart) {
364                showStockModal(stockInfo, action);
365                return false;
366            }
367
368            return true; // Stock available, proceed
369
370        } catch (error) {
371            console.error('Stock validation error:', error);
372            showStockModal({ 
373                message: 'Terjadi kesalahan saat mengecek stok. Silakan coba lagi.',
374                is_active: false 
375            }, action);
376            return false;
377        }
378    }
379
380    /**
381     * Handle button click with stock validation
382     */
383    function handleButtonClick(event, action) {
384        const button = event.target;
385        
386        
387        // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering)
388        if (button.hasAttribute('data-stock-blocked')) {
389            event.preventDefault();
390            event.stopImmediatePropagation();
391            
392            const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia';
393            showStockModal(reason);
394            
395            // Show security warning
396            if (window.showToast) {
397                window.showToast({
398                    type: 'warning',
399                    title: 'Aksi Tidak Diizinkan',
400                    message: 'Produk ini sedang habis stok. Silakan pilih varian lain.',
401                    duration: 3000
402                });
403            }
404            return false;
405        }
406        
407        // SECOND CHECK: Real-time stock validation against cached stock info
408        if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) {
409            event.preventDefault();
410            event.stopImmediatePropagation();
411            
412            showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia');
413            
414            // Re-enforce button disabling
415            button.disabled = true;
416            button.style.opacity = '0.5';
417            button.style.cursor = 'not-allowed';
418            button.setAttribute('data-stock-blocked', 'true');
419            
420            return false;
421        }
422        
423        // Skip if already processing
424        if (button.classList.contains(config.loadingClass)) {
425            event.preventDefault();
426            return false;
427        }
428
429        // Get product data from button attributes or form
430        let productId = parseInt(getDataAttribute(button, 'productId') || getDataAttribute(button, 'product-id') || 0);
431        let variantId = parseInt(getDataAttribute(button, 'variantId') || getDataAttribute(button, 'variant-id') || 0);
432        let qty = parseInt(getDataAttribute(button, 'qty') || 1);
433
434        // Try to get from global variables (set by product detail page)
435        if (productId <= 0 && typeof window.PD_ID !== 'undefined') {
436            productId = parseInt(window.PD_ID || 0);
437        }
438
439        // Try to get from parent form if not in button
440        if (productId <= 0) {
441            const form = button.closest ? button.closest('form') : null;
442            if (form) {
443                const pidAttr = form.getAttribute('data-pid');
444                const pidInput = form.querySelector('[name="product_id"]');
445                const vidInput = form.querySelector('[name="variant_id"]');
446                const qtyInput = form.querySelector('[name="qty"]');
447                
448                if (pidAttr) productId = parseInt(pidAttr);
449                if (pidInput) productId = parseInt(pidInput.value || 0);
450                if (vidInput) variantId = parseInt(vidInput.value || 0);
451                if (qtyInput) qty = parseInt(qtyInput.value || 1);
452            }
453        }
454
455        // CRITICAL: Always get current variant from selector (overrides button attributes)
456        // This ensures we validate against the currently selected variant, not cached button data
457        const variantSelector = document.getElementById('pd-variant-id');
458        if (variantSelector) {
459            const currentVariantId = parseInt(variantSelector.value || 0);
460            if (currentVariantId > 0) {
461                variantId = currentVariantId;
462            }
463        }
464
465
466        if (productId <= 0) {
467            return true; // Let default handler deal with it
468        }
469
470        // Prevent default and validate stock
471        event.preventDefault();
472        event.stopPropagation();
473        
474        // Show loading state
475        button.classList.add(config.loadingClass);
476        const originalDisabled = button.disabled;
477        button.disabled = true;
478        
479        
480        validateBeforeCart(productId, variantId, qty, action)
481            .then(isValid => {
482                if (isValid) {
483                    // Stock OK, proceed with original action
484                    if (action === 'cart' && typeof window.addToCart === 'function') {
485                        window.addToCart(productId, qty, variantId);
486                    } else if (action === 'buy' && typeof window.buyNow === 'function') {
487                        // Call buyNow from pd-buynow.js without parameters (it gets data internally)
488                        window.buyNow();
489                    } else {
490                        // Add fallback cart/buy logic here if needed
491                    }
492                }
493            })
494            .catch(error => {
495            })
496            .finally(() => {
497                // Remove loading state
498                button.classList.remove(config.loadingClass);
499                button.disabled = originalDisabled;
500            });
501
502        return false;
503    }
504
505    /**
506     * Handle mobile button click with stock validation
507     */
508    function handleMobileButtonClick(event, action) {
509        const button = event.target;
510        
511        
512        // FIRST CHECK: Is button marked as stock-blocked? (Anti-tampering)
513        if (button.hasAttribute('data-stock-blocked')) {
514            event.preventDefault();
515            event.stopImmediatePropagation();
516            
517            const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia';
518            showStockModal(reason);
519            
520            // Show security warning
521            if (window.showToast) {
522                window.showToast({
523                    type: 'warning',
524                    title: 'Aksi Tidak Diizinkan',
525                    message: 'Produk ini sedang habis stok. Silakan pilih varian lain.',
526                    duration: 3000
527                });
528            }
529            return false;
530        }
531        
532        // SECOND CHECK: Real-time stock validation against cached stock info
533        if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) {
534            event.preventDefault();
535            event.stopImmediatePropagation();
536            
537            showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia');
538            
539            // Re-enforce button disabling
540            button.disabled = true;
541            button.style.opacity = '0.5';
542            button.style.cursor = 'not-allowed';
543            button.setAttribute('data-stock-blocked', 'true');
544            
545            return false;
546        }
547        
548        // Skip if already processing
549        if (button.classList.contains(config.loadingClass)) {
550            event.preventDefault();
551            return false;
552        }
553
554        // Get product data from mobile FAB container
555        let productId = 0;
556        let variantId = 0;
557        let qty = 1;
558
559        // Try to get from mobile FAB data-pid
560        const mobileFab = document.getElementById('pd-mobile-fab');
561        if (mobileFab) {
562            const pidAttr = getDataAttribute(mobileFab, 'pid');
563            if (pidAttr) productId = parseInt(pidAttr);
564        }
565
566        // Try to get from global variables (set by product detail page)
567        if (productId <= 0 && typeof window.PD_ID !== 'undefined') {
568            productId = parseInt(window.PD_ID || 0);
569        }
570
571        // CRITICAL: Always get current variant from selector (real-time)
572        // This ensures we validate against the currently selected variant
573        const variantSelector = document.getElementById('pd-variant-id');
574        if (variantSelector) {
575            variantId = parseInt(variantSelector.value || 0);
576        }
577
578        // Try to get quantity from quantity input
579        const qtySelector = document.querySelector('[name="qty"]');
580        if (qtySelector) {
581            qty = parseInt(qtySelector.value || 1);
582        }
583        
584        // Ensure minimum quantity is 1
585        qty = Math.max(1, qty);
586
587
588        if (productId <= 0) {
589            return true; // Let default handler deal with it
590        }
591
592        // Event already prevented at interceptor level, just proceed with validation
593        
594        // Show loading state
595        button.classList.add(config.loadingClass);
596        const originalDisabled = button.disabled;
597        const originalText = button.textContent;
598        button.disabled = true;
599        button.textContent = action === 'cart' ? 'Validasi...' : 'Validasi...';
600        
601        
602        // PREEMPTIVE BLOCKING: Set blocking flags BEFORE validation to prevent race conditions
603        if (window.addToCart && typeof window.addToCart === 'function') {
604            window.addToCart._stockValidatorBlocked = true;
605        }
606        
607        // Also preemptively block other cart functions
608        const originalAddToCartSubmit = window.addToCartSubmit;
609        const originalPdAddToCartSubmit = window._pdAddToCartSubmit;
610        
611        // Temporarily block ALL cart functions during validation
612        window.addToCartSubmit = (...args) => {
613            return Promise.resolve(false);
614        };
615        
616        if (originalPdAddToCartSubmit) {
617            window._pdAddToCartSubmit = (...args) => {
618                return Promise.resolve(false);
619            };
620        }
621        
622        validateBeforeCart(productId, variantId, qty, action)
623            .then(isValid => {
624                if (isValid) {
625                    // Stock OK, proceed with original action
626                    
627                    if (action === 'cart') {
628                        // For mobile cart, we need to recreate the original behavior
629                        // Strategy: Try multiple methods to ensure cart add + toast success
630                        
631                        
632                        let cartAddSuccess = false;
633                        let errorMessage = '';
634                        
635                        // Method 1: Try original addToCart function
636                        const originalAddToCart = window.addToCart && window.addToCart._original;
637                        if (originalAddToCart && !cartAddSuccess) {
638                            try {
639                                const result = originalAddToCart(productId, qty, variantId);
640                                
641                                // Handle both sync and async returns
642                                if (result && typeof result.then === 'function') {
643                                    result.then(() => {
644                                        cartAddSuccess = true;
645                                    }).catch(err => {
646                                        errorMessage = err.message || 'addToCart failed';
647                                    });
648                                } else {
649                                    cartAddSuccess = true;
650                                }
651                            } catch (error) {
652                                errorMessage = error.message || 'addToCart exception';
653                            }
654                        }
655                        
656                        // Method 2: Try pd-addtocart.js function (backup method)
657                        if (typeof window._pdAddToCartSubmit === 'function' && !cartAddSuccess) {
658                            try {
659                                
660                                // Create fake form for pd-addtocart.js
661                                const fakeForm = document.createElement('form');
662                                const qtyInput = document.createElement('input');
663                                qtyInput.name = 'qty';
664                                qtyInput.value = qty;
665                                fakeForm.appendChild(qtyInput);
666                                
667                                // Set variant selector value for pd-addtocart to read
668                                const variantSelector = document.getElementById('pd-variant-id');
669                                if (variantSelector) {
670                                    variantSelector.value = variantId;
671                                }
672                                
673                                const fakeEvent = { 
674                                    preventDefault: () => {},
675                                    target: fakeForm
676                                };
677                                
678                                // Call pd-addtocart.js function (this should handle toast internally)
679                                const result = window._pdAddToCartSubmit(fakeEvent, productId);
680                                if (result && typeof result.then === 'function') {
681                                    result.then(() => {
682                                        cartAddSuccess = true;
683                                    });
684                                } else {
685                                    cartAddSuccess = true;
686                                }
687                            } catch (error) {
688                            }
689                        }
690                        
691                        // Method 3: Direct addToCart call (fallback)
692                        if (typeof window.addToCart === 'function' && !cartAddSuccess) {
693                            try {
694                                const result = window.addToCart(productId, qty, variantId);
695                                if (result && typeof result.then === 'function') {
696                                    result.then(() => {
697                                        cartAddSuccess = true;
698                                    });
699                                } else {
700                                    cartAddSuccess = true;
701                                }
702                            } catch (error) {
703                            }
704                        }
705                        
706                        // Always show success toast for mobile (regardless of which method worked)
707                        // This ensures user gets feedback for successful cart addition
708                        setTimeout(() => {
709                            if (window.showToast) {
710                                
711                                // Get product details for toast
712                                const getVariantSummaryText = () => {
713                                    const vs = document.getElementById('pd-variant-summary');
714                                    if (!vs) return '';
715                                    const items = Array.from(vs.querySelectorAll('.vs-item strong'))
716                                        .map(el => el.textContent.trim())
717                                        .filter(Boolean);
718                                    return items.length ? items.join(' / ') : '';
719                                };
720                                
721                                const escapeHtml = (s) => String(s||'').replace(/[&<>"']/g, m=>({
722                                    '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'
723                                }[m]));
724                                
725                                const variantText = getVariantSummaryText();
726                                const skuText = (function(){ 
727                                    const el = document.getElementById('pd-sku'); 
728                                    return el ? el.textContent.trim() : (window.PD_SKU || 'N/A'); 
729                                })();
730                                const productName = window.PD_NAME || 'Product';
731                                
732                                window.showToast({
733                                    type: 'success',
734                                    title: 'Berhasil ditambahkan ke Keranjang',
735                                    message: `<strong>${escapeHtml(productName)}</strong>${variantText ? ` · <em>${escapeHtml(variantText)}</em>` : ''}<br>SKU: ${escapeHtml(skuText)}
735 · Qty: ${qty}`,
736                                    duration: 5000,
737                                    actions: [
738                                        { 
739                                            label: 'Lihat Keranjang', 
740                                            onClick: () => { window.location.href = '/keranjang'; }
741                                        },
742                                        { 
743                                            label: 'Lanjut Belanja', 
744                                            variant: 'alt', 
745                                            onClick: () => {} 
746                                        }
747                                    ]
748                                });
749                            } else {
750                            }
751                        }, 100); // Small delay to ensure cart operation completes first
752                        
753                    } else if (action === 'buy') {
754                        // For mobile buy, call buyNow
755                        if (typeof window.buyNow === 'function') {
756                            window.buyNow();
757                        }
758                    }
759                } else {
760                    // Stock validation FAILED - absolutely NO cart function calls allowed
761                    
762                    // DO NOT CALL ANY CART FUNCTIONS - only show modal (already handled by validateBeforeCart)
763                }
764                
765                // Always restore functions after validation (success or failure)
766                setTimeout(() => {
767                    // Remove blocking flag
768                    if (window.addToCart && typeof window.addToCart === 'function') {
769                        window.addToCart._stockValidatorBlocked = false;
770                    }
771                    
772                    // Restore other functions
773                    if (originalAddToCartSubmit && typeof originalAddToCartSubmit === 'function') {
774                        window.addToCartSubmit = originalAddToCartSubmit;
775                    }
776                    if (originalPdAddToCartSubmit && typeof originalPdAddToCartSubmit === 'function') {
777                        window._pdAddToCartSubmit = originalPdAddToCartSubmit;
778                    }
779                    
780                }, 1000); // Shorter timeout since we want to restore quickly after success
781            })
782            .catch(error => {
783                // On error, also block the action
784            })
785            .finally(() => {
786                // Remove loading state
787                button.classList.remove(config.loadingClass);
788                button.disabled = originalDisabled;
789                button.textContent = originalText;
790            });
791
792        return false;
793    }
794
795    /**
796     * Intercept cart buttons
797     */
798    function interceptCartButtons() {
799
800        
801        // CRITICAL: Remove existing mobile cart handlers from product-detail.php
802        setTimeout(() => {
803            const mobileCartBtn = document.getElementById('mf-cart');
804            if (mobileCartBtn) {
805                // Clone the button to remove ALL existing event listeners
806                const newBtn = mobileCartBtn.cloneNode(true);
807                mobileCartBtn.parentNode.replaceChild(newBtn, mobileCartBtn);
808
809                
810                // Add our own handler to the new button
811                newBtn.addEventListener('click', function(event) {
812                    event.preventDefault();
813                    event.stopPropagation();
814                    event.stopImmediatePropagation();
815                    handleMobileButtonClick(event, 'cart');
816                    return false;
817                }, true);
818            }
819        }, 50);
820        
821        // Note: Add to cart buttons are now handled via form submission override (addToCartSubmit)
822        // This provides better integration with existing pd-addtocart.js toast system
823
824        // Buy now buttons - updated selectors (desktop + mobile)
825        addEventListener(document, 'click', function(event) {
826            const target = event.target;
827            
828            // Desktop buy buttons
829            if (target.classList.contains('btn-buy') ||           // Product detail buy button
830                target.classList.contains('btn-buy-now') || 
831                target.classList.contains('buy-now-btn') || 
832                target.id === 'btn-buy' ||                        // Specific ID match
833                getDataAttribute(target, 'action') === 'buy-now' ||
834                getDataAttribute(target, 'action') === 'buy') {
835                
836                handleButtonClick(event, 'buy');
837            }
838            
839            // Mobile buy buttons
840            else if (target.classList.contains('mf-buy') ||       // Mobile fab buy button
841                     target.id === 'mf-buy') {                   // Mobile buy button ID
842                
843                handleMobileButtonClick(event, 'buy');
844            }
845            
846            // Mobile cart buttons - ENHANCED BLOCKING
847            else if (target.classList.contains('mf-cart') ||      // Mobile fab cart button
848                     target.id === 'mf-cart') {                  // Mobile cart button ID
849                
850                
851                // Immediately prevent all event propagation
852                event.preventDefault();
853                event.stopPropagation();
854                event.stopImmediatePropagation();
855                
856                handleMobileButtonClick(event, 'cart');
857                
858                // Return false to ensure no other handlers run
859                return false;
860            }
861        }, true);
861 // Use capturing phase to ensure we intercept before other handlers
862        
863        // Additional protection: Override addToCart function to add mobile button state checking
864        const originalAddToCart = typeof window.addToCart === 'function' ? window.addToCart : null;
865        if (originalAddToCart) {
866            window.addToCart = async function(...args) {
867                // Check if this call is coming from a mobile button that's currently being validated
868                const mobileCartBtn = document.getElementById('mf-cart');
869                if (mobileCartBtn && mobileCartBtn.classList.contains('loading-stock')) {
870                    return Promise.reject(new Error('Mobile validation in progress - cart action blocked'));
871                }
872                
873                // Check if functions are temporarily blocked (during stock failure)
874                if (window.addToCart._stockValidatorBlocked) {
875                    return Promise.reject(new Error('Stock validation failed - cart action blocked'));
876                }
877                
878                // Allow normal calls
879                return originalAddToCart.apply(this, args);
880            };
881            
882            // Store reference to original for restoration
883            window.addToCart._original = originalAddToCart;
884        }
885    }
886
887    /**
888     * Setup global click interceptor for security
889     * This catches ALL clicks on cart/buy buttons and validates stock BEFORE any other handler
890     */
891    function setupGlobalClickInterceptor() {
892        // Add capture-phase listener to catch clicks before other handlers
893        document.addEventListener('click', function(event) {
894            const target = event.target;
895            
896            // Check if this is a cart/buy button
897            const isCartButton = target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') ||
898                                target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart');
899            const isBuyButton = target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ||
900                               target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy');
901                               
902            if (!isCartButton && !isBuyButton) {
903                return; // Not our target, allow normal processing
904            }
905            
906            const button = isCartButton ? 
907                (target.matches('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart') ? target : target.closest('.btn-add-cart, .mf-cart, #mf-cart, #btn-add-cart')) :
908                (target.matches('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy') ? target : target.closest('.btn-buy-now, .mf-buy, #mf-buy, #btn-buy'));
909            
910            // IMMEDIATE BLOCK: Prevent any processing first
911            event.preventDefault();
912            event.stopImmediatePropagation();
913            
914            // SECURITY CHECK 1: Is button marked as stock-blocked?
915            if (button && button.hasAttribute('data-stock-blocked')) {
916                
917                const reason = button.getAttribute('data-stock-reason') || 'Stok tidak tersedia';
918                showStockModal(reason);
919                
920                // Show security alert
921                if (window.showToast) {
922                    window.showToast({
923                        type: 'error',
924                        title: 'Aksi Diblokir',
925                        message: 'Produk ini sedang habis stok dan tidak dapat ditambahkan ke keranjang.',
926                        duration: 4000
927                    });
928                }
929                
930                return false;
931            }
932            
933            // SECURITY CHECK 2: Real-time validation against cached stock info
934            if (window._currentStockInfo && !window._currentStockInfo.can_add_to_cart) {
935                
936                showStockModal(window._currentStockInfo.message || 'Stok tidak tersedia');
937                
938                // Re-enforce button disabling
939                if (button) {
940                    button.disabled = true;
941                    button.style.opacity = '0.5';
942                    button.style.cursor = 'not-allowed';
943                    button.setAttribute('data-stock-blocked', 'true');
944                    button.setAttribute('data-stock-reason', window._currentStockInfo.message || 'Stok tidak tersedia');
945                }
946                
947                return false;
948            }
949            
950            // SECURITY CHECK 3: Smart client-side validation
951            const productId = window.PD_ID ? parseInt(window.PD_ID) : 0;
952            const variantSelector = document.getElementById('pd-variant-id');
953            const variantId = variantSelector ? parseInt(variantSelector.value || 0
953) : 0;
954            
955            if (productId > 0) {
956                // SMART VALIDATION: If we already have cached stock info showing stock is available,
957                // and buttons are enabled, proceed immediately without re-validation
958                if (window._currentStockInfo && window._currentStockInfo.can_add_to_cart && 
959                    button && !button.disabled && !button.hasAttribute('data-stock-blocked')) {
960                    
961                    // Allow the action to proceed immediately with toast
962                    if (isCartButton) {
963                        // Trigger cart action with proper toast handling
964                        if (typeof window._pdAddToCartSubmit === 'function') {
965                            // Use pd-addtocart.js function for proper toast handling
966                            const form = document.getElementById('pd-buy-form') || button.closest('form');
967                            if (form) {
968                                const fakeEvent = { 
969                                    target: form,
970                                    preventDefault: () => {}
971                                };
972                                window._pdAddToCartSubmit(fakeEvent, productId);
973                            }
974                        } else if (typeof window.addToCartSubmit === 'function') {
975                            // Fallback to addToCartSubmit
976                            const form = document.getElementById('pd-buy-form') || button.closest('form');
977                            if (form) {
978                                const fakeEvent = { 
979                                    target: form,
980                                    preventDefault: () => {}
981                                };
982                                window.addToCartSubmit(fakeEvent, productId);
983                            }
984                        } else if (typeof window.addToCart === 'function') {
985                            // Direct addToCart call
986                            window.addToCart(productId, 1, variantId);
987                        }
988                    } else if (isBuyButton) {
989                        // Trigger buy action
990                        if (typeof window.buyNow === 'function') {
991                            window.buyNow();
992                        }
993                    }
994                    
995                    return false; // Action completed
996                }
997                
998                // Get stock info from embedded variant data (for variant products)
999                const stockInfo = getStockFromEmbeddedData(variantId);
1000                
1001                if (stockInfo && stockInfo.hasStock) {
1002                    
1003                    // Update global stock cache
1004                    window._currentStockInfo = {
1005                        can_add_to_cart: true,
1006                        is_in_stock: true,
1007                        available_stock: stockInfo.stock,
1008                        message: 'Stok tersedia'
1009                    };
1010                    
1011                    // IMPORTANT: Check if button was explicitly blocked by variant logic (e.g., pre-order exhausted)
1012                    // If data-stock-blocked is set, respect it and don't enable the button
1013                    const isExplicitlyBlocked = button.getAttribute('data-stock-blocked') === 'true';
1014                    
1015                    if (!isExplicitlyBlocked) {
1016                        // Remove stock blocking if it was set and enable button
1017                        button.removeAttribute('data-stock-blocked');
1018                        button.removeAttribute('data-stock-reason');
1019                        button.disabled = false;
1020                        button.style.opacity = '';
1021                        button.style.cursor = '';
1022                        button.classList.remove(config.disabledClass);
1023                    } else {
1024                        // Button is blocked by variant logic, show reason
1025                        const reason = button.getAttribute('data-stock-reason') || 'Tidak tersedia';
1026                        console.log('Button blocked by variant logic:', reason);
1027                        e.preventDefault();
1028                        e.stopPropagation();
1029                        return false;
1030                    }
1031                    
1032                    // Allow the action to proceed immediately with toast
1033                    if (isCartButton) {
1034                        // Trigger cart action with proper toast handling
1035                        if (typeof window._pdAddToCartSubmit === 'function') {
1036                            // Use pd-addtocart.js function for proper toast handling
1037                            const form = document.getElementById('pd-buy-form') || button.closest('form');
1038                            if (form) {
1039                                const fakeEvent = { 
1040                                    target: form,
1041                                    preventDefault: () => {}
1042                                };
1043                                window._pdAddToCartSubmit(fakeEvent, productId);
1044                            }
1045                        } else if (typeof window.addToCartSubmit === 'function') {
1046                            // Fallback to addToCartSubmit
1047                            const form = document.getElementById('pd-buy-form') || button.closest('form');
1048                            if (form) {
1049                                const fakeEvent = { 
1050                                    target: form,
1051                                    preventDefault: () => {}
1052                                };
1053                                window.addToCartSubmit(fakeEvent, productId);
1054                            }
1055                        } else if (typeof window.addToCart === 'function') {
1056                            // Direct addToCart call
1057                            window.addToCart(productId, 1, variantId);
1058                        }
1059                    } else if (isBuyButton) {
1060                        // Trigger buy action
1061                        if (typeof window.buyNow === 'function') {
1062                            window.buyNow();
1063                        }
1064                    }
1065                } else if (stockInfo && !stockInfo.hasStock) {
1066                    
1067                    // Update global stock cache
1068                    window._currentStockInfo = {
1069                        can_add_to_cart: false,
1070                        is_in_stock: false,
1071                        available_stock: stockInfo.stock,
1072                        message: `Stok tidak tersedia${stockInfo.sku ? ` untuk ${stockInfo.sku}` : ''}`
1073                    };
1074                    
1075                    // Force block button
1076                    button.disabled = true;
1077                    button.style.opacity = '0.5';
1078                    button.style.cursor = 'not-allowed';
1079                    button.setAttribute('data-stock-blocked', 'true');
1080                    button.setAttribute('data-stock-reason', window._currentStockInfo.message);
1081                    
1082                    // Show modal with variant info
1083                    const modalMessage = stockInfo ? 
1084                        `Varian yang dipilih (${stockInfo.sku}) sedang habis stok. Silakan pilih varian lain.` :
1085                        'Produk ini sedang habis stok. Silakan pilih varian lain.';
1086                    
1087                    showStockModal(modalMessage);
1088                    
1089                    // Show security warning
1090                    if (window.showToast) {
1091                        window.showToast({
1092                            type: 'warning',
1093                            title: 'Stok Tidak Tersedia',
1094                            message: 'Varian yang dipilih sedang habis stok. Silakan pilih varian lain.',
1095                            duration: 3000
1096                        });
1097                    }
1098                } else {
1099                    // stockInfo is null (simple product) - this means embedded data is not available
1100                    // For simple products, if buttons are enabled, we should proceed (trust the API response from updateStockDisplay)
1101                    if (button && !button.disabled && !button.hasAttribute('data-stock-blocked')) {
1102                        
1103                        // Allow the action to proceed immediately with toast
1104                        if (isCartButton) {
1105                            // Trigger cart action with proper toast handling
1106                            if (typeof window._pdAddToCartSubmit === 'function') {
1107                                // Use pd-addtocart.js function for proper toast handling
1108                                const form = document.getElementById('pd-buy-form') || button.closest('form');
1109                                if (form) {
1110                                    const fakeEvent = { 
1111                                        target: form,
1112                                        preventDefault: () => {}
1113                                    };
1114                                    window._pdAddToCartSubmit(fakeEvent, productId);
1115                                }
1116                            } else if (typeof window.addToCartSubmit === 'function') {
1117                                // Fallback to addToCartSubmit
1118                                const form = document.getElementById('pd-buy-form') || button.closest('form');
1119                                if (form) {
1120                                    const fakeEvent = { 
1121                                        target: form,
1122                                        preventDefault: () => {}
1123                                    };
1124                                    window.addToCartSubmit(fakeEvent, productId);
1125                                }
1126                            } else if (typeof window.addToCart === 'function') {
1127                                // Direct addToCart call
1128                                window.addToCart(productId, 1, variantId);
1129                            }
1130                        } else if (isBuyButton) {
1131                            // Trigger buy action
1132                            if (typeof window.buyNow === 'function') {
1133                                window.buyNow();
1134                            }
1135                        }
1136                    } else {
1137                        // Button is disabled, don't allow action
1138                        showStockModal('Produk ini sedang tidak tersedia.');
1139                    }
1140                }
1141            } else {
1142                // No product ID, block action
1143                showStockModal('Data produk tidak valid.');
1144            }
1145            
1146            return false; // Always block at capture phase
1147            
1148        }, true); // Use capture phase to run BEFORE other listeners
1149        
1150
1151    }
1152
1153    /**
1154     * Initialize button data attributes from current page
1155     */
1156    function initializeButtonAttributes() {
1157        // Get product data from page globals or form
1158        let productId = 0, variantId = 0;
1159        
1160        // Try to get from global variables (set by PHP)
1161        if (window.PD_ID) productId = parseInt(window.PD_ID);
1162        
1163        // Try to get from form
1164        const form = document.getElementById('pd-buy-form');
1165        if (form) {
1166            const pidAttr = form.getAttribute('data-pid');
1167            if (pidAttr) productId = parseInt(pidAttr);
1168            
1169            const varInput = document.getElementById('pd-variant-id');
1170            if (varInput) variantId = parseInt(varInput.value || 0);
1171        }
1172        
1173        // Try to get from mobile FAB
1174        const mobileFab = document.getElementById('pd-mobile-fab');
1175        if (mobileFab && productId <= 0) {
1176            const pidAttr = getDataAttribute(mobileFab, 'pid');
1177            if (pidAttr) productId = parseInt(pidAttr);
1178        }
1179        
1180        // Set attributes on desktop buttons
1181        const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher');
1182        desktopButtons.forEach(button => {
1183            if (productId > 0) {
1184                button.setAttribute('data-product-id', productId);
1185                if (variantId > 0) {
1186                    button.setAttribute('data-variant-id', variantId);
1187                }
1188            }
1189        });
1190        
1191        // Set attributes on mobile buttons
1192        const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy');
1193        mobileButtons.forEach(button => {
1194            if (productId > 0) {
1195                button.setAttribute('data-product-id', productId);
1196                if (variantId > 0) {
1197                    button.setAttribute('data-variant-id', variantId);
1198                }
1199            }
1200        });
1201        
1202
1203        
1204        // Initial stock display update on page load
1205        if (productId > 0) {
1206            setTimeout(() => {
1207                updateStockDisplay(productId, variantId);
1208
1209            }, 100);
1210        }
1211    }
1212
1213    /**
1214     * Update button attributes when variant changes
1215     */
1216    function updateButtonAttributesForVariant(variantId) {
1217        const productId = window.PD_ID ? parseInt(window.PD_ID) : 0;
1218        
1219        if (productId <= 0) return;
1220        
1221        // Update desktop buttons
1222        const desktopButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, #btn-add-cart, #btn-buy, #btn-buy-voucher');
1223        desktopButtons.forEach(button => {
1224            button.setAttribute('data-product-id', productId);
1225            if (variantId > 0) {
1226                button.setAttribute('data-variant-id', variantId);
1227            } else {
1228                button.removeAttribute('data-variant-id');
1229            }
1230        });
1231        
1232        // Update mobile buttons
1233        const mobileButtons = document.querySelectorAll('.mf-cart, .mf-buy, #mf-cart, #mf-buy');
1234        mobileButtons.forEach(button => {
1235            button.setAttribute('data-product-id', productId);
1236            if (variantId > 0) {
1237                button.setAttribute('data-variant-id', variantId);
1238            } else {
1239                button.removeAttribute('data-variant-id');
1240            }
1241        });
1242        
1243
1244    }
1245
1246    /**
1247     * Setup variant change integration with existing product-detail.php handlers
1248     */
1249    function setupVariantChangeIntegration() {
1250        // Strategy: Hook into existing applyVariant function instead of adding new listener
1251        // This prevents conflicts with existing variant change functionality (gallery, pricing, etc.)
1252        
1253        // Method 1: Hook into existing applyVariant function
1254        setTimeout(() => {
1255            if (typeof window.applyVariant === 'function') {
1256                const originalApplyVariant = window.applyVariant;
1257                window.applyVariant = function(v, focusImage) {
1258                    // Call original function first
1259                    const result = originalApplyVariant.call(this, v, focusImage);
1260                    
1261                // Then add our stock validator logic
1262                const variantId = v ? (v.id || v.ID || 0) : 0;
1263                
1264                // Update button attributes for stock validator
1265                updateButtonAttributesForVariant(variantId);
1266                
1267                // Update stock display using embedded data (instant)
1268                const productId = window.PD_ID ? parseInt(window.PD_ID) : 0;
1269                if (productId > 0) {
1270                    updateStockDisplay(productId, variantId);
1271                }                    return result;
1272                };
1273                
1274
1275            } else {
1276
1277                setupVariantObserver();
1278            }
1279        }, 500);
1280        
1281        // Method 2: Direct observation of hidden input value changes (fallback)
1282        setTimeout(() => {
1283            setupVariantObserver();
1284        }, 1000);
1285    }
1286
1287    /**
1288     * Setup variant observer as fallback when applyVariant hook fail
1288s
1289     */
1290    function setupVariantObserver() {
1291        const variantInput = document.getElementById('pd-variant-id');
1292        if (variantInput) {
1293            let lastVariantId = parseInt(variantInput.value || '0');
1294            
1295            // Use MutationObserver to watch for value changes
1296            if (window.MutationObserver) {
1297                const observer = new MutationObserver((mutations) => {
1298                    mutations.forEach((mutation) => {
1299                        if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
1300                            const newVariantId = parseInt(variantInput.value || '0');
1301                            if (newVariantId !== lastVariantId) {
1302                    
1303                                updateButtonAttributesForVariant(newVariantId);
1304                                
1305                                // Update stock display with embedded data
1306                                const productId = window.PD_ID ? parseInt(window.PD_ID) : 0;
1307                                if (productId > 0) {
1308                                    updateStockDisplay(productId, newVariantId);
1309                                }
1310                                
1311                                lastVariantId = newVariantId;
1312                            }
1313                        }
1314                    });
1315                });
1316                
1317                observer.observe(variantInput, { 
1318                    attributes: true, 
1319                    attributeFilter: ['value'] 
1320                });
1321                
1322
1323            }
1324            
1325            // Additional polling as ultimate fallback
1326            setInterval(() => {
1327                const currentVariantId = parseInt(variantInput.value || '0');
1328                if (currentVariantId !== lastVariantId) {
1329        
1330                    updateButtonAttributesForVariant(currentVariantId);
1331                    
1332                    // Update stock display with embedded data
1333                    const productId = window.PD_ID ? parseInt(window.PD_ID) : 0;
1334                    if (productId > 0) {
1335                        updateStockDisplay(productId, currentVariantId);
1336                    }
1337                    
1338                    lastVariantId = currentVariantId;
1339                }
1340            }, 1000);
1341            
1342
1343        }
1344    }
1345
1346    /**
1347     * Update stock display and enforce stock-based button blocking using embedded data
1348     */
1349    function updateStockDisplay(productId, variantId = 0) {
1350
1351        
1352        // Get stock info from embedded data (instant)
1353        const embeddedStockInfo = getStockFromEmbeddedData(variantId);
1354        
1355        let stockInfo;
1356        if (embeddedStockInfo) {
1357            // Convert embedded data to standard format
1358            stockInfo = {
1359                can_add_to_cart: embeddedStockInfo.hasStock,
1360                is_in_stock: embeddedStockInfo.hasStock,
1361                available_stock: embeddedStockInfo.stock,
1362                message: embeddedStockInfo.hasStock ? 'Stok tersedia' : `Stok habis${embeddedStockInfo.sku ? ` untuk ${embeddedStockInfo.sku}` : ''}`,
1363                product_name: window.PD_NAME || 'Produk',
1364                sku: embeddedStockInfo.sku
1365            };
1366            
1367
1368        } else {
1369            // Fallback to API call if embedded data not available
1370
1371            checkStock(productId, variantId, 1)
1372                .then(apiStockInfo => {
1373                    updateStockDisplayWithInfo(apiStockInfo, productId, variantId);
1374                })
1375                .catch(error => {
1376                    console.error('Failed to update stock display via API:', error);
1377                });
1378            return;
1379        }
1380        
1381        updateStockDisplayWithInfo(stockInfo, productId, variantId);
1382    }
1383
1384    /**
1385     * Update stock display with provided stock info
1386     */
1387    function updateStockDisplayWithInfo(stockInfo, productId, variantId) {
1388        // Store current stock status globally for blocking checks
1389        window._currentStockInfo = stockInfo;
1390        
1391        // Update stock indicators
1392        const stockIndicators = document.querySelectorAll('.stock-indicator');
1393        stockIndicators.forEach(indicator => {
1394            if (stockInfo.is_in_stock) {
1395                indicator.classList.add('in-stock');
1396                indicator.classList.remove('out-of-stock');
1397            } else {
1398                indicator.classList.add('out-of-stock');
1399                indicator.classList.remove('in-stock');
1400            }
1401        });
1402        
1403        // Update stock count
1404        const stockCounts = document.querySelectorAll('.stock-count');
1405        stockCounts.forEach(count => {
1406            count.textContent = stockInfo.available_stock || 0;
1407        });
1408        
1409        // CRITICAL: For digital products, DO NOT modify button states here
1410        // PHP already handles button disable/enable based on LICENSE_POOL count
1411        // Only physical products need JavaScript stock validation
1412        const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true';
1413        
1414        if (isDigitalProduct) {
1415            // For digital products: ONLY update global stock info, don't touch buttons
1416            // Let PHP-rendered disabled state remain authoritative
1417            console.log('[Stock Validator] Digital product detected - skipping button state modification');
1418            return; // Exit early, preserve PHP-set button states
1419        }
1420        
1421        // ENHANCED: Force disable buttons for out-of-stock with anti-tampering (PHYSICAL PRODUCTS ONLY)
1422        const allButtons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy');
1423        allButtons.forEach(button => {
1424            if (stockInfo.can_add_to_cart) {
1425                // STOCK AVAILABLE - ENABLE BUTTONS
1426                button.classList.remove(config.disabledClass);
1427                button.disabled = false;
1428                button.style.opacity = '';
1429                button.style.cursor = '';
1430                
1431                // CRITICAL: Remove stock blocking attributes when stock becomes available
1432                button.removeAttribute('data-stock-blocked');
1433                button.removeAttribute('data-stock-reason');
1434                
1435
1436            } else {
1437                // OUT OF STOCK - FORCE DISABLE WITH ANTI-TAMPERING
1438                button.classList.add(config.disabledClass);
1439                button.disabled = true;
1440                button.style.opacity = '0.5';
1441                button.style.cursor = 'not-allowed';
1442                
1443                // Mark as stock-blocked to prevent re-enabling
1444                button.setAttribute('data-stock-blocked', 'true');
1445                button.setAttribute('data-stock-reason', stockInfo.message || 'Stok tidak tersedia');
1446                
1447
1448            }
1449        });
1450        
1451        // Start aggressive button monitoring for out-of-stock variants
1452        if (!stockInfo.can_add_to_cart) {
1453            enforceStockDisabling(productId, variantId);
1454        }
1455        
1456
1457    }
1458
1459    /**
1460     * Aggressively enforce button disabling for out-of-stock items
1461     * CRITICAL: For digital products, DO NOT modify button states (PHP is authoritative)
1462     */
1463    function enforceStockDisabling(productId, variantId) {
1464        // CRITICAL: Skip enforcement for digital products - PHP handles button states
1465        const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true';
1466        if (isDigitalProduct) {
1467            console.log('[Stock Validator] Digital product - skipping button enforcement');
1468            return; // Exit early, let PHP-rendered disabled state remain
1469        }
1470        
1471        const monitorInterval = setInterval(() => {
1472            // Check if variant changed
1473            const currentVariantSelector = document.getElementById('pd-variant-id');
1474            const currentVariantId = currentVariantSelector ? parseInt(currentVariantSelector.value || 0) : 0;
1475            
1476            if (currentVariantId !== variantId) {
1477                // Variant changed, stop monitoring this variant
1478                clearInterval(monitorInterval);
1479
1480                return;
1481            }
1482            
1483            // Check current stock status using embedded data (real-time check)
1484            const currentStockInfo = getStockFromEmbeddedData(currentVariantId);
1485            
1486            if (currentStockInfo && !currentStockInfo.hasStock) {
1487                // Still out of stock - continue monitoring for tampering
1488                const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy');
1489                buttons.forEach(button => {
1490                    // Check if button was manually enabled (inspect attack)
1491                    if (!button.disabled || button.style.opacity !== '0.5') {
1492                        
1493                        // Force re-disable
1494                        button.disabled = true;
1495                        button.style.opacity = '0.5';
1496                        button.style.cursor = 'not-allowed';
1497                        button.setAttribute('data-stock-blocked', 'true');
1498                        button.setAttribute('data-stock-reason', `Stok tidak tersedia${currentStockInfo.sku ? ` untuk ${currentStockInfo.sku}` : ''}`);
1499                        
1500                        // Show warning toast
1501                        if (window.showToast) {
1502                            window.showToast({
1503                                type: 'warning',
1504                                title: 'Aksi Tidak Diizinkan',
1505                                message: 'Produk ini sedang habis stok. Silakan pilih varian lain.',
1506                                duration: 3000
1507                            });
1508                        }
1509                    }
1510                });
1511            } else {
1512                // Stock available OR embedded data shows stock - stop monitoring
1513                clearInterval(monitorInterval);
1514
1515                // For PHYSICAL products only, enable buttons
1516                // For DIGITAL products, skip (PHP is authoritative)
1517                const isDigitalProduct = window.IS_DIGITAL === true || window.IS_DIGITAL === 'true';
1518                
1519                if (!isDigitalProduct && currentStockInfo && currentStockInfo.hasStock) {
1520                    const buttons = document.querySelectorAll('.btn-add-cart, .btn-buy-now, .mf-cart, .mf-buy, #btn-add-cart, #btn-buy, #btn-buy-voucher, #mf-cart, #mf-buy');
1521                    buttons.forEach(button => {
1522                        button.disabled = false;
1523                        button.style.opacity = '';
1524                        button.style.cursor = '';
1525                        button.removeAttribute('data-stock-blocked');
1526                        button.removeAttribute('data-stock-reason');
1527                        button.classList.remove(config.disabledClass);
1528                    });
1529
1530                }
1531            }
1532        }, 500); // Check every 500ms for tampering attempts
1533        
1534        // Auto-stop monitoring after 30 seconds to prevent memory leaks
1535        setTimeout(() => {
1536            clearInterval(monitorInterval);
1537
1538        }, 30000);
1539    }
1540
1541    /**
1542     * Override addToCartSubmit function to add stock validation
1543     * This function now works with pd-addtocart.js integration
1544     */
1545    function overrideAddToCartSubmitFunction() {
1546        // Check if pd-addtocart.js is already loaded
1547        const pdAddToCartExists = typeof window.addToCartSubmit === 'function' && 
1548                                  window.addToCartSubmit.toString().includes('showToast');
1549        
1550        if (pdAddToCartExists) {
1551
1552            
1553            // Store the pd-addtocart.js function
1554            const pdAddToCartSubmit = window.addToCartSubmit;
1555            window._pdAddToCartSubmit = pdAddToCartSubmit;
1556            
1557            // Override with stock validation that calls pd-addtocart.js
1558            window.addToCartSubmit = async function(e, productId) {
1559                
1560                // Don't prevent default here - let pd-addtocart handle it if stock is valid
1561                
1562                try {
1563                    // Extract product data from form (same way as pd-addtocart.js)
1564                    const form = e.target;
1565                    const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1;
1566                    
1567                    // CRITICAL: Always get current variant from selector (real-time)
1568                    let variantId = 0;
1569                    const variantSelector = document.getElementById('pd-variant-id');
1570                    if (variantSelector) {
1571                        variantId = parseInt(variantSelector.value || '0', 10) || 0;
1572                    }
1573                    
1574                    if (!productId || productId <= 0) {
1575                        return pdAddToCartSubmit(e, productId);
1576                    }
1577
1578
1579                    // Validate stock before proceeding
1580                    const isValid = await validateBeforeCart(productId, variantId, qty, 'cart');
1581                    
1582                    if (isValid) {
1583                        // Stock OK, call pd-addtocart.js function (it will handle toast)
1584                        return pdAddToCartSubmit(e, productId);
1585                    } else {
1586                        // Prevent the form submission
1587                        e.preventDefault();
1588                        // Stock validation failed, modal already shown by validateBeforeCart
1589                        return false;
1590                    }
1591                    
1592                } catch (error) {
1593                    e.preventDefault();
1594                    // On error, show modal and don't proceed
1595                    showStockModal({ 
1596                        message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.',
1597                        is_active: false 
1598                    }, 'cart');
1599                    return false;
1600                }
1601            };
1602        } else {
1603            // Original logic for when pd-addtocart.js is not present
1604            const originalAddToCartSubmit = typeof window.addToCartSubmit === 'function' ? window.addToCartSubmit : null;
1605            
1606            if (originalAddToCartSubmit) {
1607                window._originalAddToCartSubmit = originalAddToCartSubmit;
1608            }
1609            
1610            // Override with stock validation
1611            window.addToCartSubmit = async function(e, productId) {
1612                
1613                e.preventDefault();
1614                
1615                try {
1616                    const form = e.target;
1617                    const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1;
1618                    
1619                    // CRITICAL: Always get current variant from selector (real-time)
1620                    let variantId = 0;
1621                    const variantSelector = document.getElementById('pd-variant-id');
1622                    if (variantSelector) {
1623                        variantId = parseInt(variantSelector.value || '0', 10) || 0;
1624                    }
1625                    
1626                    if (!productId || productId <= 0) {
1627                        if (originalAddToCartSubmit) return originalAddToCartSubmit(e, productId);
1628                        return false;
1629                    }
1630
1631
1632                    const isValid = await validateBeforeCart(productId, variantId, qty, 'cart');
1633                    
1634                    if (isValid) {
1635                        if (originalAddToCartSubmit) {
1636                            return originalAddToCartSubmit(e, productId);
1637                        } else {
1638                            if (typeof window.addToCart === 'function') {
1639                                await window.addToCart(productId, qty, variantId);
1640                            }
1641                        }
1642                    } else {
1643                    }
1644                    
1645                } catch (error) {
1646                    showStockModal({ 
1647                        message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.',
1648                        is_active: false 
1649                    }, 'cart');
1650                }
1651                
1652                return false;
1653            };
1654        }
1655
1656
1657    }
1658
1659    /**
1660     * Override buyNow function to add stock validation
1661     */
1662    function overrideBuyNowFunction() {
1663        // Store original buyNow function if it exists
1664        const originalBuyNow = typeof window.buyNow === 'function' ? window.buyNow : null;
1665        
1666        // Override with stock validation
1667        window.buyNow = async function(e) {
1668            
1669            try {
1670                // Get product data (same way as pd-buynow.js)
1671                const form = document.getElementById('pd-buy-form');
1672                if (!form) {
1673                    if (originalBuyNow) return originalBuyNow(e);
1674                    return;
1675                }
1676
1677                const productId = parseInt(form.dataset.pid || '0', 10);
1678                const qty = parseInt(form.querySelector('[name=qty]')?.value || '1', 10) || 1;
1679                
1680                // CRITICAL: Always get current variant from selector (real-time)
1681                let variantId = 0;
1682                const variantSelector = document.getElementById('pd-variant-id');
1683                if (variantSelector) {
1684                    variantId = parseInt(variantSelector.value || '0', 10) || 0;
1685                }
1686                
1687                if (!productId || productId <= 0) {
1688                    if (originalBuyNow) return originalBuyNow(e);
1689                    return;
1690                }
1691
1692
1693                // Validate stock before proceeding
1694                const isValid = await validateBeforeCart(productId, variantId, qty, 'buy');
1695                
1696                if (isValid) {
1697                    // Stock OK, call original buyNow function
1698                    if (originalBuyNow) {
1699                        return originalBuyNow(e);
1700                    } else {
1701                    }
1702                } else {
1703                    // Stock validation failed, modal already shown by validateBeforeCart
1704                }
1705                
1706            } catch (error) {
1707                // On error, show modal and don't proceed
1708                showStockModal({ 
1709                    message: 'Terjadi kesalahan saat memvalidasi stok. Silakan coba lagi.',
1710                    is_active: false 
1711                }, 'buy');
1712            }
1713        };
1714
1715
1716    }
1717
1718    /**
1719     * Initialize stock validation
1720     */
1721    function init() {
1722        // Wait for DOM ready
1723        if (document.readyState === 'loading') {
1724            addEventListener(document, 'DOMContentLoaded', function() {
1725        initializeButtonAttributes();
1726        setupVariantChangeIntegration(); // Setup variant change integration
1727        interceptCartButtons();
1728        setupGlobalClickInterceptor(); // Setup global click security interceptor                // Override functions after other scripts load
1729                // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js
1730                setTimeout(() => {
1731                    overrideAddToCartSubmitFunction();
1732                    overrideBuyNowFunction();
1733                }, 100);
1734                
1735                setTimeout(() => {
1736                    // Re-check and re-override if pd-addtocart.js loaded later
1737                    overrideAddToCartSubmitFunction();
1738                }, 500);
1739                
1740                setTimeout(() => {
1741                    // Final check for very late loading scripts
1742                    overrideAddToCartSubmitFunction();
1743                }, 1000);
1744                
1745
1746            });
1747        } else {
1748            initializeButtonAttributes();
1749            setupVariantChangeIntegration(); // Setup variant change integration
1750            interceptCartButtons();
1751            setupGlobalClickInterceptor(); // Setup global click security interceptor
1752            
1753            // Override functions after other scripts load
1754            // Use multiple timeouts to catch late-loading scripts like pd-addtocart.js
1755            setTimeout(() => {
1756                overrideAddToCartSubmitFunction();
1757                overrideBuyNowFunction();
1758            }, 100);
1759            
1760            setTimeout(() => {
1761                // Re-check and re-override if pd-addtocart.js loaded later
1762                overrideAddToCartSubmitFunction();
1763            }, 500);
1764            
1765            setTimeout(() => {
1766                // Final check for very late loading scripts
1767                overrideAddToCartSubmitFunction();
1768            }, 1000);
1769            
1770
1771        }
1772    }
1773
1774    // Public API
1775    return {
1776        init: init,
1777        checkStock: checkStock,
1778        validateBeforeCart: validateBeforeCart,
1779        updateStockDisplay: updateStockDisplay,
1780        showStockModal: showStockModal,
1781        config: config
1782    };
1783})();
1784
1785// Auto-initialize
1786if (typeof window !== 'undefined') {
1787    StockValidator.init();
1788}

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.