const API_BASE = '/api'; function getOrCreateSessionId() { let sessionId = localStorage.getItem('cart_session_id'); if (!sessionId) { sessionId = 'sess_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36); localStorage.setItem('cart_session_id', sessionId); } return sessionId; } function getSelectedRegion() { const stored = localStorage.getItem('selectedRegion'); return stored ? JSON.parse(stored) : null; } async function getExchangeRate(fromCurrency, toCurrency) { if (fromCurrency === toCurrency) { return 1.0; } try { const response = await fetch(`${API_BASE}/currency/rate/${fromCurrency}/${toCurrency}`); const data = await response.json(); if (data.success && data.rate) { return data.rate; } } catch (err) { console.error('Failed to get exchange rate:', err); } return 1.0; // Fallback } // Get cart from localStorage function getCart() { try { return JSON.parse(localStorage.getItem('cart') || '[]'); } catch { return []; } } // Store form data between steps let checkoutData = { customerInfo: {}, shippingInfo: {}, items: [] }; let currentStep = 1; let itemsWithCurrentPrices = []; let currency = 'USD'; // Load saved checkout data from localStorage function loadCheckoutData() { try { const saved = localStorage.getItem('checkoutData'); if (saved) { const parsed = JSON.parse(saved); checkoutData.customerInfo = parsed.customerInfo || {}; checkoutData.shippingInfo = parsed.shippingInfo || {}; if (parsed.currentStep) { currentStep = parsed.currentStep; } } } catch (error) { console.error('Error loading checkout data:', error); } } // Save checkout data to localStorage function saveCheckoutData() { try { localStorage.setItem('checkoutData', JSON.stringify({ customerInfo: checkoutData.customerInfo, shippingInfo: checkoutData.shippingInfo, currentStep: currentStep })); } catch (error) { console.error('Error saving checkout data:', error); } } // Clear saved checkout data function clearCheckoutData() { localStorage.removeItem('checkoutData'); } // Step management function setStep(step) { currentStep = step; saveCheckoutData(); updateStepIndicator(); renderStepContent(); } function updateStepIndicator() { document.querySelectorAll('.step').forEach((stepEl, index) => { const stepNum = index + 1; stepEl.classList.remove('active', 'completed'); if (stepNum < currentStep) { stepEl.classList.add('completed'); stepEl.querySelector('.step-number').textContent = '✓'; } else if (stepNum === currentStep) { stepEl.classList.add('active'); stepEl.querySelector('.step-number').textContent = stepNum; } else { stepEl.querySelector('.step-number').textContent = stepNum; } }); } async function renderCheckout() { const items = getCart(); if (items.length === 0) { document.getElementById('checkoutContent').innerHTML = `

Your cart is empty

Add some products to checkout!

Continue Shopping
`; return; } // Get user's selected region for currency const selectedRegion = getSelectedRegion(); const targetCurrency = selectedRegion ? selectedRegion.currency : 'USD'; currency = targetCurrency; // Get exchange rate once for all products const exchangeRate = await getExchangeRate('USD', targetCurrency); // Fetch current prices from API let subtotal = 0; itemsWithCurrentPrices = []; for (const item of items) { try { const headers = {}; if (selectedRegion) { headers['X-User-Region'] = selectedRegion.code; } const response = await fetch(`${API_BASE}/products/${item.productId}`, { headers }); const data = await response.json(); if (data.success) { const product = data.product; // Convert price to target currency const currentPriceUSD = product.price; // In cents const currentPrice = Math.round(currentPriceUSD * exchangeRate); // Converted const itemTotal = currentPrice * item.quantity; subtotal += itemTotal; itemsWithCurrentPrices.push({ ...item, currentPrice, productName: product.name, currency: targetCurrency, taxIncluded: product.taxIncluded || false }); } } catch (error) { console.error('Error loading product:', error); } } checkoutData.items = itemsWithCurrentPrices; loadCheckoutData(); // Load saved form data renderOrderSummary(); setStep(currentStep); } function renderOrderSummary() { const summaryContent = document.getElementById('summaryContent'); const items = checkoutData.items; if (items.length === 0) { summaryContent.innerHTML = '
Loading...
'; return; } const subtotal = items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0); const shipping = checkoutData.shippingInfo?.cost || 0; const tax = checkoutData.shippingInfo?.tax || 0; const adjustedSubtotal = checkoutData.shippingInfo?.adjustedSubtotal || subtotal; const total = adjustedSubtotal + shipping + (checkoutData.shippingInfo?.country ? tax : 0); summaryContent.innerHTML = `
${items.map(item => `
${escapeHtml(item.productName)} x${item.quantity} ${item.currency} ${((item.currentPrice * item.quantity) / 100).toFixed(2)}
`).join('')}
Subtotal: ${currency} ${(subtotal / 100).toFixed(2)}
Shipping: ${currency} ${(shipping / 100).toFixed(2)}
Tax : ${checkoutData.shippingInfo?.country ? `${currency} ${(tax / 100).toFixed(2)}` : 'Uncalculated'}
Total: ${currency} ${(total / 100).toFixed(2)}
`; } function renderStepContent() { const content = document.getElementById('checkoutContent'); if (currentStep === 1) { renderStep1(content); } else if (currentStep === 2) { renderStep2(content); } else if (currentStep === 3) { renderStep3(content); } } function renderStep1(container) { const data = checkoutData.customerInfo; container.innerHTML = `

Customer Information

`; } async function renderStep2(container) { const data = checkoutData.shippingInfo; // Fetch countries let countries = [{ code: 'US', name: 'United States' }]; try { const countriesResponse = await fetch(`${API_BASE}/countries`); const countriesData = await countriesResponse.json(); if (countriesData.success) { countries = countriesData.countries; } } catch (error) { console.error('Error fetching countries:', error); } const countriesOptions = countries.map(c => `` ).join(''); container.innerHTML = `

Shipping Address

Shipping Method

`; // Update tax if country is already selected if (data.country) { setTimeout(() => updateTaxFromAddress(), 100); } else { // Update summary to show "Uncalculated" for tax updateOrderSummary(); } } function renderStep3(container) { const customer = checkoutData.customerInfo; const shipping = checkoutData.shippingInfo; const items = checkoutData.items; const subtotal = items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0); const shippingCost = shipping.cost || 0; const tax = shipping.tax || 0; const total = (shipping.adjustedSubtotal || subtotal) + shippingCost + tax; container.innerHTML = `

Review Your Order

Customer Information

Name: ${escapeHtml(customer.firstName || '')} ${escapeHtml(customer.lastName || '')}

Email: ${escapeHtml(customer.email || '')}

${customer.phone ? `

Phone: ${escapeHtml(customer.phone)}

` : ''}

Shipping Address

${escapeHtml(shipping.address || '')}

${escapeHtml(shipping.city || '')}, ${escapeHtml(shipping.state || '')} ${escapeHtml(shipping.zip || '')}

${escapeHtml(shipping.country || '')}

Shipping Method

${shipping.shippingMethod === 'express' ? 'Express' : shipping.shippingMethod === ' overnight' ? 'Overnight' : 'Standard'} Shipping

${currency} ${(shippingCost / 100).toFixed(2)}

Order Summary

${items.map(item => `
${escapeHtml(item.productName)} x${item.quantity} ${item.currency} ${((item.currentPrice * item.quantity) / 100).toFixed(2)}
`).join('')}
Subtotal ${currency} ${((shipping.adjustedSubtotal || subtotal) / 100).toFixed(2)}
Shipping ${currency} ${(shippingCost / 100).toFixed(2)}
Tax ${currency} ${(tax / 100).toFixed(2)}
Total ${currency} ${(total / 100).toFixed(2)}
`; } function handleStep1(event) { event.preventDefault(); const form = event.target; const formData = new FormData(form); checkoutData.customerInfo = { email: formData.get('email'), firstName: formData.get('firstName'), lastName: formData.get('lastName'), phone: formData.get('phone') || '' }; saveCheckoutData(); setStep(2); } async function handleStep2(event) { event.preventDefault(); const form = event.target; const formData = new FormData(form); const shippingMethod = formData.get('shippingMethod'); const shippingCost = Math.round(parseFloat(document.getElementById('shippingMethod').options[document.getElementById('shippingMethod').selectedIndex].dataset.cost) * 100) || 0; checkoutData.shippingInfo = { name: `${checkoutData.customerInfo.firstName} ${checkoutData.customerInfo.lastName}`, address: formData.get('address'), city: formData.get('city'), state: formData.get('state'), zip: formData.get('zip'), country: formData.get('country'), shippingMethod, cost: shippingCost }; saveCheckoutData(); // Calculate tax if country is selected (AWAIT the async function!) if (checkoutData.shippingInfo.country) { await updateTaxFromAddress(); } setStep(3); } async function handleStep3() { const submitBtn = event.target; submitBtn.disabled = true; submitBtn.textContent = 'Creating...'; try { // Prepare cart items const cartItems = checkoutData.items.map(item => ({ productId: item.productId, quantity: item.quantity })); const requestData = { items: cartItems, customerInfo: checkoutData.customerInfo, shippingInfo: { ...checkoutData.shippingInfo, taxRate: checkoutData.shippingInfo.taxRate || 0 } }; const response = await fetchWithCsrf(`${API_BASE}/payment-links/create`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-session-id': getOrCreateSessionId() }, body: JSON.stringify(requestData) }); const data = await response.json(); if (!data.success) { throw new Error(data.error || 'Failed to create payment link'); } // Clear cart and checkout data, then redirect to payment page localStorage.removeItem('cart'); clearCheckoutData(); showNotification('Payment link created successfully!', 'success', 2000); setTimeout(() => { window.location.href = data.paymentLink.url; }, 2000); } catch (err) { showNotification(`Error: ${err.message}`, 'error'); submitBtn.disabled = false; submitBtn.textContent = 'Create Payment Link'; } } async function updateShipping() { const select = document.getElementById('shippingMethod'); const option = select.options[select.selectedIndex]; const shippingCost = Math.round(parseFloat(option.dataset.cost) * 100) || 0; checkoutData.shippingInfo.cost = shippingCost; await updateTaxFromAddress(); } async function updateTaxFromAddress() { const countryInput = document.getElementById('countryInput'); const stateInput = document.getElementById('stateInput'); const country = countryInput?.value?.trim() || checkoutData.shippingInfo?.country || ''; const state = stateInput?.value?.trim() || checkoutData.shippingInfo?.state || ''; // Always update summary, even if no country (to show "Uncalculated") if (!country) { checkoutData.shippingInfo.tax = 0; checkoutData.shippingInfo.taxRate = 0; checkoutData.shippingInfo.adjustedSubtotal = checkoutData.items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0); saveCheckoutData(); updateOrderSummary(); return; } const items = checkoutData.items; let currency = 'USD'; // Fetch tax rate let taxRate = 0.08; try { const taxParams = new URLSearchParams(); if (country) taxParams.append('country', country); if (state) taxParams.append('state', state); const taxResponse = await fetch(`${API_BASE}/tax/rate?${taxParams.toString()}`); const taxData = await taxResponse.json(); if (taxData.success) { taxRate = taxData.taxRate; } } catch (error) { console.error('Error fetching tax rate:', error); } // Calculate tax based on tax-included/excluded products let tax = 0; let adjustedSubtotal = 0; for (const item of items) { if (item.taxIncluded) { const itemBasePrice = Math.round((item.currentPrice * item.quantity) / (1 + taxRate)); const itemTax = (item.currentPrice * item.quantity) - itemBasePrice; tax += itemTax; adjustedSubtotal += itemBasePrice; } else { const itemTax = Math.round((item.currentPrice * item.quantity) * taxRate); tax += itemTax; adjustedSubtotal += (item.currentPrice * item.quantity); } } const shippingCost = checkoutData.shippingInfo?.cost || 0; const total = adjustedSubtotal + shippingCost + tax; // Update checkout data checkoutData.shippingInfo.taxRate = taxRate; checkoutData.shippingInfo.tax = tax; checkoutData.shippingInfo.adjustedSubtotal = adjustedSubtotal; checkoutData.shippingInfo.total = total; saveCheckoutData(); updateOrderSummary(); } function updateOrderSummary() { const items = checkoutData.items; const subtotal = items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0); const shipping = checkoutData.shippingInfo?.cost || 0; const tax = checkoutData.shippingInfo?.tax || 0; const taxRate = checkoutData.shippingInfo?.taxRate || 0; const adjustedSubtotal = checkoutData.shippingInfo?.adjustedSubtotal || subtotal; // Calculate total - include tax only if country is known const total = adjustedSubtotal + shipping + (checkoutData.shippingInfo?.country ? tax : 0); const summarySubtotal = document.getElementById('summary-subtotal'); const summaryShipping = document.getElementById('summary-shipping'); const summaryTaxRow = document.getElementById('summary-tax-row'); const summaryTax = document.getElementById('summary-tax'); const summaryTaxRate = document.getElementById('summary-tax-rate'); const summaryTotal = document.getElementById('summary-total'); if (summarySubtotal) summarySubtotal.textContent = `${currency} ${(adjustedSubtotal / 100).toFixed(2)}`; if (summaryShipping) summaryShipping.textContent = `${currency} ${(shipping / 100).toFixed(2)}`; // Always show tax - show "Uncalculated" if country not selected if (summaryTaxRow && summaryTax && summaryTaxRate) { if (checkoutData.shippingInfo?.country) { summaryTax.textContent = `${currency} ${(tax / 100).toFixed(2)}`; summaryTaxRate.textContent = `(${(taxRate * 100).toFixed(2)}%)`; } else { summaryTax.textContent = 'Uncalculated'; summaryTaxRate.textContent = ''; } summaryTaxRow.style.display = 'flex'; } // Calculate total - include tax only if country is known const finalTotal = adjustedSubtotal + shipping + (checkoutData.shippingInfo?.country ? tax : 0); if (summaryTotal) summaryTotal.textContent = `${currency} ${(finalTotal / 100).toFixed(2)}`; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize document.addEventListener('DOMContentLoaded', () => { renderCheckout(); }); // Handle country change - update shipping info and recalculate tax async function handleCountryChange() { const countryInput = document.getElementById('countryInput'); const country = countryInput?.value?.trim() || ''; // Update checkout data with new country if (checkoutData.shippingInfo) { checkoutData.shippingInfo.country = country; saveCheckoutData(); } // Recalculate tax immediately await updateTaxFromAddress(); } // Handle state change - update shipping info and recalculate tax async function handleStateChange() { const stateInput = document.getElementById('stateInput'); const state = stateInput?.value?.trim() || ''; // Update checkout data with new state if (checkoutData.shippingInfo) { checkoutData.shippingInfo.state = state; saveCheckoutData(); } // Recalculate tax immediately await updateTaxFromAddress(); } // Make functions global window.setStep = setStep; window.handleStep1 = handleStep1; window.handleStep2 = handleStep2; window.handleStep3 = handleStep3; window.updateShipping = updateShipping; window.updateTaxFromAddress = updateTaxFromAddress; window.updateOrderSummary = updateOrderSummary; window.handleCountryChange = handleCountryChange; window.handleStateChange = handleStateChange;