1/* global hrtkBWMR */ 2/** 3 * Booking Widget Math Reconcile â JS patch. 4 * 5 * The HR plugin's booking widget JS double-subtracts the weekly discount 6 * when `price_with_dates = average_price`: 7 * 8 * actualPrice (strikethrough) = totalPrice / nights CORRECT post-discount 9 * price (active bold) = (totalPrice - discount) / nights double-discounted 10 * 11 * `totalPrice` (rateplan.total or total - taxes) already has the discount 12 * applied server-side, so subtracting `discount` again on the JS side is a 13 * bug. Result: customer sees a per-night number lower than total/nights. 14 * 15 * Fix logic: 16 * Let S = strike value (HR's actualPrice) = totalPrice / nights 17 * Let A = active value (HR's price) = (totalPrice - discount) / nights 18 * Then discount/nights = S - A, and 19 * pre-discount per-night = (totalPrice + discount) / nights = 2S - A 20 * 21 * With showStrikethrough = true (default): 22 * strike = (2S - A) pre-discount per-night 23 * active = S post-discount per-night 24 * matches the search results card's strike+active pair 25 * 26 * With showStrikethrough = false: 27 * active = S, strike removed 28 * 29 * The JS is enqueued conditionally by Core::enqueue_frontend_patch (only 30 * when this module is enabled). 31 */ 32(function () { 33 "use strict"; 34 35 var settings = (typeof hrtkBWMR !== "undefined" && hrtkBWMR) || {}; 36 var showStrikethrough = settings.show_strikethrough !== false; // default true 37 38 function parsePrice(text) { 39 if (!text) return null; 40 var match = String(text).match(/[\d.,]+/); 41 if (!match) return null; 42 var raw = match[0]; 43 44 var lastDot = raw.lastIndexOf("."); 45 var lastComma = raw.lastIndexOf(","); 46 47 if (lastDot === -1 && lastComma === -1) { 48 var n = parseFloat(raw); 49 return isNaN(n) ? null : n; 50 } 51 52 var lastSepChar = lastDot > lastComma ? "." : ","; 53 var afterLast = raw.slice(raw.lastIndexOf(lastSepChar) + 1); 54 55 var decimalSep, thousandsSep; 56 if (afterLast.length <= 2) { 57 decimalSep = lastSepChar; 58 thousandsSep = lastSepChar === "." ? "," : "."; 59 } else { 60 thousandsSep = lastSepChar; 61 decimalSep = lastSepChar === "." ? "," : "."; 62 } 63 64 var normalized = raw.split(thousandsSep).join("").replace(decimalSep, "."); 65 var value = parseFloat(normalized); 66 return isNaN(value) ? null : value; 67 } 68 69 function formatLikeTemplate(value, templateText) { 70 var template = String(templateText); 71 var match = template.match(/[\d.,]+/); 72 if (!match) { 73 return value.toFixed(2); 74 } 75 76 var raw = match[0]; 77 var prefix = template.slice(0, match.index); 78 var suffix = template.slice(match.index + raw.length); 79 80 var lastDot = raw.lastIndexOf("."); 81 var lastComma = raw.lastIndexOf(","); 82 83 var decimalSep, thousandsSep, decimals; 84 if (lastDot === -1 && lastComma === -1) { 85 decimalSep = "."; 86 thousandsSep = ","; 87 decimals = 0; 88 } else { 89 var lastSepChar = lastDot > lastComma ? "." : ","; 90 var afterLast = raw.slice(raw.lastIndexOf(lastSepChar) + 1); 91 if (afterLast.length <= 2) { 92 decimalSep = lastSepChar; 93 thousandsSep = lastSepChar === "." ? "," : "."; 94 decimals = afterLast.length; 95 } else { 96 thousandsSep = lastSepChar; 97 decimalSep = lastSepChar === "." ? "," : "."; 98 decimals = 2; 99 } 100 } 101 102 var fixed = value.toFixed(decimals); 103 var parts = fixed.split("."); 104 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep); 105 return prefix + parts.join(decimalSep) + suffix; 106 } 107 108 function reconcileRentUnit(rentUnit) { 109 if (!rentUnit) return; 110 111 var del = rentUnit.querySelector("del.rent-deleted"); 112 var active = rentUnit.querySelector(".rent-after-date.rent-amount"); 113 114 // No del = no buggy strike+active pair. Either no discount applied or 115 // HR didn't render a strike â nothing to fix. 116 if (!del || !active) return; 117 118 var strikeText = del.textContent; 119 var activeText = active.textContent; 120 var fingerprint = strikeText + "|" + activeText; 121 122 // Idempotency: skip if we've already reconciled this exact pair 123 if (rentUnit.dataset.hrtkBwmrFor === fingerprint) return; 124 125 if (!showStrikethrough) { 126 // Just promote the strike (correct post-discount value) into the 127 // active span and remove the strike. 128 active.textContent = strikeText; 129 if (del.parentNode) del.parentNode.removeChild(del); 130 rentUnit.dataset.hrtkBwmrFor = "no-strike"; 131 return; 132 } 133 134 var S = parsePrice(strikeText); 135 var A = parsePrice(activeText); 136 137 if (S == null || A == null || S <= A) { 138 // Defensive fallback: just promote strike to active. 139 active.textContent = strikeText; 140 if (del.parentNode) del.parentNode.removeChild(del); 141 rentUnit.dataset.hrtkBwmrFor = "fallback"; 142 return; 143 } 144 145 var preDiscount = 2 * S - A; 146 var newStrikeText = formatLikeTemplate(preDiscount, strikeText); 147 148 del.textContent = newStrikeText; 149 active.textContent = strikeText; 150 rentUnit.dataset.hrtkBwmrFor = newStrikeText + "|" + strikeText; 151 } 152 153 function sweep(root) { 154 var rentUnits = root.querySelectorAll(".rent-unit"); 155 for (var i = 0; i < rentUnits.length; i++) { 156 reconcileRentUnit(rentUnits[i]); 157 } 158 } 159
160 function init() { 161 sweep(document); 162 163 var observer = new MutationObserver(function () { 164 sweep(document); 165 }); 166 167 observer.observe(document.body, { 168 childList: true, 169 subtree: true, 170 characterData: true, 171 }); 172 } 173 174 if (document.readyState === "loading") { 175 document.addEventListener("DOMContentLoaded", init); 176 } else { 177 init(); 178 } 179})();
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.