File History: src/main/resources/static/js/checkout.js

← View file content

File Content at Commit 690c1f6

1 const API_BASE = '/api';
2
3 function getOrCreateSessionId() {
4 let sessionId = localStorage.getItem('cart_session_id');
5 if (!sessionId) {
6 sessionId = 'sess_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
7 localStorage.setItem('cart_session_id', sessionId);
8 }
9 return sessionId;
10 }
11
12 function getSelectedRegion() {
13 const stored = localStorage.getItem('selectedRegion');
14 return stored ? JSON.parse(stored) : null;
15 }
16
17 async function getExchangeRate(fromCurrency, toCurrency) {
18 if (fromCurrency === toCurrency) {
19 return 1.0;
20 }
21
22 try {
23 const response = await fetch(`${API_BASE}/currency/rate/${fromCurrency}/${toCurrency}`);
24 const data = await response.json();
25
26 if (data.success && data.rate) {
27 return data.rate;
28 }
29 } catch (err) {
30 console.error('Failed to get exchange rate:', err);
31 }
32
33 return 1.0; // Fallback
34 }
35
36 // Get cart from localStorage
37 function getCart() {
38 try {
39 return JSON.parse(localStorage.getItem('cart') || '[]');
40 } catch {
41 return [];
42 }
43 }
44
45 // Store form data between steps
46 let checkoutData = {
47 customerInfo: {},
48 shippingInfo: {},
49 items: []
50 };
51
52 let currentStep = 1;
53 let itemsWithCurrentPrices = [];
54 let currency = 'USD';
55
56 // Load saved checkout data from localStorage
57 function loadCheckoutData() {
58 try {
59 const saved = localStorage.getItem('checkoutData');
60 if (saved) {
61 const parsed = JSON.parse(saved);
62 checkoutData.customerInfo = parsed.customerInfo || {};
63 checkoutData.shippingInfo = parsed.shippingInfo || {};
64 if (parsed.currentStep) {
65 currentStep = parsed.currentStep;
66 }
67 }
68 } catch (error) {
69 console.error('Error loading checkout data:', error);
70 }
71 }
72
73 // Save checkout data to localStorage
74 function saveCheckoutData() {
75 try {
76 localStorage.setItem('checkoutData', JSON.stringify({
77 customerInfo: checkoutData.customerInfo,
78 shippingInfo: checkoutData.shippingInfo,
79 currentStep: currentStep
80 }));
81 } catch (error) {
82 console.error('Error saving checkout data:', error);
83 }
84 }
85
86 // Clear saved checkout data
87 function clearCheckoutData() {
88 localStorage.removeItem('checkoutData');
89 }
90
91 // Step management
92 function setStep(step) {
93 currentStep = step;
94 saveCheckoutData();
95 updateStepIndicator();
96 renderStepContent();
97 }
98
99 function updateStepIndicator() {
100 document.querySelectorAll('.step').forEach((stepEl, index) => {
101 const stepNum = index + 1;
102 stepEl.classList.remove('active', 'completed');
103 if (stepNum < currentStep) {
104 stepEl.classList.add('completed');
105 stepEl.querySelector('.step-number').textContent = '✓';
106 } else if (stepNum === currentStep) {
107 stepEl.classList.add('active');
108 stepEl.querySelector('.step-number').textContent = stepNum;
109 } else {
110 stepEl.querySelector('.step-number').textContent = stepNum;
111 }
112 });
113 }
114
115 async function renderCheckout() {
116 const items = getCart();
117
118 if (items.length === 0) {
119 document.getElementById('checkoutContent').innerHTML = `
120 <div style="text-align: center; padding: 4rem 2rem;">
121 <h3>Your cart is empty</h3>
122 <p style="color: #666; margin: 1rem 0;">Add some products to checkout!</p>
123 <a href="/" class="btn">Continue Shopping</a>
124 </div>
125 `;
126 return;
127 }
128
129 // Get user's selected region for currency
130 const selectedRegion = getSelectedRegion();
131 const targetCurrency = selectedRegion ? selectedRegion.currency : 'USD';
132 currency = targetCurrency;
133
134 // Get exchange rate once for all products
135 const exchangeRate = await getExchangeRate('USD', targetCurrency);
136
137 // Fetch current prices from API
138 let subtotal = 0;
139 itemsWithCurrentPrices = [];
140
141 for (const item of items) {
142 try {
143 const headers = {};
144 if (selectedRegion) {
145 headers['X-User-Region'] = selectedRegion.code;
146 }
147
148 const response = await fetch(`${API_BASE}/products/${item.productId}`, { headers });
149 const data = await response.json();
150
151 if (data.success) {
152 const product = data.product;
153
154 // Convert price to target currency
155 const currentPriceUSD = product.price; // In cents
156 const currentPrice = Math.round(currentPriceUSD * exchangeRate); // Converted
157 const itemTotal = currentPrice * item.quantity;
158 subtotal += itemTotal;
159
160 itemsWithCurrentPrices.push({
161 ...item,
162 currentPrice,
163 productName: product.name,
164 currency: targetCurrency,
165 taxIncluded: product.taxIncluded || false
166 });
167 }
168 } catch (error) {
169 console.error('Error loading product:', error);
170 }
171 }
172
173 checkoutData.items = itemsWithCurrentPrices;
174 loadCheckoutData(); // Load saved form data
175 renderOrderSummary();
176 setStep(currentStep);
177 }
178
179 function renderOrderSummary() {
180 const summaryContent = document.getElementById('summaryContent');
181 const items = checkoutData.items;
182
183 if (items.length === 0) {
184 summaryContent.innerHTML = '<div class="loading">Loading...</div>';
185 return;
186 }
187
188 const subtotal = items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0);
189 const shipping = checkoutData.shippingInfo?.cost || 0;
190 const tax = checkoutData.shippingInfo?.tax || 0;
191 const adjustedSubtotal = checkoutData.shippingInfo?.adjustedSubtotal || subtotal;
192 const total = adjustedSubtotal + shipping + (checkoutData.shippingInfo?.country ? tax : 0);
193
194 summaryContent.innerHTML = `
195 <div style="margin-bottom: 1.5rem;">
196 ${items.map(item => `
197 <div class="summary-item product">
198 <span class="product-name">${escapeHtml(item.productName)} x${item.quantity}</span>
199 <span>${item.currency} ${((item.currentPrice * item.quantity) / 100).toFixed(2)}</span>
200 </div>
201 `).join('')}
202 </div>
203 <div style="border-top: 1px solid var(--border-color); padding-top: 1rem;">
204 <div class="summary-item">
205 <span>Subtotal:</span>
206 <span id="summary-subtotal">${currency} ${(subtotal / 100).toFixed(2)}</span>
207 </div>
208 <div class="summary-item">
209 <span>Shipping:</span>
210 <span id="summary-shipping">${currency} ${(shipping / 100).toFixed(2)}</span>
211 </div>
212 <div id="summary-tax-row" class="summary-item">
213 <span>Tax <small id="summary-tax-rate" style="color: #666;"></small>:</span>
214 <span id="summary-tax">${checkoutData.shippingInfo?.country ? `${currency} ${(tax / 100).toFixed(2)}` : 'Uncalculated'}</span>
215 </div>
216 <div class="summary-item summary-total">
217 <span>Total:</span>
218 <span id="summary-total">${currency} ${(total / 100).toFixed(2)}</span>
219 </div>
220 </div>
221 `;
222 }
223
224 function renderStepContent() {
225 const content = document.getElementById('checkoutContent');
226
227 if (currentStep === 1) {
228 renderStep1(content);
229 } else if (currentStep === 2) {
230 renderStep2(content);
231 } else if (currentStep === 3) {
232 renderStep3(content);
233 }
234 }
235
236 function renderStep1(container) {
237 const data = checkoutData.customerInfo;
238
239 container.innerHTML = `
240 <div class="step-content active">
241 <h3 style="margin-bottom: 1.5rem;">Customer Information</h3>
242 <form id="step1Form" onsubmit="handleStep1(event)">
243 <div class="form-group">
244 <label>Email *</label>
245 <input type="email" name="email" required placeholder="customer@example.com" value="${data.email || ''}">
246 </div>
247 <div class="form-row">
248 <div class="form-group">
249 <label>First Name *</label>
250 <input type="text" name="firstName" required value="${data.firstName || ''}">
251 </div>
252 <div class="form-group">
253 <label>Last Name *</label>
254 <input type="text" name="lastName" required value="${data.lastName || ''}">
255 </div>
256 </div>
257 <div class="form-group">
258 <label>Phone</label>
259 <input type="tel" name="phone" placeholder="+1234567890" value="${data.phone || ''}">
260 </div>
261 <div class="step-actions">
262 <div></div>
263 <button type="submit" class="btn btn-success">Continue to Shipping</button>
264 </div>
265 </form>
266 </div>
267 `;
268 }
269
270 async function renderStep2(container) {
271 const data = checkoutData.shippingInfo;
272
273 // Fetch countries
274 let countries = [{ code: 'US', name: 'United States' }];
275 try {
276 const countriesResponse = await fetch(`${API_BASE}/countries`);
277 const countriesData = await countriesResponse.json();
278 if (countriesData.success) {
279 countries = countriesData.countries;
280 }
281 } catch (error) {
282 console.error('Error fetching countries:', error);
283 }
284
285 const countriesOptions = countries.map(c =>
286 `<option value="${c.code}" ${(data.country === c.code) ? 'selected' : (c.code === 'US' && !data.country) ? 'selected' : ''}>${escapeHtml(c.name)}</option>`
287 ).join('');
288
289 container.innerHTML = `
290 <div class="step-content active">
291 <h3 style="margin-bottom: 1.5rem;">Shipping Address</h3>
292 <form id="step2Form" onsubmit="handleStep2(event)">
293 <div class="form-group">
294 <label>Street Address *</label>
295 <input type="text" name="address" required value="${data.address || ''}">
296 </div>
297 <div class="form-row">
298 <div class="form-group">
299 <label>City *</label>
300 <input type="text" name="city" required value="${data.city || ''}">
301 </div>
302 <div class="form-group">
303 <label>State/Province *</label>
304 <input type="text" name="state" id="stateInput" required value="${data.state || ''}" onchange="handleStateChange()" oninput="handleStateChange()">
305 </div>
306 </div>
307 <div class="form-row">
308 <div class="form-group">
309 <label>ZIP/Postal Code *</label>
310 <input type="text" name="zip" required value="${data.zip || ''}">
311 </div>
312 <div class="form-group">
313 <label>Country *</label>
314 <select name="country" id="countryInput" required onchange="handleCountryChange()">
315 ${countriesOptions}
316 </select>
317 </div>
318 </div>
319
320 <div class="form-section" style="margin-top: 2rem;">
321 <h3>Shipping Method</h3>
322 <div class="form-group">
323 <label>Shipping Method</label>
324 <select name="shippingMethod" id="shippingMethod" onchange="updateShipping()">
325 <option value="standard" data-cost="0" ${data.shippingMethod === 'standard' ? 'selected' : ''}>Standard (Free)</option>
326 <option value="express" data-cost="10" ${data.shippingMethod === 'express' ? 'selected' : ''}>Express ($10.00)</option>
327 <option value="overnight" data-cost="25" ${data.shippingMethod === 'overnight' ? 'selected' : ''}>Overnight ($25.00)</option>
328 </select>
329 </div>
330 </div>
331
332 <div class="step-actions" style="gap: 1rem;display: grid;">
333 <button type="submit" class="btn btn-success">Continue to Review</button>
334 <button type="button" class="btn" onclick="setStep(1)">Back</button>
335 </div>
336 </form>
337 </div>
338 `;
339
340 // Update tax if country is already selected
341 if (data.country) {
342 setTimeout(() => updateTaxFromAddress(), 100);
343 } else {
344 // Update summary to show "Uncalculated" for tax
345 updateOrderSummary();
346 }
347 }
348
349 function renderStep3(container) {
350 const customer = checkoutData.customerInfo;
351 const shipping = checkoutData.shippingInfo;
352 const items = checkoutData.items;
353
354 const subtotal = items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0);
355 const shippingCost = shipping.cost || 0;
356 const tax = shipping.tax || 0;
357 const total = (shipping.adjustedSubtotal || subtotal) + shippingCost + tax;
358
359 container.innerHTML = `
360 <div class="step-content active">
361 <h3 style="margin-bottom: 1.5rem;">Review Your Order</h3>
362
363 <div class="form-section">
364 <h4 style="margin-bottom: 1rem;">Customer Information</h4>
365 <div style="background: var(--bg-color); padding: 1rem; border-radius: 8px;">
366 <p><strong>Name:</strong> ${escapeHtml(customer.firstName || '')} ${escapeHtml(customer.lastName || '')}</p>
367 <p><strong>Email:</strong> ${escapeHtml(customer.email || '')}</p>
368 ${customer.phone ? `<p><strong>Phone:</strong> ${escapeHtml(customer.phone)}</p>` : ''}
369 </div>
370 </div>
371
372 <div class="form-section">
373 <h4 style="margin-bottom: 1rem;">Shipping Address</h4>
374 <div style="background: var(--bg-color); padding: 1rem; border-radius: 8px;">
375 <p>${escapeHtml(shipping.address || '')}</p>
376 <p>${escapeHtml(shipping.city || '')}, ${escapeHtml(shipping.state || '')} ${escapeHtml(shipping.zip || '')}</p>
377 <p>${escapeHtml(shipping.country || '')}</p>
378 </div>
379 </div>
380
381 <div class="form-section">
382 <h4 style="margin-bottom: 1rem;">Shipping Method</h4>
383 <div style="background: var(--bg-color); padding: 1rem; border-radius: 8px;">
384 <p>${shipping.shippingMethod === 'express' ? 'Express' : shipping.shippingMethod === ' overnight' ? 'Overnight' : 'Standard'} Shipping</p>
385 <p style="color: #666; font-size: 0.9rem;">${currency} ${(shippingCost / 100).toFixed(2)}</p>
386 </div>
387 </div>
388
389 <div class="form-section">
390 <h4 style="margin-bottom: 1rem;">Order Summary</h4>
391 <div style="background: var(--bg-color); padding: 1.5rem; border-radius: 8px;">
392 ${items.map(item => `
393 <div style="display: flex; justify-content: space-between; margin-bottom: 0.75rem;">
394 <span>${escapeHtml(item.productName)} x${item.quantity}</span>
395 <span>${item.currency} ${((item.currentPrice * item.quantity) / 100).toFixed(2)}</span>
396 </div>
397 `).join('')}
398
399 <div style="border-top: 1px solid var(--border-color); margin-top: 1rem; padding-top: 1rem;">
400 <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
401 <span style="color: #666;">Subtotal</span>
402 <span>${currency} ${((shipping.adjustedSubtotal || subtotal) / 100).toFixed(2)}</span>
403 </div>
404 <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
405 <span style="color: #666;">Shipping</span>
406 <span>${currency} ${(shippingCost / 100).toFixed(2)}</span>
407 </div>
408 <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
409 <span style="color: #666;">Tax</span>
410 <span>${currency} ${(tax / 100).toFixed(2)}</span>
411 </div>
412 <div style="display: flex; justify-content: space-between; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-color); font-weight: 700; font-size: 1.1rem;">
413 <span>Total</span>
414 <span>${currency} ${(total / 100).toFixed(2)}</span>
415 </div>
416 </div>
417 </div>
418 </div>
419
420 <div class="step-actions" style="gap: 1rem;display: grid;">
421 <button type="button" class="btn btn-success" onclick="handleStep3()">Continue to Payment</button>
422 <button type="button" class="btn" onclick="setStep(2)">Back</button>
423 </div>
424 </div>
425 `;
426 }
427
428 function handleStep1(event) {
429 event.preventDefault();
430 const form = event.target;
431 const formData = new FormData(form);
432
433 checkoutData.customerInfo = {
434 email: formData.get('email'),
435 firstName: formData.get('firstName'),
436 lastName: formData.get('lastName'),
437 phone: formData.get('phone') || ''
438 };
439
440 saveCheckoutData();
441 setStep(2);
442 }
443
444 async function handleStep2(event) {
445 event.preventDefault();
446 const form = event.target;
447 const formData = new FormData(form);
448
449 const shippingMethod = formData.get('shippingMethod');
450 const shippingCost = Math.round(parseFloat(document.getElementById('shippingMethod').options[document.getElementById('shippingMethod').selectedIndex].dataset.cost) * 100) || 0;
451
452 checkoutData.shippingInfo = {
453 name: `${checkoutData.customerInfo.firstName} ${checkoutData.customerInfo.lastName}`,
454 address: formData.get('address'),
455 city: formData.get('city'),
456 state: formData.get('state'),
457 zip: formData.get('zip'),
458 country: formData.get('country'),
459 shippingMethod,
460 cost: shippingCost
461 };
462
463 saveCheckoutData();
464
465 // Calculate tax if country is selected (AWAIT the async function!)
466 if (checkoutData.shippingInfo.country) {
467 await updateTaxFromAddress();
468 }
469
470 setStep(3);
471 }
472
473 async function handleStep3() {
474 const submitBtn = event.target;
475 submitBtn.disabled = true;
476 submitBtn.textContent = 'Creating...';
477
478 try {
479 // Prepare cart items
480 const cartItems = checkoutData.items.map(item => ({
481 productId: item.productId,
482 quantity: item.quantity
483 }));
484
485 const requestData = {
486 items: cartItems,
487 customerInfo: checkoutData.customerInfo,
488 shippingInfo: {
489 ...checkoutData.shippingInfo,
490 taxRate: checkoutData.shippingInfo.taxRate || 0
491 }
492 };
493
494 const response = await fetchWithCsrf(`${API_BASE}/payment-links/create`, {
495 method: 'POST',
496 headers: {
497 'Content-Type': 'application/json',
498 'x-session-id': getOrCreateSessionId()
499 },
500 body: JSON.stringify(requestData)
501 });
502
503 const data = await response.json();
504
505 if (!data.success) {
506 throw new Error(data.error || 'Failed to create payment link');
507 }
508
509 // Clear cart and checkout data, then redirect to payment page
510 localStorage.removeItem('cart');
511 clearCheckoutData();
512 showNotification('Payment link created successfully!', 'success', 2000);
513 setTimeout(() => {
514 window.location.href = data.paymentLink.url;
515 }, 2000);
516 } catch (err) {
517 showNotification(`Error: ${err.message}`, 'error');
518 submitBtn.disabled = false;
519 submitBtn.textContent = 'Create Payment Link';
520 }
521 }
522
523 async function updateShipping() {
524 const select = document.getElementById('shippingMethod');
525 const option = select.options[select.selectedIndex];
526 const shippingCost = Math.round(parseFloat(option.dataset.cost) * 100) || 0;
527
528 checkoutData.shippingInfo.cost = shippingCost;
529 await updateTaxFromAddress();
530 }
531
532 async function updateTaxFromAddress() {
533 const countryInput = document.getElementById('countryInput');
534 const stateInput = document.getElementById('stateInput');
535 const country = countryInput?.value?.trim() || checkoutData.shippingInfo?.country || '';
536 const state = stateInput?.value?.trim() || checkoutData.shippingInfo?.state || '';
537
538 // Always update summary, even if no country (to show "Uncalculated")
539 if (!country) {
540 checkoutData.shippingInfo.tax = 0;
541 checkoutData.shippingInfo.taxRate = 0;
542 checkoutData.shippingInfo.adjustedSubtotal = checkoutData.items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0);
543 saveCheckoutData();
544 updateOrderSummary();
545 return;
546 }
547
548 const items = checkoutData.items;
549 let currency = 'USD';
550
551 // Fetch tax rate
552 let taxRate = 0.08;
553 try {
554 const taxParams = new URLSearchParams();
555 if (country) taxParams.append('country', country);
556 if (state) taxParams.append('state', state);
557
558 const taxResponse = await fetch(`${API_BASE}/tax/rate?${taxParams.toString()}`);
559 const taxData = await taxResponse.json();
560 if (taxData.success) {
561 taxRate = taxData.taxRate;
562 }
563 } catch (error) {
564 console.error('Error fetching tax rate:', error);
565 }
566
567 // Calculate tax based on tax-included/excluded products
568 let tax = 0;
569 let adjustedSubtotal = 0;
570
571 for (const item of items) {
572 if (item.taxIncluded) {
573 const itemBasePrice = Math.round((item.currentPrice * item.quantity) / (1 + taxRate));
574 const itemTax = (item.currentPrice * item.quantity) - itemBasePrice;
575 tax += itemTax;
576 adjustedSubtotal += itemBasePrice;
577 } else {
578 const itemTax = Math.round((item.currentPrice * item.quantity) * taxRate);
579 tax += itemTax;
580 adjustedSubtotal += (item.currentPrice * item.quantity);
581 }
582 }
583
584 const shippingCost = checkoutData.shippingInfo?.cost || 0;
585 const total = adjustedSubtotal + shippingCost + tax;
586
587 // Update checkout data
588 checkoutData.shippingInfo.taxRate = taxRate;
589 checkoutData.shippingInfo.tax = tax;
590 checkoutData.shippingInfo.adjustedSubtotal = adjustedSubtotal;
591 checkoutData.shippingInfo.total = total;
592
593 saveCheckoutData();
594 updateOrderSummary();
595 }
596
597 function updateOrderSummary() {
598 const items = checkoutData.items;
599 const subtotal = items.reduce((sum, item) => sum + (item.currentPrice * item.quantity), 0);
600 const shipping = checkoutData.shippingInfo?.cost || 0;
601 const tax = checkoutData.shippingInfo?.tax || 0;
602 const taxRate = checkoutData.shippingInfo?.taxRate || 0;
603 const adjustedSubtotal = checkoutData.shippingInfo?.adjustedSubtotal || subtotal;
604 // Calculate total - include tax only if country is known
605 const total = adjustedSubtotal + shipping + (checkoutData.shippingInfo?.country ? tax : 0);
606
607 const summarySubtotal = document.getElementById('summary-subtotal');
608 const summaryShipping = document.getElementById('summary-shipping');
609 const summaryTaxRow = document.getElementById('summary-tax-row');
610 const summaryTax = document.getElementById('summary-tax');
611 const summaryTaxRate = document.getElementById('summary-tax-rate');
612 const summaryTotal = document.getElementById('summary-total');
613
614 if (summarySubtotal) summarySubtotal.textContent = `${currency} ${(adjustedSubtotal / 100).toFixed(2)}`;
615 if (summaryShipping) summaryShipping.textContent = `${currency} ${(shipping / 100).toFixed(2)}`;
616
617 // Always show tax - show "Uncalculated" if country not selected
618 if (summaryTaxRow && summaryTax && summaryTaxRate) {
619 if (checkoutData.shippingInfo?.country) {
620 summaryTax.textContent = `${currency} ${(tax / 100).toFixed(2)}`;
621 summaryTaxRate.textContent = `(${(taxRate * 100).toFixed(2)}%)`;
622 } else {
623 summaryTax.textContent = 'Uncalculated';
624 summaryTaxRate.textContent = '';
625 }
626 summaryTaxRow.style.display = 'flex';
627 }
628
629 // Calculate total - include tax only if country is known
630 const finalTotal = adjustedSubtotal + shipping + (checkoutData.shippingInfo?.country ? tax : 0);
631 if (summaryTotal) summaryTotal.textContent = `${currency} ${(finalTotal / 100).toFixed(2)}`;
632 }
633
634 function escapeHtml(text) {
635 const div = document.createElement('div');
636 div.textContent = text;
637 return div.innerHTML;
638 }
639
640 // Initialize
641 document.addEventListener('DOMContentLoaded', () => {
642 renderCheckout();
643 });
644
645 // Handle country change - update shipping info and recalculate tax
646 async function handleCountryChange() {
647 const countryInput = document.getElementById('countryInput');
648 const country = countryInput?.value?.trim() || '';
649
650 // Update checkout data with new country
651 if (checkoutData.shippingInfo) {
652 checkoutData.shippingInfo.country = country;
653 saveCheckoutData();
654 }
655
656 // Recalculate tax immediately
657 await updateTaxFromAddress();
658 }
659
660 // Handle state change - update shipping info and recalculate tax
661 async function handleStateChange() {
662 const stateInput = document.getElementById('stateInput');
663 const state = stateInput?.value?.trim() || '';
664
665 // Update checkout data with new state
666 if (checkoutData.shippingInfo) {
667 checkoutData.shippingInfo.state = state;
668 saveCheckoutData();
669 }
670
671 // Recalculate tax immediately
672 await updateTaxFromAddress();
673 }
674
675 // Make functions global
676 window.setStep = setStep;
677 window.handleStep1 = handleStep1;
678 window.handleStep2 = handleStep2;
679 window.handleStep3 = handleStep3;
680 window.updateShipping = updateShipping;
681 window.updateTaxFromAddress = updateTaxFromAddress;
682 window.updateOrderSummary = updateOrderSummary;
683 window.handleCountryChange = handleCountryChange;
684 window.handleStateChange = handleStateChange;
685

Commits

Commit Author Date Message File SHA Actions
f0438c2 <f69e50@finnacloud.com> 1766443042 +0300 12/22/2025, 10:37:22 PM increment once more 48adb76 View
188fc92 <f69e50@finnacloud.com> 1766442998 +0300 12/22/2025, 10:36:38 PM increment 48adb76 View
4617f76 <f69e50@finnacloud.com> 1766442953 +0300 12/22/2025, 10:35:53 PM rename branch from main to master oops 48adb76 View
e6d1548 <f69e50@finnacloud.com> 1766442769 +0300 12/22/2025, 10:32:49 PM add initial test workflow file 48adb76 View
9c24ca4 <f69e50@finnacloud.com> 1766442705 +0300 12/22/2025, 10:31:45 PM add CI configuration and test script for Jenkins build 48adb76 View
690c1f6 <f69e50@finnacloud.com> 1766368110 +0300 12/22/2025, 1:48:30 AM initialize backend structure with controllers, DTOs, and configuration files 48adb76 Hide