diff --git a/package-lock.json b/package-lock.json index df67185e..558ff855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3925,9 +3925,9 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -4696,9 +4696,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -4842,7 +4842,9 @@ } }, "node_modules/@vitest/coverage-v8/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -7017,7 +7019,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8048,7 +8052,9 @@ } }, "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { @@ -8341,7 +8347,9 @@ } }, "node_modules/cssnano/node_modules/yaml": { - "version": "1.10.2", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { @@ -8962,7 +8970,9 @@ } }, "node_modules/editorconfig/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -14668,7 +14678,9 @@ } }, "node_modules/js-beautify/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -16002,7 +16014,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.2", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -16739,7 +16753,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "dev": true, "license": "MIT" }, @@ -16769,7 +16785,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -19734,7 +19752,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -20483,7 +20503,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -20565,7 +20587,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/src/constants/experiments.js b/src/constants/experiments.js index c336503b..1c0edd15 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -19,9 +19,18 @@ const experimentSettings = Object.freeze({ ISS_ENABLE_ADYEN_V1: 'ISS_Enable_Adyen_V1' }); +const experimentTest = Object.freeze({ + ISS_MOBILE_FIRST_POPUP_V3: 'ISS_Mobile_First_Popup_V3' +}); + +const experimentVariation = Object.freeze({ + ISS_MOBILE_FIRST_CONTROL_V3: 'NoShowMobileFirstPopUp_V3_CONTROL', + ISS_MOBILE_FIRST_TEST_V3: 'YesShowMobileFirstPopUp_V3_TEST' +}); + const experimentTriggers = Object.freeze({ SITE_ENTRY: 'SiteEntry', PAGE_ENTRY: 'PageEntry' }); -export { experimentUniverses, experimentSettings, experimentTriggers }; +export { experimentUniverses, experimentSettings, experimentTest, experimentVariation, experimentTriggers }; diff --git a/src/helpers/service-location-helper.js b/src/helpers/service-location-helper.js index 13aced81..5da94148 100644 --- a/src/helpers/service-location-helper.js +++ b/src/helpers/service-location-helper.js @@ -73,6 +73,21 @@ export async function getZipCodeData(zipCode) { return await processZipCodeResults(useMainStore().validateZip({ zip: zipCode })); } +export function isServiceableMobileWithData(isGlassServiceableMobile, isRecalibrationServiceableMobile) { + const isBigTruck = useMainStore().order.serviceLocation.isBigTruck; + if (isBigTruck) { + return false; + } + + if (isRecalibrationServiceableMobile === true || isRecalibrationServiceableMobile === false) { + return ( + isGlassServiceableMobile + && isRecalibrationServiceableMobile + ); + } + return !!isGlassServiceableMobile; +} + export function mapTimeSlot(timeSlot, timeOfDay) { return { ...timeSlot, diff --git a/src/layouts/schedule-page/schedule-page.vue b/src/layouts/schedule-page/schedule-page.vue index f0b1a150..2e65643b 100644 --- a/src/layouts/schedule-page/schedule-page.vue +++ b/src/layouts/schedule-page/schedule-page.vue @@ -77,7 +77,7 @@ import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import suggestTimeslotModal from '@/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue'; // Supporting files -import { experimentSettings, experimentUniverses } from '@/constants/experiments'; +import { experimentSettings, experimentTest, experimentVariation, experimentUniverses } from '@/constants/experiments'; import { AppointmentTypeStrings, GET_MOBILE_TIME_SLOTS, @@ -99,6 +99,7 @@ import { getPricedMobileFeePart, getMobileZipCodeData, getZipCodeData, + isServiceableMobileWithData, mapTimeSlot } from '@/helpers/service-location-helper'; import { Form } from 'vee-validate'; @@ -337,7 +338,7 @@ export default { mobileProviderNumber: resultMap.providers?.mobileProviderNumber, serviceabilityDetails: resultMap.serviceabilityDetails, zipCode: initialServiceLocationObj.zipCode - } + }; vm.setCmsContent(resultMap.cmsContent); vm.$refs.serviceLocation.initializeComponent(serviceLocationData); @@ -492,13 +493,18 @@ export default { ); }, async checkMobileFirstExperience(mobileFirstObject = {}) { + const mobileFirstExperimentVariation = this.getExperimentVariationNameByTestName(experimentTest.ISS_MOBILE_FIRST_POPUP_V3); + const isMobileFirstTestVariation = mobileFirstExperimentVariation === experimentVariation.ISS_MOBILE_FIRST_TEST_V3; const showMobileFirst = this.getSettingValue(experimentSettings.ISS_MOBILE_FIRST_SHOW_FIRST_MOBILE_APPOINTMENT); if (showMobileFirst) { const pmMobileDaysLimit = this.getSettingValue(experimentSettings.ISS_MOBILE_FIRST_MAX_PM_MOBILE_DAYS); const mobileDaysLimit = this.getSettingValue(experimentSettings.ISS_MOBILE_FIRST_MAX_MOBILE_DAYS); const hasSelectedDate = !!this.getSelectedDate(); - const isServicableMobile = mobileFirstObject.serviceabilityDetails?.isGlassServiceableMobile; - if (!hasSelectedDate && isServicableMobile && mobileFirstObject.mobileProviderNumber && mobileFirstObject.zipCode) { + const isServiceableMobile = isServiceableMobileWithData( + mobileFirstObject.serviceabilityDetails?.isGlassServiceableMobile ?? null, + mobileFirstObject.serviceabilityDetails?.isRecalibrationServiceableMobile ?? null + ); + if (!hasSelectedDate && isServiceableMobile && mobileFirstObject.mobileProviderNumber && mobileFirstObject.zipCode) { const todayDateObject = new Date(); const todayDateString = convertDateToDateString(todayDateObject); const calendarViewDirection = 'future'; @@ -624,7 +630,11 @@ export default { this.mainStore.logMobileFirstExperimentExposure(); } - this.$refs[SUGGEST_TIMESLOT_MODAL_REF_NAME].openModal(timeSlotForAutoSelectionMobile); + if (isMobileFirstTestVariation) { + this.$refs[SUGGEST_TIMESLOT_MODAL_REF_NAME].openModal(timeSlotForAutoSelectionMobile); + } else { + console.log('In Control group or experiment not found, skip showing popup'); + } } } } diff --git a/src/layouts/schedule-page/service-location/service-location.vue b/src/layouts/schedule-page/service-location/service-location.vue index 227c20bb..7476acd1 100644 --- a/src/layouts/schedule-page/service-location/service-location.vue +++ b/src/layouts/schedule-page/service-location/service-location.vue @@ -252,7 +252,7 @@ export default { return false; } - if (this.isRecalibrationServiceableMobile !== null) { + if (this.isRecalibrationServiceableMobile === true || this.isRecalibrationServiceableMobile === false) { return ( this.isGlassServiceableMobile && this.isRecalibrationServiceableMobile diff --git a/src/mixins/experiment-mixin.js b/src/mixins/experiment-mixin.js index 7f9a761e..fab87d88 100644 --- a/src/mixins/experiment-mixin.js +++ b/src/mixins/experiment-mixin.js @@ -12,6 +12,17 @@ export default { const experiment = useMainStore().applicationUser.experiments.find((e) => e.universeName === experimentName); return experiment ? experiment : null; }, + getExperimentByTestName(testName) { + const experiment = useMainStore().applicationUser.experiments.find((experiment) => experiment.testName === testName); + return experiment ? experiment : null; + }, + getExperimentVariationNameByTestName(testName) { + const experimentByTestName = this.getExperimentByTestName(testName); + if (!experimentByTestName) { + return null; + } + return experimentByTestName.variationName; + }, getSettingValue(settingName) { return this.hasSetting(settingName) ? useMainStore().experimentSettings[settingName] diff --git a/src/store/index.js b/src/store/index.js index aa56015a..f3b21047 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1934,6 +1934,9 @@ export const useMainStore = defineStore({ this.updateMobileFee(fee); } else if (fee.partNumber === partNumberStrings.RECYCLE_FEE) { this.updateRecycleFee(fee); + } else if (fee.partNumber === partNumberStrings.LABOR2) { + // Do not add Labor2 to feeItems since it's already been added to supportingItems + this.addPartNumberFeeItem(null, partNumberStrings.LABOR2); } else { this.addPartNumberFeeItem(fee, fee.partNumber); } @@ -1948,8 +1951,8 @@ export const useMainStore = defineStore({ // Process Recycle Fee this.updateRecycleFee(partsData.find((rf) => rf.partNumber === partNumberStrings.RECYCLE_FEE)); - // Remove RecycleFee and Labor2 from supporting items as they're saved in feeItems; - this.order.lineItems.supportingItems = partsData.filter((i) => i.partNumber !== partNumberStrings.RECYCLE_FEE && i.partNumber !== partNumberStrings.LABOR2); + // Remove RecycleFee from supporting items since it's already been added to feeItems + this.order.lineItems.supportingItems = partsData.filter((i) => i.partNumber !== partNumberStrings.RECYCLE_FEE); }, updateVaps(partsData) { @@ -2792,7 +2795,6 @@ export const useMainStore = defineStore({ ); }, - updateContactInfo(contactInfo) { this.order.customer.firstName = contactInfo?.firstName ?? this.order.customer.firstName; this.order.customer.lastName = contactInfo?.lastName ?? this.order.customer.lastName; @@ -3084,7 +3086,7 @@ export const useMainStore = defineStore({ }); return response.data; }, - + hasSubmittedOrder() { return window.sessionStorage.getItem(webStorageConstants.SUBMITTED_ORDER) !== null; }, diff --git a/src/store/store.spec.js b/src/store/store.spec.js index bd601c24..38a52c16 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -2490,16 +2490,12 @@ describe('Store', () => { }); }); describe('updateSupportingItems method', () => { - it('updateSupportingItems removes Recyle Fee and Labor2 Fee from supporting items in store', () => { + it('updateSupportingItems removes Recycle Fee from supporting items in store', () => { // Arrange const partsData = [ { partNumber: 'RECYCLE FEE', partType: 'REPLACE FEE' - }, - { - partNumber: 'LABOR2', - partType: 'NON_GLASS' } ]; @@ -2507,14 +2503,38 @@ describe('Store', () => { store.updateSupportingItems(partsData); // Assert - expect.not.objectContaining({ - partNumber: 'RECYCLE FEE', - partType: 'REPLACE FEE' - }); - expect.not.objectContaining({ - partNumber: 'LABOR2', - partType: 'NON_GLASS' - }); + expect(store.order.lineItems.supportingItems).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + partNumber: 'RECYCLE FEE', + partType: 'REPLACE FEE' + }) + ]) + ); }); }); -}); + describe('updateGlassFees method', () => { + it('updateGlassFees does not add Labor2 fee to feeItems', () => { + // Arrange + const feeData = [ + { + partNumber: 'LABOR2', + partType: 'LABOR FEE' + } + ]; + + // Act + store.updateGlassFees(feeData); + + // Assert + expect(store.order.lineItems.feeItems).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + partNumber: 'LABOR2', + partType: 'LABOR FEE' + }) + ]) + ); + }); + }); +}); \ No newline at end of file