Merge branch 'develop' into feature/jnou/more-fixes-playwright

This commit is contained in:
JennyNou 2026-04-22 13:19:56 -04:00
commit b123b2e17f
20 changed files with 165 additions and 50 deletions

View file

@ -14,6 +14,7 @@ const GaEvents = Object.freeze({
const GaCategories = Object.freeze({
API_RESPONSE: 'Api_Response',
BAILOUT: 'Bailout',
CONFIRMATION_CLICKED: "confirmation clicked",
CONFIRMATION_CLICKED_INSHOP: "inshop confirmation clicked",
CONFIRMATION_CLICKED_MOBILE: "mobile confirmation clicked",
@ -57,4 +58,10 @@ const analyticsServicePackageMap = new Map([
[packageNames.TIER_THREE, 'essentialsplus']
]);
export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap };
const analyticsVapsModalMap = new Map([
['FrontWiperModal', 'wiper'],
['RearWiperModal', 'wiper'],
['RainDefenseModal', 'raindefense'],
]);
export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap, analyticsVapsModalMap };

View file

@ -28,6 +28,20 @@ export function getServiceLineItems(order) {
];
}
/**
* Returns an array containing only the
* @param {object} order order object
* @returns {[]} array of service line items
*/
export function getAvailableLineItems(order) {
const { glassParts, supportingItems } = getLineItems(order);
const availableLineItems = [
...(glassParts ?? []),
...(supportingItems ?? [])
];
return availableLineItems;
}
/**
* Returns an array containing only the non-service line items (Vaps and Fees)
* @param {object} order order object

View file

@ -74,11 +74,6 @@ export async function getZipCodeData(zipCode) {
}
export function isServiceableMobileWithData(isGlassServiceableMobile, isRecalibrationServiceableMobile) {
const isBigTruck = useMainStore().order.serviceLocation.isBigTruck;
if (isBigTruck) {
return false;
}
if (isRecalibrationServiceableMobile === true || isRecalibrationServiceableMobile === false) {
return (
isGlassServiceableMobile

View file

@ -42,7 +42,7 @@
linkType="text"
text="Remove"
href="javascript:void(0)"
@clickEvent="removeVap(item.partType)">
@clickEvent="removeVap(item.partType, item?.name ?? '')">
<template #after-text>
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
@ -367,8 +367,15 @@ export default {
?.filter((vapsLineItem) => vapsLineItem.partType === partType) ?? [];
return this.getCartItem(label, lineItems, cartItemType.VAP, partType);
},
removeVap(partType) {
removeVap(partType, partName) {
const newVaps = useMainStore().lineItems.vaps?.filter((vap) => vap?.partType !== partType) ?? [];
if (partType === partTypeStrings.FRONT_WIPER) {
this.pushEventToGA('Removed From Cart', 'Product - Front Wiper', 'Safelite advanced', true);
} else if (partType === partTypeStrings.REAR_WIPER) {
this.pushEventToGA('Removed From Cart', 'Product - Rear Wiper', 'Safelite advanced', true);
} else {
this.pushEventToGA('Removed From Cart', `Product - Other - ${partName}`, 'none', true);
}
useMainStore().updateVaps(newVaps);
useMainStore().updateServicePackage(null);
},

View file

@ -88,8 +88,10 @@ import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import textboxQuestion from '@/digital-components/textbox-question/textbox-question.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
// Supporting files
import analyticsMixIn from '@/mixins/analytics-mixin.js';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { GaCategories } from '@/constants/analytics';
import globalRules from '@/constants/global-rules';
import settleAllPromises from '@/helpers/layout-helper';
import { useMainStore } from '@/store';
@ -111,8 +113,17 @@ export default {
Form,
textBlock
},
mixins: [BaseFormMixin],
mixins: [BaseFormMixin, analyticsMixIn],
async beforeRouteEnter(to, from, next) {
const fromPage = from?.query?.issPage || 'external';
// Only log bailout event if user is coming from a different page.
// This ensures that we cannot get stuck in a loop, if the bailout-page ever bails out.
if (fromPage !== issPageValues.BAILOUT_PAGE) {
const bailoutCode = useMainStore().pageData(issPageValues.BAILOUT_PAGE).bailoutCode;
const bailoutString = Object.keys(BailoutCode).find(key => BailoutCode[key] === bailoutCode);
analyticsMixIn.methods.pushEventToGA(GaCategories.BAILOUT, bailoutString, fromPage, true, null, null);
}
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results

View file

@ -145,6 +145,7 @@ import widgetFields from '@/constants/cms-widget-fields.js';
import { formatAmountInDollars, createUnorderedListFromStringOfParagraphs, createOrderedListFromStringOfParagraphs } from '@/helpers/text-helper.js';
import showIssLoadingModal from '@/helpers/loading-modal-helper';
import { getPriceOfLineItems } from '@/helpers/price-calculator';
import { getAvailableLineItems } from '@/helpers/cart-helper';
import coverageStatuses from '@/constants/coverage-statuses';
import coverageType from '@/constants/coverage-type';
import oemEndorsementModal from '@/layouts/coverage-statement/oem-endorsement-modal/oem-endorsement-modal.vue';
@ -470,11 +471,7 @@ export default {
this.isPageLoading = false;
},
async getPricedParts() {
const { glassParts, supportingItems } = this.mainStore.order.lineItems;
const availableLineItems = [
...(glassParts ?? []),
...(supportingItems ?? [])
];
const availableLineItems = getAvailableLineItems(this.mainStore.order);
// We only call the ITAC pricing endpoint if we are not repair or we are NoComp
if (!this.isRepair || this.mainStore.isNoComp) {
@ -494,6 +491,7 @@ export default {
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD);
} else if (this.isITACQuoteVisible || this.isNoCompQuoteVisible) {
this.mainStore.updateIsSafeliteProvider(true);
this.pushEventToGA('ShopSelection', 'Safelite', this.mainStore.currentDeductible + `_` + getPriceOfLineItems(getAvailableLineItems(this.mainStore.order)), true);
this.navigateWithScenario(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE);
} else {
this.$router.navigateBailout(bailoutMessage.coverageStatementInvalidState());

View file

@ -665,7 +665,7 @@ export default {
// TODO: Check if we're coming from SFA and add relevant events
// eslint-disable-next-line
if (false) {
this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'SFA', null);
this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'SFA', true);
}
else {
this.pushEventToGA('visitor_info_confirmation', 'referring_site', 'ClientSite', true);

View file

@ -20,6 +20,7 @@ import buttonQuestion from '@/digital-components/button-question/button-question
import widgetFields from '@/constants/cms-widget-fields.js';
import paymentMethodListButton from './payment-method-list-button/payment-method-list-button.vue';
import { markRaw } from 'vue';
import { analyticsPaymentTypeMap } from '@/constants/analytics';
export default {
name: 'payment-method-question',
@ -43,6 +44,8 @@ export default {
return this.modelValue;
},
set(selectedMethod) {
const paymentMethod = analyticsPaymentTypeMap.get(this.piaType) ?? 'UNKNOWN';
this.pushEventToGA('submit_order', 'payment_type_selected', paymentMethod, true);
this.$emit('update:modelValue', selectedMethod);
}
},

View file

@ -156,6 +156,9 @@ import { getCartTotal } from '@/helpers/cart-helper';
import { formatAmountInDollars } from '@/helpers/text-helper';
import widgetFields from '@/constants/cms-widget-fields';
import MaskaFormattedMasks from '@/constants/maska-masks';
import { containsRecalParts } from '@/helpers/recal-helper';
import coverageType from '@/constants/coverage-type';
import partTypeStrings from '@/constants/part-type-strings';
export default {
name: 'payment-method',
@ -197,6 +200,9 @@ export default {
vm.updateFooterButtonText(vm.customCallToActionButtonCopy);
});
},
mounted() {
this.handleAnalyticsEventsOnMounted();
},
data() {
return {
paymentMethod: null,
@ -411,6 +417,7 @@ export default {
this.$refs.siteFooter.updateButtonText(newValue);
},
async forwardButtonAction() {
this.handleAnalyticsEventsOnForwardButtonAction();
this.mainStore.savePaymentMethodChoice(this.paymentMethod);
this.savePageDataToStore(issPageValues.PAYMENT_METHOD, { smsOptIn: this.smsOptIn });
if (!this.hideSMSOptIn) {
@ -460,6 +467,7 @@ export default {
handleEditClicked(section) {
switch (section) {
case 'location':
this.pushEventToGA('order_summary', 'change', 'location', true);
const scenario = this.mainStore.isMobileAppointment
? this.navigationScenarios.EDIT_SERVICE_LOCATION_MOBILE
: this.navigationScenarios.EDIT_SERVICE_LOCATION_INSHOP;
@ -469,6 +477,7 @@ export default {
);
break;
case 'schedule':
this.pushEventToGA('order_summary', 'change', 'time', true);
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_SCHEDULE,
this.$route
@ -499,6 +508,58 @@ export default {
},
closeContactDetailsWithoutSaving() {
this.smsOptIn = 'No';
},
handleAnalyticsEventsOnMounted() {
const mobileOrInshop = this.mainStore.isMobileAppointment ? 'mobile' : 'in_shop';
const verifiedOrNotVerified = this.mainStore.isVerified ? 'verified' : 'not_verified';
const repairOrReplace = this.mainStore.damage.isRepair ? 'repair' : 'replace';
this.pushEventToGA('order_summary', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true);
if (containsRecalParts(this.mainStore.lineItems)) {
let coverageTypeForLabel = '';
if (this.mainStore.isITAC) {
coverageTypeForLabel = 'ITAC';
} else if (this.mainStore.isNoComp) {
coverageTypeForLabel = 'no_comp';
} else if (this.mainStore.isVerified) {
coverageTypeForLabel = 'verified';
} else {
coverageTypeForLabel = 'unverified';
}
const recalibrationType = this.mainStore.lineItems.glassParts?.find((part) => part.requiresRecalibration)?.recalibrationType;
this.pushEventToGA(`recalibration_added_${coverageTypeForLabel}`, this.mainStore.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true);
}
const hasFrontWiper = this.mainStore.lineItems?.vaps?.some((part) => part.partType === partTypeStrings.FRONT_WIPER);
const hasRearWiper = this.mainStore.lineItems?.vaps?.some((part) => part.partType === partTypeStrings.REAR_WIPER);
const wiperEventLabel = `front_${hasFrontWiper ? 'advanced' : 'none'}_rear_${hasRearWiper ? 'advanced' : 'none'}`;
this.pushEventToGA('wipers', 'attached_to_order', wiperEventLabel, true);
},
handleAnalyticsEventsOnForwardButtonAction() {
this.pushEventToGA('order_summary', 'edit', 'contact_details', true);
let action;
let label;
switch (this.mainStore.insuranceCoverage?.coverageType) {
case coverageType.Deductible:
action = 'Covered';
label = this.mainStore.currentDeductible;
break;
case coverageType.ITAC:
action = 'ITAC';
label = this.mainStore.currentDeductible + "_" + getCartTotal(this.mainStore.order);
break;
case coverageType.NO_COMP:
action = 'NOCOMP';
label = 'Unverified';
break;
case coverageType.NONE:
default:
action = 'Unverified';
label = 'Unverified';
break;
d
}
this.pushEventToGA('Submit Appointment', action, label, true, null, 1);
}
}
};

View file

@ -201,6 +201,7 @@ export default {
policyVehicleId: vehicle.id,
vin: this.selectedVehicleVin
});
useMainStore().updateVehicleCoverage({
noCoverage: this.noCoverageForSelectedVehicle,
deductible: this.deductibleForSelectedVehicle,
@ -208,6 +209,9 @@ export default {
endorsements: this.endorsementsForSelectedVehicle,
cvrgEndorsementCode: this.endorsementCodesForSelectedVehicle
});
this.pushEventToGA("policy_vehicle", "submitted", useMainStore().order.vehicle.make + "_" + useMainStore().order.vehicle.model + " " + useMainStore().order.vehicle.carId, true);
this.navigateForward();
} catch (e) {
if (e.isAxiosError && e.status === 404) {
@ -223,6 +227,9 @@ export default {
vin: vehicle.vin
});
this.policyVinFound = false;
this.pushEventToGA("policy_vehicle", "submitted", useMainStore().order.vehicle.make + "_" + useMainStore().order.vehicle.model + " " + useMainStore().order.vehicle.carId, true);
this.navigateForward();
return;
}

View file

@ -67,6 +67,8 @@ import showIssLoadingModal from '@/helpers/loading-modal-helper';
import bailoutMessage from '@/constants/bailoutMessage';
import baseFormMixin from '@/mixins/base-form-mixin';
import PROVIDER_PREFERENCE_OPTIONS from '@/constants/provider-preference';
import { getPriceOfLineItems } from '@/helpers/price-calculator';
import { getAvailableLineItems } from '@/helpers/cart-helper';
// Import Component
import { Form } from 'vee-validate';
@ -159,12 +161,14 @@ export default {
this.mainStore.updateIsSafeliteProvider(true);
const scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE;
this.pushEventToGA('prefered_provider', 'Safelite', this.mainStore.accountNameForEvents, true);
this.pushEventToGA('ShopSelection', 'Safelite', this.mainStore.currentDeductible + `_` + getPriceOfLineItems(getAvailableLineItems(this.mainStore.order)), true);
this.navigateForward(scenario);
},
scheduleWithTPA() {
this.mainStore.updateIsSafeliteProvider(false);
const scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED;
this.pushEventToGA('prefered_provider', 'TPA', this.mainStore.accountNameForEvents, true);
this.pushEventToGA('ShopSelection', 'Other', this.mainStore.currentDeductible + `_` + getPriceOfLineItems(getAvailableLineItems(this.mainStore.order)), true);
this.navigateForward(scenario);
},
findAnotherShopClicked() {

View file

@ -961,12 +961,9 @@ export default {
}
this.pushEventToGA('appointment', 'availability_mobile', `days_out_${dateDiffString}`, true, null, null);
},
pushServiceTypeEvent() {
pushServiceTypeEvent() {
if (this.selectedAppointmentType) {
const gaScheduleType = {
['service_type']: this.selectedAppointmentType.toLowerCase()
};
this.pushGenericObjectToGA(gaScheduleType);
this.pushEventToGA('appointment', 'service_type', this.selectedAppointmentType.toLowerCase(), true, null, null);
}
},
async refreshDatePicker() {

View file

@ -249,10 +249,6 @@ export default {
return this.isGlassServiceableInshop;
},
isServiceableMobile() {
if (this.isBigTruck) {
return false;
}
if (this.isRecalibrationServiceableMobile === true || this.isRecalibrationServiceableMobile === false) {
return (
this.isGlassServiceableMobile

View file

@ -115,7 +115,8 @@ export default {
},
textLinkEmit(copy) {
this.$parent.$emit('link-event', {
args: getRouterLinkRouteFromCopy(copy)
route: getRouterLinkRouteFromCopy(copy),
packageName: this.value
});
}
}

View file

@ -81,6 +81,7 @@ import servicePackageQuestion from '@/layouts/service-packages/service-package-q
import issPageValues from '@/router/router-constants/issPage-values';
import bailoutMessage from '@/constants/bailoutMessage';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import { analyticsServicePackageMap, analyticsVapsModalMap } from '@/constants/analytics';
const store = useMainStore();
@ -205,8 +206,11 @@ export default {
}
},
methods: {
openModalAction(modalName) {
this.$refs[modalName.args].openModal();
openModalAction(eventData) {
this.$refs[eventData.route].openModal();
const packageName = analyticsServicePackageMap.get(eventData.packageName) ?? 'UNKNOWN_PACKAGE';
const modalName = analyticsVapsModalMap.get(eventData.route) ?? 'UNKNOWN_MODAL';
this.pushEventToGA('service_package', 'modal_displayed', `${packageName}_${modalName}_modal_displayed`, true);
},
arePagePrerequisitesValid() {
return (
@ -238,6 +242,9 @@ export default {
store.updateVaps(this.selectedVaps);
store.updateServicePackage(this.selectedPackageTier);
const packageName = analyticsServicePackageMap.get(this.selectedPackageTier) ?? 'UNKNOWN_PACKAGE';
this.pushEventToGA('service_package', 'attached_a_package', packageName, true);
const scenario = store.isMobileAppointment
? this.navigationScenarios.CLICKED_FORWARD_MOBILE
: this.navigationScenarios.CLICKED_FORWARD_INSHOP;

View file

@ -270,17 +270,17 @@ export default {
this.mainStore.applicationUser.firstHit = false;
}
// TODO: Check if we're coming from SFA
// TODO: Check if we're coming from SFA - site does not support SFA yet, add this back when it is enabled.
// eslint-disable-next-line
if (false) {
this.pushEventToGA("co_branded", "welcome_clicked_cta", "yes_clicked", 0);
this.pushEventToGA("visitor_info_welcome", "referring_site", "SFA", null);
this.pushEventToGA("co_branded", "welcome_clicked_cta", "yes_clicked", true, null, '0');
this.pushEventToGA("visitor_info_welcome", "referring_site", "SFA", true);
}
else {
this.pushEventToGA("visitor_info_welcome", "referring_site", "ClientSite", null);
this.pushEventToGA("visitor_info_welcome", "referring_site", "ClientSite", true);
}
this.pushEventToGA("visitor_info_welcome", "client_name", this.mainStore.accountNameForEvents, null);
this.pushEventToGA("visitor_info_welcome", "client_name", this.mainStore.accountNameForEvents, true);
},
computed: {
DamageCauseOptions() {
@ -358,6 +358,18 @@ export default {
}
promises.push(this.mainStore.getCoveragePolicyInfo());
await Promise.all(promises);
this.pushEventToGA("policy_search", "policy_found", this.mainStore.isPolicyLookupSuccessful ? "Yes" : "No", true);
if ( this.mainStore.isPolicyLookupSuccessful) {
this.pushEventToGA("zip_validation", "success", "N/A", true);
}
else {
if ( this.mainStore.order.policy.policyLookupErrorCode === 2) {
this.pushEventToGA("zip_validation", "fail", "N/A", true);
}
}
await saveSession({ shouldAwaitSaveSessionQueue: true, bailoutOnError: true })
this.navigateForward();
},

View file

@ -20,7 +20,8 @@ import {
GaEvents,
ValueToLogTypes
} from '@/constants/analytics';
import { getCartTotal, getSubtotal } from '@/helpers/cart-helper';
import { getPriceOfLineItems } from '@/helpers/price-calculator';
import { getAvailableLineItems, getCartTotal, getSubtotal } from '@/helpers/cart-helper';
import { getRecalPartNumbers, isRecalOrder } from "@/helpers/recal-helper";
import coverageStatuses from '@/constants/coverage-statuses';
import coverageType from '@/constants/coverage-type';
@ -128,15 +129,6 @@ export default {
this.logPageView(analyticsPageEvents.ENTRY);
},
pushValueToGA() {
const gaServiceType = {
['service_type']: useMainStore().order?.serviceLocation?.appointmentType?.toLowerCase()
};
if (gaServiceType && gaServiceType['service_type']) {
this.pushGenericObjectToGA(gaServiceType);
}
},
pushOrderToDataLayer() {
// helper check for if an object is defined (but maybe falsey)
const isDefined = (x) => x !== null && x !== undefined;
@ -282,7 +274,7 @@ export default {
payload.priceTotal = "";
// ITAC or NoComp (where cash price is shown)
} else if (store.isVerified && (store.isITAC || store.isNoComp)) {
const subtotal = getSubtotal(order).toFixed(2);
const subtotal = getPriceOfLineItems(getAvailableLineItems(order)).toFixed(2);
payload.priceSubTotal = parseFloat(subtotal);
const total = getCartTotal(order).toFixed(2);
payload.priceTotal = parseFloat(total);
@ -292,7 +284,7 @@ export default {
}
// Cash Quote or Cash Price Sub Total
payload.cashPriceSubTotal = getSubtotal(order).toString();
payload.cashPriceSubTotal = getPriceOfLineItems(getAvailableLineItems(order)).toString();
// Recalibration
payload.isRecalibrationOnOrder = isRecalOrder(order.lineItems);

View file

@ -190,9 +190,6 @@ router.afterEach(async (to, from) => {
// Push experiments to Data Layer
analyticsMixin.methods.pushExperimentsToDataLayer();
// Push values to GA
analyticsMixin.methods.pushValueToGA();
// Push current order status to Data Layer
analyticsMixin.methods.pushOrderToDataLayer();
}

View file

@ -9,7 +9,7 @@ const routerTitles = Object.freeze({
[issPageValues.ADDRESS_LOOKUP]: `Address Lookup ${titleSuffix}`,
[issPageValues.ADDRESS_VEHICLES]: `Address Vehicles ${titleSuffix}`,
[issPageValues.BAILOUT_PAGE]: `Bailout Page ${titleSuffix}`,
[issPageValues.BAILOUT_PAGE]: `Need Help ${titleSuffix}`,
[issPageValues.CAPABILITY_QUESTIONS]: `Capability Questions ${titleSuffix}`,
[issPageValues.CONTACT_CONFIRMATION]: `Contact Confirmation ${titleSuffix}`,
[issPageValues.CONTACT_DETAILS]: `Contact Details ${titleSuffix}`,

View file

@ -112,7 +112,8 @@ export const getDefaultState = () => ({
endorsementQuestionAnswers: [],
cvrgEndorsementCode: null,
status: null,
policyData: null
policyData: null,
policyLookupErrorCode: 0
},
customer: {
address: {
@ -451,7 +452,7 @@ export const useMainStore = defineStore({
.reduce((r, c) => Object.assign(r, c), {}) ?? {},
originalDeductible: (state) => (state.order.damage.isRepair ? state.order.originalDeductible.repair : state.order.originalDeductible.replace),
currentDeductible: (state) => (state.order.damage.isRepair ? state.order.currentDeductible.repair : state.order.currentDeductible.replace),
accountNameForEvents: (state) => state.issConfig.clientName.replaceAll(" ", "").replaceAll("&", "amp")
accountNameForEvents: (state) => state.issConfig.clientName
},
actions:
{
@ -561,6 +562,7 @@ export const useMainStore = defineStore({
console.log(`Coverage lookup attempt #${this.applicationUser.coverageAttempts}. Max attempts allowed: 10.`);
try {
order.policy.policyLookupErrorCode = 0;
const response = await globalMethods.callHttpClient({
method: endpoints.CoveragePolicyInfo.method,
endpoint: endpoints.CoveragePolicyInfo.url,
@ -598,6 +600,9 @@ export const useMainStore = defineStore({
// populate vehicles
order.policy.vehicles = responsePolicy.vehicles ?? [];
} else {
if ( response?.data?.isError) {
order.policy.policyLookupErrorCode = response?.data?.errorCode;
}
this.updateCoverageType(coverageType.NONE);
}
} catch (e) {
@ -2119,6 +2124,7 @@ export const useMainStore = defineStore({
this.order.policy.endorsementQuestionAnswers = [];
this.order.policy.cvrgEndorsementCode = null;
this.order.policy.policyData = null;
this.order.policy.policyLookupErrorCode = 0;
this.order.policy.deductible.repair = null;
this.order.policy.deductible.replace = null;
},