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 = `
`;
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 = `
`;
}
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 = `
`;
// 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
`;
}
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;