diff --git a/src/constants/application-config.js b/src/constants/application-config.js index a72b306d3..6a747617f 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -31,6 +31,7 @@ const applicationConfig = { "https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent", FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging", AFFILIATE_COOKIE_CONTAINING_NAME: "_Track", + COOKIE_NAME_LENGTH_MAX_LIMIT: 44, }; export { applicationConfig }; diff --git a/src/constants/cookie-names.js b/src/constants/cookie-names.js index d62934cf5..72e6168fb 100644 --- a/src/constants/cookie-names.js +++ b/src/constants/cookie-names.js @@ -10,6 +10,9 @@ const cookieNames = { DXDEV: "dxdev", SESSION_ID: "sid", SESSION_KEY: "skey", + + //Commission Junction Cookie + CJE: "cje", }; const cookieExpirations = { @@ -17,6 +20,8 @@ const cookieExpirations = { DXDEV: convertToSeconds({ years: 1 }), FUNNEL_USER_ID: convertToSeconds({ weeks: 1 }), FUNNEL_SESSION_KEY: convertToSeconds({ minutes: 30 }), + CJE: convertToSeconds({ days: 395 }), + NON_CJ: convertToSeconds({ days: 100000 }), }; export { cookieNames, cookieExpirations }; diff --git a/src/constants/query-strings.js b/src/constants/query-strings.js index a21d93200..4a5c47943 100644 --- a/src/constants/query-strings.js +++ b/src/constants/query-strings.js @@ -36,6 +36,13 @@ const queryStrings = { VIN_SELECTION: "vinselection", SERVICE_PACKAGE: "servicepackage", NUMBER_OF_CHIPS: "numberofchips", + UTM_SOURCE: "_source", + UTM_MEDIUM: "_medium", + UTM_CAMPAIGN: "_campaign", + CJ_EVENT: "cjevent", + CJUNCTION: "cjunction", + ORGANIC: "organic", + ORGANIC_SOCIAL: "organic_social", }; export { queryStrings }; diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue index 495973144..9e5a43bd6 100644 --- a/src/fmg-components/cart/cart.vue +++ b/src/fmg-components/cart/cart.vue @@ -140,7 +140,7 @@
Promo code {{ promoCode }} applied @@ -184,6 +184,7 @@ export default { allowItemRemoval: Boolean, recyclingModalCmsWidgetName: String, showAsPaid: Boolean, + isInsurance: Boolean, insuranceDeductible: Number, insuranceCompanyName: String, showInsuranceCoverageAs: String, @@ -283,6 +284,7 @@ export default { (lineItemsToKeep) => lineItemsToKeep.cartItemType != cartItemType ); }, + getPromoCodeList() { if (this.$refs["promoModalQuestion"]) { return this.$refs["promoModalQuestion"].getPromoCodeList(); @@ -299,6 +301,14 @@ export default { this.$emit("update:modelValue", newValue); }, }, + lineItemsWithoutRecal() { + if (!this.lineItems || this.lineItems.length < 1) return; + const lineItemsCopy = deepClone(this.lineItems); + lineItemsCopy.supportingItems = baseMixin.methods.filterOutRecalibration( + lineItemsCopy.supportingItems + ); + return lineItemsCopy; + }, showCoverageAsPending() { return this.showInsuranceCoverageAs === coverageStatus.PENDING; }, @@ -427,7 +437,14 @@ export default { packagePrice() { let packagePrice = 0; - if (!this.showCoverageAsVerified && !this.showCoverageAsPending) { + if (!this.isInsurance) { + // CASH ONLY + packagePrice = baseMixin.methods.getTierOnePackagePrice( + baseMixin.methods.filterOutFees( + baseMixin.methods.filterOutRecalibration(this.availableLineItems) + ) + ); + } else if (!this.showCoverageAsVerified && !this.showCoverageAsPending) { packagePrice = baseMixin.methods.getTierOnePackagePrice( baseMixin.methods.filterOutFees(this.availableLineItems) ); @@ -916,8 +933,6 @@ export default { promoCartItems() { const promoCartItems = []; - // clone the promos array - const promosClone = deepClone(this.promo); // get unique promo codes const uniquePromoCodes = [ ...new Set( @@ -983,16 +998,23 @@ export default { return this.getCmsContent("SubtotalTextWidget", "Text"); }, subTotal() { - return baseMixin.methods.getSubTotal(this.lineItems); + if (!this.lineItems || this.lineItems.length < 1) return; + return this.isInsurance + ? baseMixin.methods.getSubTotal(this.lineItems) + : baseMixin.methods.getSubTotal(this.lineItemsWithoutRecal); }, salesTax() { - return baseMixin.methods.getSalesTax(this.lineItems); + if (!this.lineItems || this.lineItems.length < 1) return; + return this.isInsurance + ? baseMixin.methods.getSalesTax(this.lineItems) + : baseMixin.methods.getSalesTax(this.lineItemsWithoutRecal); }, amountDue() { - if (this.showAsPaid) { - return 0; - } - return baseMixin.methods.getAmountDue(this.lineItems); + if (!this.lineItems || this.lineItems.length < 1) return; + if (this.showAsPaid) return 0; + return this.isInsurance + ? baseMixin.methods.getAmountDue(this.lineItems) + : baseMixin.methods.getAmountDue(this.lineItemsWithoutRecal); }, amountPaid() { if (!this.showAsPaid) { diff --git a/src/helpers/heritage-integration/cookie-helper.js b/src/helpers/heritage-integration/cookie-helper.js index 1b7b551ca..0bd4ae563 100644 --- a/src/helpers/heritage-integration/cookie-helper.js +++ b/src/helpers/heritage-integration/cookie-helper.js @@ -1,7 +1,9 @@ import { cookieNames, cookieExpirations } from "@/constants/cookie-names"; import store from "@/store"; import { applicationConfig } from "@/constants/application-config"; - +import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { queryStrings } from "@/constants/query-strings"; +//import { cookieParameters } from "@/constants/cookie-parameters"; /* Will update the cookie if present, or create a new one if not. */ @@ -221,6 +223,79 @@ export function getAffiliateCookies() { return affiliateCookies; } +export function setupAdvertiserTracking() { + var utmSource = getQuerystringParameter(queryStrings.UTM_SOURCE); + var utmMedium = getQuerystringParameter(queryStrings.UTM_MEDIUM); + var utmCampaign = getQuerystringParameter(queryStrings.UTM_CAMPAIGN); + const cjEvent = getQuerystringParameter(queryStrings.CJ_EVENT); + // helper check for if an object is defined (but maybe falsey) + const isDefined = (x) => x !== null && x !== undefined; + if (isDefined(cjEvent) != "") { + const cjeCookieName = cookieNames.CJE; + if (!isCookieSet(cjeCookieName)) { + createCJEventCookie(cjEvent, cjeCookieName); + } + } + //Firstly, Can't continue without utm-source or utm-medium. + //Secondly, Ignore the advertiser for CJ-affiliate-click and Email-click. + if ( + !isDefined(utmSource) || + isDefined(utmSource) == "" || + !isDefined(utmMedium) || + isDefined(utmMedium) == "" || + utmSource?.toLowerCase() == queryStrings.CJUNCTION || + utmMedium?.toLowerCase() == queryStrings.EMAIL || + utmMedium?.toLowerCase() == queryStrings.ORGANIC || + utmMedium?.toLowerCase() == queryStrings.ORGANIC_SOCIAL + ) { + return; + } + //UTC date time format yyyy-MM-dd/HH:mm:ss + const currentDateTime = new Date().toISOString().replace(/T/, "/").replace(/\..+/, ""); + + //Format cookie, by removing spaces and special characters. + utmSource = FormatCookieElementName(utmSource); + utmMedium = FormatCookieElementName(utmMedium); + utmCampaign = FormatCookieElementName(utmCampaign); + + var cookieNameBuilder = utmSource + "_" + utmMedium; + //Since utmCampaign is optional, ignore the rest. + if (isDefined(utmCampaign) && utmCampaign != "") { + cookieNameBuilder = cookieNameBuilder + "_" + utmCampaign; + } + const cookieName = + (cookieNameBuilder.length > applicationConfig.COOKIE_NAME_LENGTH_MAX_LIMIT + ? cookieNameBuilder.substring(0, applicationConfig.COOKIE_NAME_LENGTH_MAX_LIMIT) + : cookieNameBuilder) + applicationConfig.AFFILIATE_COOKIE_CONTAINING_NAME; + + if (!isCookieSet(cookieName)) { + const values = [ + `utm_source= ${utmSource}`, + `utm_medium= ${utmMedium}`, + `utm_campaign= ${utmCampaign}`, + `timestamp= ${currentDateTime}`, + `tagEvent= ${cjEvent}`, + ]; + let valueString = values.join("&"); + createOrUpdateCookie(cookieName, valueString, { + useDefaultFunnelCookieAttributes: true, + maxAge: cookieExpirations.NON_CJ, + }); + } else { + const cookieValue = getCookieValueByName(cookieName); + if (cookieValue) { + let cookieValueObj = convertStringToObject(cookieValue); + cookieValueObj.timestamp = currentDateTime; // Update the property + // Set the updated cookie + const cookieValueString = objectToString(cookieValueObj); + createOrUpdateCookie(cookieName, cookieValueString, { + useDefaultFunnelCookieAttributes: true, + maxAge: cookieExpirations.NON_CJ, + }); + } + } +} + /* =========================== = PRIVATE FUNCTIONS = @@ -253,7 +328,7 @@ function setFunnelCookieProperties(properties) { function createOrUpdateCookie( key, value = "", - { useDefaultFunnelCookieAttributes = true, maxAge, isSecure = true } + { useDefaultFunnelCookieAttributes = true, maxAge, isSecure = true, crossSiteAccess = false } ) { let cookieToAdd = `${key}=${value}; `; @@ -266,6 +341,9 @@ function createOrUpdateCookie( if (!isNaN(maxAge)) { cookieToAdd += `max-age=${maxAge};`; } + if (crossSiteAccess && isSecure) { + cookieToAdd += `sameSite=None;`; + } document.cookie = cookieToAdd; } @@ -315,3 +393,42 @@ function getCookiesContainingName(name) { } return matchingCookies; } +function createCJEventCookie(cjEvent, cookieName) { + createOrUpdateCookie(cookieName, cjEvent, { + useDefaultFunnelCookieAttributes: true, + maxAge: cookieExpirations.CJE, + isSecure: true, + crossSiteAccess: true, + }); +} + +function FormatCookieElementName(cookie) { + // Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + const problemSymbols = '\\s\\(\\)<>@,;:\\\\/\\[\\]\\?=\\{}"'; + const otherSymbols = "!#$%^&+\\-*|~`"; + const symbolsToIgnore = new RegExp("[" + problemSymbols + otherSymbols + "]+", "g"); + + return (cookie || "").replace(symbolsToIgnore, "."); +} + +function convertStringToObject(str) { + let obj = {}; + let pairs = str.split("&"); + + pairs.forEach((pair) => { + let [key, value] = pair.split("="); + obj[key] = value; + }); + + return obj; +} +function objectToString(obj) { + let str = ""; + for (let key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + str += key + "=" + obj[key] + "&"; + } + } + // Remove the trailing '&' + return str.slice(0, -1); +} diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index a5a778cc8..8723ab85a 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -30,6 +30,7 @@ pageName="payment-method" servicePackageOptionsCmsName="ServicePackageTitle" recyclingModalCmsWidgetName="RecycleModal" + :isInsurance="isInsurance" :insuranceDeductible="currentDeductible" :insuranceCompanyName="insuranceCompanyName" :showInsuranceCoverageAs="showInsuranceCoverageAs" /> @@ -525,14 +526,18 @@ export default { await submitWorkOrder({ pageNameToLog: "payment-method", submitAfterSave: true }); this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_FORWARD, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_PIA_ALERT]: false } ); } else { if (this.paymentMethod == paymentMethods.INSURANCE) { this.dispatchStoreAction(this.storeActions.SAVE_PAYMENT_TYPE, true, false); this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_INSURANCE, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_PIA_ALERT]: false } ); } else { this.setupPia(); @@ -554,14 +559,16 @@ export default { } catch (error) { console.log("error: response from pia submit work order:" + error.message); this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - this.$route.params[routerParams.DISPLAY_PIA_ALERT] = true; + this.$route.query[queryStrings.DISPLAY_PIA_ALERT] = true; return; } } this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); this.$router.navigateWithoutSaving( this.navigationScenarios.CLICKED_PAY_NOW, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_PIA_ALERT]: false } ); }, hasSubmittedOrder() { @@ -682,7 +689,7 @@ export default { shouldDisplayPiaAlert() { return ( this.$route.query[queryStrings.DISPLAY_PIA_ALERT] || - this.$route.params[routerParams.DISPLAY_PIA_ALERT] + eval(window.history.state.displayPiaAlert) ); }, // Necessary to make the watcher of lineItems work diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 987caf56d..5dc103c68 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -747,7 +747,7 @@ export default { shouldDisplayPiaAlert(payMethod) { return ( this.$route.query[queryStrings.DISPLAY_PIA_ALERT] === payMethod || - this.$route.params[routerParams.DISPLAY_PIA_ALERT] === payMethod + eval(window.history.state.displayPiaAlert) ); }, }, diff --git a/src/layouts/vehicle-damage/vehicle-damage.spec.js b/src/layouts/vehicle-damage/vehicle-damage.spec.js index b490114da..bc4ab9356 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.spec.js +++ b/src/layouts/vehicle-damage/vehicle-damage.spec.js @@ -310,12 +310,11 @@ describe("vehicle-damage.vue", () => { describe("alert", () => { test("when displayVehicleChangeAlert router params is true, the alert: 'vehicleChangeAlert' should be rendered", () => { // Arrange & Act + window.history.pushState({ displayVehicleChangeAlert: "true" }, "", ""); const { wrapper } = setupMocks({ mountOptionsMockData: { route: { - params: { - [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true, - }, + state: { displayVehicleChangeAlert: true }, }, }, }); @@ -325,21 +324,24 @@ describe("vehicle-damage.vue", () => { test("when displayVehicleChangeAlert router params is false, the alert: 'vehicleChangeAlert' should not be rendered", () => { // Arrange & Act + window.history.pushState({ displayVehicleChangeAlert: "false" }, "", ""); const { wrapper } = setupMocks({ mountOptionsMockData: { route: { - params: { + state: { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false, }, }, }, }); + // Assert expect(wrapper.findComponent({ ref: "vehicleChangeAlert" }).exists()).toBe(false); }); test("when displayVehicleChangeAlert router params is undefined, the alert: 'vehicleChangeAlert' should not be rendered", () => { // Arrange & Act + window.history.pushState({ displayVehicleChangeAlert: "false" }, "", ""); const { wrapper } = setupMocks({ mountOptionsMockData: { route: { @@ -850,7 +852,7 @@ function setupMocks({ pageHeaderWidgetHeaderText, mountOptionsMockData, funnelCo navigateWithSaving: jest.fn(), }, route: { - params: { + state: { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false, }, }, diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index a7dfdfb49..0c35cb88e 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -103,6 +103,7 @@ import { errorMessages } from "@/constants/error-messages"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; +import { routerParams } from "@/router/router-constants/router-params"; import store from "@/store"; import baseMixin from "@/mixins/base-mixin"; @@ -435,20 +436,26 @@ export default { if (skipVin) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_SKIP_VIN, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } ); } else { if (store.getters.damage.isRepair) { this.$router.navigateWithSaving( this.navigationScenarios .CLICKED_FORWARD_WITH_REPAIR_AND_VERIFIED_INSURANCE, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } ); } else { this.$router.navigateWithSaving( this.navigationScenarios .CLICKED_FORWARD_WITH_REPLACE_AND_VERIFIED_INSURANCE, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } ); } } @@ -457,17 +464,23 @@ export default { else if (store.getters.vehicle.vin) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } ); } else if (skipVin) { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITH_SKIP_VIN, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } ); } else { this.$router.navigateWithSaving( this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, - this.$route + this.$route, + {}, + { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: false } ); } }, @@ -596,7 +609,7 @@ export default { ); }, shouldDisplayVehicleChangeAlert() { - return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]; + return eval(window.history.state.displayVehicleChangeAlert); }, shouldHideBackButton() { return this.$store.getters.requiresVerifiedRedirecting; diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index 109f90d04..c86cdacb5 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -102,6 +102,12 @@ export default { }); return filteredLineItems; }, + filterOutRecalibration(lineItems) { + const filteredLineItems = lineItems.filter((item) => { + return item.partType != partTypeStrings.RECALIBRATION; + }); + return filteredLineItems; + }, getTotalPriceOfAllLineItemsAndChildParts(lineItems, includeTax) { let totalPrice = 0; lineItems.forEach((lineItem) => { diff --git a/src/router/index.js b/src/router/index.js index 55f2e60e1..23b61282c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -19,6 +19,7 @@ import { updateSessionIdCookie, deleteFunnelCookie, getAffiliateCookies, + setupAdvertiserTracking, } from "@/helpers/heritage-integration/cookie-helper"; import { loadSessionIfPresent, saveSession } from "@/helpers/heritage-integration/order-helper"; import { @@ -103,7 +104,7 @@ const routes = [ if (affiliateCookies != null && affiliateCookies.length > 0) { store.commit(storeMutations.UPDATE_AFFILIATE_COOKIES, affiliateCookies); } - + setupAdvertiserTracking(); const loadSessionResponse = await loadSessionIfPresent( to.query.isInsurance != null ? to.query.isInsurance == "true" @@ -286,7 +287,7 @@ router.afterEach(async (to, from) => { store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name); // If saving on navigation is requested, check for saved SessionId or EmailAddress to determine if saving is appropriate - if (eval(to.params.isSavingNavigation)) { + if (eval(window.history.state.isSavingNavigation)) { if ( store.getters.applicationUser.savedSessionId || store.getters.order.customer?.emailAddress @@ -430,7 +431,7 @@ async function navigate( router.push({ name: "root", query: Object.assign(optionalQuery, queryStringsObject), - params: optionalParams, + state: optionalParams, }); } else if (matchingScenarioMap.destinationUrl !== undefined) { navigateToUrl(matchingScenarioMap.destinationUrl, optionalQuery);