1/** 2 * TourCardProcessors - Tour Data Processing Logic 3 * 4 * Handles all tour data transformation and processing 5 * Mirrors architecture of PricingCalculator.js 6 * Part of the Tour Card Architecture refactor 7 * 8 * METHODS: 9 * - processTourDuration(duration) - Parse and format duration strings 10 * - processTourPrice(tour) - Calculate pricing with commission 11 * - processTourIcons(tour, locations, tags) - Extract icon data 12 * - processTourLocation(tourLocation) - Format location strings 13 * - processActivities(acts) - Parse activity tags 14 * - normalizeRating(peRating) - Normalize ratings to scale 15 * 16 * MIGRATION FROM: 17 * - widget.js (lines 3304-3608) - Original processor functions 18 * 19 * USED IN: 20 * - TourCardIntegration.js - Main entry point for tour data processing 21 * - widget.js - Legacy integration (via TourCardIntegration) 22 * 23 * @class TourCardProcessors 24 * @since 2026-01-12 25 */ 26var TourCardProcessors = { 27 28 /** 29 * Process tour duration data 30 * Converts duration string format "days::hours::minutes" into formatted display text 31 * 32 * @param {string} duration - Duration string in format "days::hours::minutes" 33 * @returns {Object} Object with formatted time string and total duration in minutes 34 * @property {string} raw - Full duration string (e.g., "1 Day 2 Hours 30 Minutes") 35 * @property {string} formatted - Abbreviated duration (e.g., "1 Day 2h 30min") 36 * @property {number} totalMinutes - Total duration in minutes 37 * 38 * @example 39 * processTourDuration("1::2::30") 40 * // Returns: { raw: "1 Day 2 Hours 30 Minutes", formatted: "1 Day 2h 30min", totalMinutes: 1590 } 41 */ 42 processTourDuration: function(duration) { 43 if (!duration) { 44 return { raw: '', formatted: '', totalMinutes: 0 }; 45 } 46 47 duration = duration.split(/::/); 48 var days = parseInt(duration[0], 10); 49 var hours = parseInt(duration[1], 10); 50 var minutes = parseInt(duration[2], 10); 51 var sortTime = 0; 52 var time = ''; 53 54 if (days > 0) { 55 time += days + ' Day' + (days == 1 ? '' : 's') + ' '; 56 sortTime += days * 1440; 57 } 58 if (hours > 0) { 59 time += hours + ' Hour' + (hours == 1 ? '' : 's') + ' '; 60 sortTime += hours * 60; 61 } 62 if (minutes > 0) { 63 time += minutes + ' Minute' + (minutes == 1 ? '' : 's') + ' '; 64 sortTime += minutes; 65 } 66 67 var formattedTime = time.replace(/Hour(s)?/g, 'h').replace(/Minute(s)?/g, 'min'); 68 69 return { 70 raw: time.trim(), 71 formatted: formattedTime.trim(), 72 totalMinutes: sortTime 73 }; 74 }, 75 76 /** 77 * Process tour price data and return calculated values 78 * Handles commission calculation, discount pricing, and currency formatting 79 * 80 * @param {Object} tour - Tour object with price information 81 * @returns {Object} Object containing all price-related calculated values 82 * @property {number} priceNumber - Numeric price value 83 * @property {string} price - Formatted price string with currency 84 * @property {string} discountPrice - Formatted discount price (if applicable) 85 * @property {string} priceText - Price descriptor (e.g., "Per Person") 86 * @property {string} currencySpace - Space between currency symbol and amount 87 * @property {string} hasDiscount - CSS class if discount applies 88 * @property {string} bottomText - Additional price note text 89 * @property {string} tooltipTitle - Tooltip text for price 90 * @property {boolean} hasPrices - Whether tour has valid pricing 91 * @property {boolean} isDiscounted - Whether tour has discount pricing 92 * @property {boolean} isPerPerson - Whether price is per person 93 * @property {Array} newPriceArray - Parsed price array 94 * 95 * @example 96 * processTourPrice({ productPrice: "Per Person::$::100::..." }) 97 * // Returns: { priceNumber: 100, price: "$100", isPerPerson: true, ... } 98 */ 99 processTourPrice: function(tour) { 100 var priceData = { 101 priceNumber: 0, 102 price: '', 103 discountPrice: '', 104 priceText: '', 105 currencySpace: '', 106 hasDiscount: '', 107 bottomText: '', 108 tooltipTitle: '', 109 isoCode: '', 110 hasPrices: false, 111 isDiscounted: false, 112 isPerPerson: false, 113 newPriceArray: [] 114 }; 115 116 if (!tour.productPrice || typeof tour.productPrice !== 'string') { 117 return priceData; 118 } 119 120 var newPriceArray = tour.productPrice.split("::"); 121 priceData.newPriceArray = newPriceArray; 122
123 // Show just the currency glyph with no surrounding spaces (e.g. "US$ " -> "$"); the ISO code 124 // rendered after the price disambiguates which dollar/etc., so the country prefix is redundant. 125 if (typeof newPriceArray[1] === 'string') { 126 var strippedSymbol = newPriceArray[1].replace(/[A-Za-z]/g, '').replace(/\s+/g, ''); 127 if (strippedSymbol !== '') { 128 newPriceArray[1] = strippedSymbol; 129 } 130 } 131 132 var expirationTime = new Date(newPriceArray[6]).getTime(); 133 if (typeof earliestExperiation == 'undefined') { 134 var earliestExperiation = expirationTime; 135 } else if (expirationTime < earliestExperiation) { 136 earliestExperiation = expirationTime; 137 } 138 139 if (tour.hasPrices != "1" || newPriceArray.filter(Boolean).length <= 1) { 140 return priceData; 141 } 142 143 priceData.hasPrices = true; 144 145 if (newPriceArray[1] == "$") { 146 priceData.currencySpace = ""; 147 } else { 148 priceData.currencySpace = " "; 149 } 150 151 var userType = xUtils.checkUserType(); 152 var commissionParams = CommissionParameters.fromLocalStorage(); 153 154 // Hide discount prices if markup is being applied 155 if (commissionParams && commissionParams.getServiceFeeMarkup() > 0) { 156 newPriceArray[4] = ''; 157 } 158 159 if (newPriceArray[3] != "" && newPriceArray[4] == "") { 160 priceData.hasDiscount = ' low-discounted'; 161 } 162 163 if (newPriceArray[4] != "") { 164 priceData.isDiscounted = true; 165 priceData.priceNumber = parseInt(newPriceArray[4]); 166 priceData.discountPrice = newPriceArray[1] + priceData.currencySpace + Number(parseInt(newPriceArray[4])).toLocaleString(); 167 priceData.price = newPriceArray[1] + priceData.currencySpace + Number(parseInt(newPriceArray[2])).toLocaleString(); 168 } else { 169 priceData.price = newPriceArray[1] + Number(parseInt(newPriceArray[2])).toLocaleString(); 170 priceData.priceNumber = parseInt(newPriceArray[2]); 171 172 // Apply markup ONLY when service_fee_markup > 0 173 // Card prices should only show markup, not base commission 174 // Purchase flow (calendar) applies full commission (base + markup) 175 if (commissionParams && commissionParams.getServiceFeeMarkup() > 0) { 176 var original_price = newPriceArray[2]; 177 178 // MIGRATED: Use new PricingCalculator for commission markup calculation 179 var priceNumber = PricingCalculator.calculateCommissionPrice( 180 newPriceArray[2], 181 commissionParams 182 ); 183 priceData.priceNumber = priceNumber; 184 priceData.price = newPriceArray[1] + Number(Math.round(priceNumber)).toLocaleString(); 185 } 186 } 187 188 priceData.priceText = newPriceArray[0]; 189 priceData.isPerPerson = (newPriceArray[0] == "Per Person"); 190 191 if (newPriceArray[3] != "" && newPriceArray[4] == "") { 192 priceData.bottomText = newPriceArray[3]; 193 } 194 195 priceData.tooltipTitle = newPriceArray[7] || ''; 196 priceData.isoCode = (newPriceArray[7] || '').toUpperCase(); 197 198 return priceData; 199 }, 200 201 /** 202 * Process tour icons data (pickup and shore excursion) 203 * Extracts and organizes icon-related information for display 204 * 205 * @param {Object} tour - Tour object with icon-related information 206 * @param {Object} locations - Locations object to populate (passed by reference) 207 * @param {Array} tags - Tags array to populate (passed by reference) 208 * @returns {Object} Object containing all icon-related data 209 * @property {boolean} hasPickup - Whether tour has pickup service 210 * @property {boolean} hasShoreEx - Whether tour is shore excursion 211 * @property {Array} pickupTowns - Array of pickup locations 212 * @property {string} pickupTooltip - Tooltip text for pickup icon 213 * @property {string} pickupIcon - CSS class for pickup icon 214 * @property {string} shoreExTooltip - Tooltip text for shore ex icon 215 * @property {string} shoreExIcon - CSS class for shore ex icon 216 * @property {string} shoreExCssClass - Additional CSS class for shore ex 217 * @property {boolean} hasRefundPolicy - Has cruise refund policy 218 * @property {boolean} hasShipGuarantee - Has ship guarantee 219 * @property {boolean} shouldShowIcons - Whether to display icons section 220 * 221 * @example 222 * processTourIcons(tour, {}, []) 223 * // Returns: { hasPickup: true, pickupTooltip: "Pickup available in: ...", ... } 224 */ 225 processTourIcons: function(tour, locations, tags) { 226 var iconData = { 227 hasPickup: false, 228 hasShoreEx: false, 229 pickupTowns: [], 230 pickupTooltip: '', 231 pickupIcon: 'tour-card__icon--transportation', 232 shoreExTooltip: '', 233 shoreExIcon: '', 234 shoreExCssClass: '', 235 hasRefundPolicy: false, 236 hasShipGuarantee: false, 237 shouldShowIcons: false 238 }; 239 240 if (tour.pickupTowns) { 241 iconData.hasPickup = true; 242 var pickupTownsArray = tour.pickupTowns.split("~~~"); 243 var pickupList = ""; 244 245 for (var x = 0; x < pickupTownsArray.length; x++) { 246 var this_pickup = pickupTownsArray[x].split("::"); 247 248 if (locations[this_pickup[1]] == undefined) { 249 locations[this_pickup[1]] = []; 250 } 251 locations[this_pickup[1]].push(this_pickup[0]); 252 253 pickupList += this_pickup[0] + ", "; 254 255 iconData.pickupTowns.push({ 256 name: this_pickup[0], 257 region: this_pickup[1] 258 }); 259 260 tags.push(this_pickup[0]); 261 tags.push(this_pickup[1]); 262 } 263 264 tags = tags.concat(pickupTownsArray); 265 iconData.pickupTooltip = "Pickup available in: " + pickupList; 266 267 } else if (tour.hotel_pickup == 1) { 268 iconData.hasPickup = true; 269 iconData.pickupTooltip = ''; 270 } 271 272 var hasRefund = (tour.shoreex_refund_policy == "Cruise Friendly Refund Policy"); 273 var hasShipGuarantee = (tour.shoreex_ontime_policy == "Back to Ship Guarantee"); 274 275 if (hasRefund || hasShipGuarantee) { 276 iconData.hasShoreEx = true; 277 iconData.hasRefundPolicy = hasRefund; 278 iconData.hasShipGuarantee = hasShipGuarantee; 279 280 var shoreex_note = ""; 281 var shorex_icon = ""; 282 283 if (hasRefund) { 284 shoreex_note = tour.shoreex_refund_policy; 285 shorex_icon = "tour-card__icon--shore-ex-refund"; 286 } 287 288 if (hasShipGuarantee) { 289 if (shoreex_note.length == 0) { 290 shoreex_note = "Back to Ship Guarantee"; 291 shorex_icon = "tour-card__icon--cruise"; 292 } else { 293 shoreex_note += " & Back to Ship Guarantee"; 294 shorex_icon = "tour-card__icon--shore-ex-ship"; 295 } 296 } 297 298 iconData.shoreExTooltip = shoreex_note; 299 iconData.shoreExIcon = shorex_icon; 300 301 if (iconData.hasPickup) { 302 iconData.shoreExCssClass = "css_shoreex"; 303 } else { 304 iconData.shoreExCssClass = ""; 305 } 306 } 307 308 iconData.shouldShowIcons = (iconData.hasPickup || iconData.hasShoreEx); 309 310 return iconData; 311 }, 312 313 /** 314 * Process tour location data 315 * Formats location string from "city::region" format to display format 316 * 317 * @param {string} tourLocation - Tour location string in format "city::region" 318 * @returns {string} Formatted location string (e.g., "Paris, France") 319 * 320 * @example 321 * processTourLocation("Paris::France") 322 * // Returns: "Paris, France" 323 */ 324 processTourLocation: function(tourLocation) { 325 if (!tourLocation) { 326 return ""; 327 } 328 329 var location = tourLocation.split("::"); 330 return location[0] + ", " + location[1]; 331 }, 332 333 /** 334 * Normalize PE Rating to scale of 10 335 * Ensures rating is within valid range (6-10) 336 * 337 * @param {number} peRating - Original PE Rating 338 * @returns {number} Normalized rating (6-10) 339 * 340 * @example 341 * normalizeRating(2.5) 342 * // Returns: 6 (minimum threshold) 343 * 344 * @example 345 * normalizeRating(4.5) 346 * // Returns: 9 347 */ 348 normalizeRating: function(peRating) { 349 var rating = Math.round(peRating * 2); 350 351 if (rating >= 1 && rating <= 6) { 352 rating = 6; 353 } 354 355 return rating; 356 }, 357 358 /** 359 * Process tour activities/tags data 360 * Parses activity string and extracts activity information 361 * 362 * @param {string} acts - Activities string in format "::::~~~id::name~~~id::name" 363 * @returns {Object} Object containing processed activities data 364 * @property {number} totalActivities - Total number of activities 365 * @property {string} mainActivity - Primary activity name 366 * @property {Array} allActivities - Array of all activity names 367 * @property {Array}
367 activityTags - Raw activity tag strings 368 * @property {boolean} hasActivities - Whether tour has activities 369 * 370 * @example 371 * processActivities("123::Hiking~~~456::Swimming") 372 * // Returns: { 373 * // totalActivities: 2, 374 * // mainActivity: "Hiking", 375 * // allActivities: ["Hiking", "Swimming"], 376 * // hasActivities: true 377 * // } 378 */ 379 processActivities: function(acts) { 380 var activityData = { 381 totalActivities: 0, 382 mainActivity: '', 383 allActivities: [], 384 activityTags: [], 385 hasActivities: false 386 }; 387 388 if (acts == null || acts === '') { 389 return activityData; 390 } 391 392 var actTags = acts.split("~~~"); 393 394 if (actTags[0] === "::::") { 395 actTags.shift(); 396 } 397 398 if (actTags.length > 0 && actTags[0] === "::::") { 399 actTags.shift(); 400 } 401 402 activityData.activityTags = actTags; 403 activityData.totalActivities = actTags.length; 404 405 if (activityData.totalActivities === 0) { 406 return activityData; 407 } 408 409 activityData.hasActivities = true; 410 411 for (var i = 0; i < actTags.length; i++) { 412 var parts = actTags[i].split("::"); 413 if (parts.length >= 2) { 414 var activityName = parts[1]; 415 if (activityName && activityName.trim().length > 0) { 416 activityData.allActivities.push(activityName); 417 } 418 } 419 } 420 421 if (activityData.allActivities.length > 0) { 422 activityData.mainActivity = activityData.allActivities[0]; 423 } 424 425 return activityData; 426 } 427 428}; 429
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.