Merging from develop

This commit is contained in:
Leah Schumann 2022-04-22 07:56:28 -04:00
commit 14f6174a76
47 changed files with 1189 additions and 370 deletions

View file

@ -79,4 +79,44 @@ stages:
__VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId)
# QA Build/Deploy
- stage: Qa
variables:
- group: FixMyGlassQa
jobs:
- deployment: qaBuildDeployment
displayName: Build and Deploy FMG - QA
environment: digitalCloud-qa
container: node
workspace:
clean: all
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: templates/digital/step-build-vue.yml@AzureDevOps
parameters:
buildOutputDir: dist
- template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters:
artifactName: vueDist
awsProfile: $(qaDeploymentProfile)
outputPath: /fmg/
deployBuckets:
safelite-qa-fmg-us-east-1:
clearFolder: true
deployFolder: ''
region: us-east-1
appDeployVariables:
__VUE_APP_CONSUMER_API_GATEWAY__: $(__VUE_APP_CONSUMER_API_GATEWAY__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
cfDistributionId: $(cfDistributionId)

View file

@ -17,11 +17,9 @@ module.exports = {
"!src/layouts/vin-lookup/vin-lookup.vue",
"!src/layouts/vehicle-damage/windshield-damage-type-question/windshield-damage-type-question.vue",
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/address-poc/address-poc.vue",
"!src/layouts/nested-radio-poc/nested-radio.vue",
"!src/layouts/button-question-examples/**/*.vue",
"!src/layouts/part-questions/**/*.vue",
"!src/layouts/reveal/**/*.vue",
"!src/layouts/estimate/**/*.vue",
// REMOVE THESE AFTER WRITING UNIT TESTS
"!src/layouts/address-lookup/customer-questions/customer-questions.vue",
@ -31,7 +29,7 @@ module.exports = {
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 87,
statements: 86,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
},
},

View file

@ -1,6 +1,10 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script>
window.dataLayer = [{}];
</script>
<script><%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY %></script>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
@ -11,9 +15,17 @@
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- Google Tag Manager -->
<noscript>
<iframe src="<%= process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC %>"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>

View file

@ -6,7 +6,9 @@
</div>
<div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName ? groupName + '-radio-group' : ''">
<legend class="sr-only">{{groupName}}</legend>
<legend class="sr-only" :data-focus-target="groupName" tabindex="-1">
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
</legend>
<div :class="getComponentWrapperClasses">
<component
:is="buttonType"
@ -33,6 +35,7 @@
:selectedValues="selectedValues"
data-test="button"
:validationRules="validationRules"
:class="[suppressError ? 'alertError' : '']"
/>
</div>
</fieldset>

View file

@ -22,13 +22,13 @@
isPrimary
:buttonText="buttonText"
loaderColor="white"
:class="isDisabled && 'form-test-invalid'"
:aria-disabled="isDisabled"
:isDisabled="isDisabled"
:class="isForwardActionDisabled && 'form-test-invalid'"
:aria-disabled="isForwardActionDisabled"
:isDisabled="isForwardActionDisabled"
@click-event="buttonClick"
/>
</div>
<div class="col-auto link-col py-1 text-break">
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
<textLink
linkType="navigation"
:text="backLink"
@ -48,8 +48,10 @@ import buttonMain from "@/ux-components/button-main/button-main";
export default {
name: "funnelFooter",
props: {
isDisabled: Boolean,
isForwardActionDisabled: Boolean,
isBackButtonHidden: {type: Boolean, default: false},
cmsWidgetName: String,
},
components: {
textLink,
@ -70,6 +72,9 @@ export default {
beforeUnmount() {
window.removeEventListener('resize', this.onResize);
},
unmounted() {
document.onkeydown = null;
},
computed: {
backLink(){
return this.getCmsContent(this.cmsWidgetName, 'BackButtonText');
@ -87,8 +92,15 @@ export default {
},
removeLoader(){
this.$refs.buttonMain.removeLoader();
document.onkeydown = function (e) {
return true;
};
},
buttonClick() {
//prevent keyboard input after button click
document.onkeydown = function (e) {
return false;
};
this.$emit("ForwardClicked");
},
linkClick() {

View file

@ -1,25 +1,25 @@
<template>
<div class="textbox-question" :class="hasError ? 'has-error' : ''">
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<input v-model="value"
<input v-model="value"
v-maska="mask"
:type="type"
class="form-control"
:ref="inputId"
:id="inputId"
:type="type"
class="form-control"
:ref="inputId"
:id="inputId"
:name="inputId"
:placeholder="placeholderText"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
autocomplete="off"
:placeholder="placeholderText"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
autocomplete="off"
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
:validationRules="validationRules"
@input="handleChange"
@blur="handleBlur" />
<div v-show="errorMessage" class="row mt-2 form-test-error">
<span role="alert">{{ errorMessage }}</span>
</div>
</div>
</div>
</template>
@ -56,7 +56,7 @@ export default {
setup(props) {
const fieldOptions = {
type: "text",
value: props.modelValue,
value: props.modelValue,
};
const {
@ -64,15 +64,17 @@ export default {
handleBlur,
handleChange,
meta,
validate
validate,
errors,
} = useField(props.inputId, props.validationRules, fieldOptions);
return {
return {
errorMessage,
handleBlur,
handleChange,
validate,
meta,
meta,
errors,
};
},
computed: {
@ -86,7 +88,7 @@ export default {
set: function(newValue) {
this.$emit("update:modelValue", newValue);
}
},
},
labelText: {
get: function () {
var thisQuestionText = "";
@ -102,7 +104,7 @@ export default {
return thisQuestionText;
}
}
}
},
watch: {
value(newValue) {

View file

@ -1,5 +1,10 @@
const cookieNames = {
FUNNEL_SESSION_INFO: "FunnelSessionInfo",
// Existing Safelite.com cookies
DXDEV: "dxdev",
SESSION_ID: "sid",
SESSION_KEY: "skey"
};
export { cookieNames };

View file

@ -0,0 +1,7 @@
const dynamicStrings = {
GLOBAL_STATE: "globalState",
CUSTOM: "custom",
ROUTER_LINK: "routerLink"
};
export { dynamicStrings };

View file

@ -67,6 +67,10 @@ const endpoints = {
url: "/location/api/v1/location/zip",
method: "GET",
},
LogExperimentExposureIfAssigned:{
url: "/analytics/api/v1/analytics/log-experiment-exposure",
method: "POST",
}
};
export { endpoints };

View file

@ -19,6 +19,8 @@ const errorMessages = {
EMAIL_ADDRESS_FORMAT: "Please enter a valid email address",
SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP",
VIN_REQUIRED: "Please enter your VIN",
VIN_FORMAT: "Please enter a valid VIN",
};
export { errorMessages };
export { errorMessages };

View file

@ -0,0 +1,6 @@
const experimentUniverses = {
CONCEPT_FUNNEL: 'ConceptFunnel'
};
export { experimentUniverses };

View file

@ -18,6 +18,7 @@ const storeActions = {
LOAD_ORDER: "loadOrder",
SET_REFERRAL_INFORMATION: "setReferralInformation",
VALIDATE_ZIP: "validateZip",
LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure",
// DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",

View file

@ -30,6 +30,7 @@ export default {
}
},
(error) => {
console.error(error);
return reject(error.response);
}
);

View file

@ -1,5 +1,6 @@
import { storeActions } from "@/constants/store-actions.js";
import store from "@/store";
import { dynamicStrings } from "../constants/dynamic-strings";
export function fetchCmsContentForPage(fmgPage) {
return store
@ -40,12 +41,15 @@ export function fetchCmsContentForPage(fmgPage) {
function mapStringToState(str) {
// Pull all matches out of the string.
const regexExp = new RegExp("{(.*?):(.*?)}", "g");
const matches = [...str.matchAll(regexExp)];
const regexMatches = [...str.matchAll(regexExp)];
const globalStateMatches = regexMatches.filter(match => {
return match[1] === dynamicStrings.GLOBAL_STATE;
})
// Our final string value that will be built from the matches.
let stringBuilder = "";
for (const match of matches) {
for (const match of globalStateMatches) {
// Reset store state for each match.
let storeState = store.state;
@ -60,7 +64,7 @@ function mapStringToState(str) {
const stringWithReplacement = str.replace(match[0], storeState);
// If we still have values we need to substitute, call this function again.
if (stringWithReplacement.includes("{globalState:")) {
if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
return mapStringToState(stringWithReplacement);
}
@ -96,7 +100,7 @@ function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === "string") {
if (widgetModel[key].includes("{globalState:")) {
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]);
}
return widgetModel[key];
@ -116,4 +120,4 @@ function processWidgetItemForReplacement(widgetModel, key) {
// If we have something else like a number, boolean, etc. just return it
return widgetModel[key];
}
}

View file

@ -0,0 +1,22 @@
import store from "@/store";
export function getDamageString() {
return store.getters.damage.glassToReplace.length > 1 ? "match" : store.getters.damage.glassToReplace[0].location;
}
export function compareGlassOptions(newOptions, currentOptions){
const optionsMap = {
Windshield: "windshieldOptions",
Driver: "driverSideOptions",
Passenger: "passengerSideOptions",
Rear: "backGlassOptions"
}
for(const option of currentOptions){
if(!newOptions[optionsMap[option.location]].availableReplacementOptions.includes(option.name)){
return true;
}
}
return false;
}

View file

@ -0,0 +1,37 @@
import {getDamageString, compareGlassOptions} from "./damage-helper";
jest.mock("@/store", () => ({
getters: {damage: {
glassToReplace: [{location: "TEST"}]
}
}
}));
describe("damage-helper.js", () => {
it("Should return damage getter info", () => {
const damage = getDamageString();
expect(damage).toEqual("TEST")
});
});
describe("damage-helper.js", () => {
it("Should return false if no mismatches between each array", () => {
const newOptions = {
windshieldOptions: {availableReplacementOptions: ["windshield"]}
}
const currentOptions = [{location: "Windshield", name: "windshield"}];
const misMatch = compareGlassOptions(newOptions, currentOptions);
expect(misMatch).toEqual(false);
});
});
describe("damage-helper.js", () => {
it("Should return true if there are any mismatches between arrays", () => {
const newOptions = {
windshieldOptions: {availableReplacementOptions: ["window"]}
}
const currentOptions = [{location: "Windshield", name: "windshield"}];
const misMatch = compareGlassOptions(newOptions, currentOptions);
expect(misMatch).toEqual(true);
});
});

View file

@ -18,19 +18,20 @@ export function updateOrCreateFunnelCookie() {
ReferralNumber: store.getters.order.referralNumber,
ReferralDate: store.getters.order.referralDate,
ReferralCorrelationId: store.getters.order.referralCorrelationId,
ReferralParentAccountNumber: store.getters.order.parentAccountNumber,
});
}
/*
Gets the current instance of the funnel cookie.
Returns null if cookie isn't valid JSON.
*/
*/
export function getFunnelCookie() {
const cookieJson = document.cookie
?.split("; ")
?.find(row => row.startsWith(`${cookieNames.FUNNEL_SESSION_INFO}=`))
?.split("=")[1];
try {
return JSON.parse(cookieJson);
} catch (error) {
@ -45,6 +46,55 @@ export function deleteFunnelCookie() {
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=; Max-Age=0; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()}`;
}
/*
Gets cookie domain value. Localhost will be empty "".
*/
export function getCookieDomainValue() {
return location.hostname.includes("localhost") ? "" : `domain=${getDomainWithoutSubdomain()};`;
}
/*
Gets value of dxdev cookie, and then extracts "did" value from it.
Returns empty string if cookie not found or "did" string not present.
*/
export function getDeviceIdValue(){
// Sometimes these cookie contains more than the device ID.
const cookieValue = getCookieValueByName(cookieNames.DXDEV);
const cookieValuesSplit = cookieValue.split('=');
// If this is the only value, just use that.
if(cookieValuesSplit.length === 2 && cookieValuesSplit[0] === 'did'){
return cookieValuesSplit[1];
}
const cookieValueMatch = cookieValue.match("^did=[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}");
if(cookieValueMatch){
return cookieValueMatch[0].split('=')[1];
}
return '';
}
/*
Gets value of skey cookie, returns 0 if not found.
*/
export function getSessionKeyValue(){
const cookieValue = getCookieValueByName(cookieNames.SESSION_KEY);
if(cookieValue){
return cookieValue;
}
return 0;
}
/*
===========================
= PRIVATE FUNCTIONS =
===========================
*/
/*
Used to set properties on the funnel cookie.
Takes an object with properties to set. Will overwrite existing properties.
@ -65,10 +115,9 @@ function setFunnelCookieProperties(properties) {
}
}
export function getCookieDomainValue() {
return location.hostname.includes("localhost") ? "" : `domain=${getDomainWithoutSubdomain()};`;
}
/*
Gets current domain without the subdomain for cookie.
*/
function getDomainWithoutSubdomain() {
let url = location.hostname;
if (url.includes("localhost")) {
@ -82,3 +131,16 @@ function getDomainWithoutSubdomain() {
.slice(-(urlParts.length === 4 ? 3 : 2))
.join('.')}`;
}
/*
Gets cookie value by name, returns empty string if not found.
*/
function getCookieValueByName(name) {
const value = "; " + document.cookie;
const parts = value.split("; " + name + "=");
if (parts.length === 2) {
return parts.pop().split(";").shift();
}
return "";
}

View file

@ -1,4 +1,4 @@
import {getFunnelCookie} from "@/helpers/heritage-integration/cookie-helper.js";
import {getFunnelCookie, getDeviceIdValue, getSessionKeyValue} from "@/helpers/heritage-integration/cookie-helper.js";
import { removeAllTestCookies, setupCookies } from "@/helpers/unit-test-helper";
describe("cookies", () => {
@ -100,5 +100,31 @@ describe("cookies", () => {
expect(actualCookieValue).toBeNull();
});
})
describe("getDeviceIdValue", () => {
test("getDeviceIdValue, should return GUID", () => {
// Arrange
setupCookies({});
// Act
const result = getDeviceIdValue();
//Assert
expect(result).toBe('21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe');
});
test("getSessionKeyValue, should return session key int", () => {
// Arrange
setupCookies({});
// Act
const result = getSessionKeyValue();
//Assert
expect(result).toBe('12345');
});
});
})

View file

@ -2,55 +2,38 @@ import { queryStrings } from "@/constants/query-strings";
import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { saveOrder } from "@/helpers/heritage-integration/order-helper.js";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import store from "@/store";
import router from "@/router";
/*
If the user has visited the funnel before this method will determine the bets place to
drop them so they don't start at the beginning again. This method will return 'heritage' if
the user has an existing order and they come back in from the Safelite.com CTA.
the user has an existing order and they come back in from the Safelite.com CTA.
*/
export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) {
// If the user is coming in via the Safelite.Com CTA
if (toRoute.query[queryStrings.START_TYPE] === 'fmg') {
// If they have an existing order, return 'heritage' for the page name.
if (existingHeritageOrder) {
return 'heritage';
}
return await getLatestPageForRedirection();
}
// If this is a non-CTA navigation, determine where to send the user based on page prerequisites.
// This also works if a user has a 'fmg' start_type query string but no current order.
// That shouldn't happen, but it's possible.
const vehicleMakeComponent = (await lazyLoadComponent('vehicle-make')()).default;
const vehicleModelComponent = (await lazyLoadComponent('vehicle-model')()).default;
const vehicleStyleComponent = (await lazyLoadComponent('vehicle-style')()).default;
const vehicleDamageComponent = (await lazyLoadComponent('vehicle-damage')()).default;
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-year";
} else if (!vehicleModelComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-make";
} else if (!vehicleStyleComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-model";
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return "vehicle-style";
} else if (store.getters.damage.isRepair == null || !store.getters.vehicle.carId) {
return 'vehicle-damage'
} else {
if (store.getters.vehicle.vin) {
return 'vehicle-damage';
//return "vin-lookup"; (uncomment)
} else {
return 'vehicle-damage';
//return "estimate" (uncomment)
}
// If navigating to a specific page, and that page is not part of the vin pages.
// Return that page, so that it can navigate like normal.
if (toRoute.query[queryStrings.FMG_PAGE] !== undefined && !isVinRelatedPage(toRoute)) {
return overrideYmmsDirectionIfNeeded(toRoute);
}
// If this is not a direct link to a page using fmgPage, not from Safelite.com CTA or this is a vin related page.
// Get the latest page for redirection.
const latestPageRoute = await getLatestPageForRedirection();
return latestPageRoute;
}
/*
@ -65,10 +48,81 @@ export async function navigateToHeritageFunnel() {
externalUrls.HERITAGE_FUNNEL,
{
corid: store.getters.order.referralCorrelationId,
src: "concept-funnel",
// TODO CSR-28, remove this
cns: "all",
experiments: "RemoveServiceAreaPage=ServAreaRemoval_V7=NoShowPackages_CONTROL=true,ConceptFunnel=ConceptFunnel_V1=ConceptFunnel_TEST=true"
src: "concept-funnel"
}
);
}
}
/*
Logic for getting the last "valid" page a user visited.
*/
async function getLatestPageForRedirection() {
// If this is a non-CTA navigation, determine where to send the user based on page prerequisites.
// This also works if a user has a 'fmg' start_type query string but no current order.
// That shouldn't happen, but it's possible.
const vehicleMakeComponent = (await lazyLoadComponent('vehicle-make')()).default;
const vehicleModelComponent = (await lazyLoadComponent('vehicle-model')()).default;
const vehicleStyleComponent = (await lazyLoadComponent('vehicle-style')()).default;
const vehicleDamageComponent = (await lazyLoadComponent('vehicle-damage')()).default;
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_YEAR;
} else if (!vehicleModelComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MAKE;
} else if (!vehicleStyleComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MODEL;
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_STYLE;
} else if (store.getters.damage.isRepair == null || !store.getters.vehicle.carId) {
return fmgPageValues.VEHICLE_DAMAGE;
} else {
if (store.getters.vehicle.vin) {
return fmgPageValues.VIN_LOOKUP;
} else {
return fmgPageValues.ESTIMATE;
}
}
}
/*
Overrides functionality to go to the YMMS pages in certain cases.
If this is not one of the cases, it returns the 'to' fmgPage value.
*/
/* istanbul ignore next */
function overrideYmmsDirectionIfNeeded(toRoute) {
const fmgPageValue = toRoute.query[queryStrings.FMG_PAGE];
if (store.getters.payment.insuranceCoverage.isVerified) {
switch (fmgPageValue) {
case fmgPageValues.VEHICLE_YEAR:
case fmgPageValues.VEHICLE_MAKE:
case fmgPageValues.VEHICLE_MODEL:
case fmgPageValues.VEHICLE_STYLE:
{
return fmgPageValues.VEHICLE_DAMAGE;
}
default: {
return fmgPageValue;
}
}
} else {
return fmgPageValue;
}
}
/*
Determine if the page is a vin related page.
*/
/* istanbul ignore next */
function isVinRelatedPage(toRoute) {
const fmgPageValue = toRoute.query[queryStrings.FMG_PAGE];
return fmgPageValue === fmgPageValues.VIN_LOOKUP ||
fmgPageValue === fmgPageValues.LICENSE_PLATE_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_VEHICLES ||
fmgPageValue === fmgPageValues.ESTIMATE;
}

View file

@ -228,8 +228,7 @@ describe("getPageToRouteExistingOrderTo", () => {
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
//expect(result).toBe('vin-lookup');
expect(result).toBe('vehicle-damage');
expect(result).toBe('vin-lookup');
});
test("getPageToRouteExistingOrderTo, should return estimate", async () => {
@ -284,8 +283,7 @@ describe("getPageToRouteExistingOrderTo", () => {
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
//expect(result).toBe('estimate');
expect(result).toBe('vehicle-damage');
expect(result).toBe('estimate');
});
test("getPageToRouteExistingOrderTo, existing order, should return heritage", async () => {

View file

@ -25,7 +25,7 @@ export async function loadOrderIfPresent() {
// Load referral if there is a cookie, and it doesn't indicate it needs a state reset.
return (await loadOrder(funnelCookie.ReferralNumber, funnelCookie.ReferralDate, funnelCookie.ReferralCorrelationId)).data;
return (await loadOrder(funnelCookie.ReferralNumber, funnelCookie.ReferralDate, funnelCookie.ReferralCorrelationId, funnelCookie.ReferralParentAccountNumber)).data;
}
/*
@ -54,12 +54,13 @@ export async function saveOrder() {
Calls API to load order given the referral number, referralDate, and referralCorrelationId
and returns the response.
*/
async function loadOrder(referralNumber, referralDate, referralCorrelationId) {
async function loadOrder(referralNumber, referralDate, referralCorrelationId, accountNumber) {
const response = await baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOAD_ORDER,
{
referralNumber: referralNumber.toString(),
referralDate: referralDate,
referralCorrelationId: referralCorrelationId
referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber?.toString()
}, false);
return response;

View file

@ -15,10 +15,10 @@ export function settleAllPromises(promiseResultMap) {
// when returned, so other promises do. Map the results to the object
// so that the object is the return data.
if (results[i].value.data === undefined) {
resultMap[promiseName] = results[i].value;
if (results[i]?.value?.data === undefined) {
resultMap[promiseName] = results[i]?.value;
} else {
resultMap[promiseName] = results[i].value.data;
resultMap[promiseName] = results[i]?.value?.data;
}
}

View file

@ -60,7 +60,9 @@ export const cookies = {
[cookieNames.FUNNEL_SESSION_INFO]: `{"ReferralNumber":"1566818","ReferralDate":"2022-03-15T10:56:24.597","ReferralCorrelationId":"404d2b04-f86e-45c3-b373-127b6217b060","ShouldResetState":false,"DidHeritageFunnelUpdateLast":true}`,
"UNIQUE_SESSION_ID": "33756020-b58e-4ec7-b8b8-3f1576719c40",
"anotherCookie": "{}",
"someOtherCookie": "{}"
"someOtherCookie": "{}",
"dxdev": "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
"skey": "12345"
};
export function removeAllTestCookies() {

View file

@ -44,6 +44,7 @@
ref="funnelFooter"
:isDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
:isForwardActionDisabled="!meta.valid"
/>
</div>

View file

@ -0,0 +1,13 @@
<template>
<p> Estimate </p>
</template>
<script>
export default {
name: "estimate",
methods: {
arePagePrerequisitesValid() {
return true;
},
}
};
</script>

View file

@ -44,7 +44,7 @@
/>
<funnel-footer
ref="funnelFooter"
:isDisabled="!meta.valid"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>

View file

@ -26,7 +26,8 @@
</div>
<alert
class="my-3"
cmsWidgetName="NoServiceZipWidget"
:manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody"
v-if="newServiceZipRequired"
alertClass="alert-danger"
/>
@ -38,7 +39,8 @@
/>
<alert
class="my-3"
cmsWidgetName="MatchedDifferentVehicleAlertWidget"
:manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody"
v-if="vinDoesNotMatchCarId"
alertClass="alert-warning"
/>
@ -50,7 +52,7 @@
<funnelFooter
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isDisabled="!meta.valid"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
@ -75,8 +77,10 @@ import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages";
import { getDamageString, compareGlassOptions } from "@/helpers/damage-helper";
import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
// DEFINE VALIDATION RULES
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
@ -118,8 +122,32 @@ export default {
zip: '',
email: '',
serviceZip: '',
carIdEntered: '',
customAlertData: {},
newCarId: false,
glassOptionsMismatch: false,
};
},
computed: {
MatchedDifferentVehicleAlertHeader(){
let text = this.getCmsContent("MatchedDifferentVehicleAlertWidget", "HeadlineText").replaceAll("{custom:damage}", getDamageString());
return text;
},
MatchedDifferentVehicleAlertBody(){
let text = this.getCmsContent("MatchedDifferentVehicleAlertWidget", "BodyText").replaceAll("{custom:damage}", getDamageString()).replaceAll("{custom:plateLookupYear}", this.customAlertData?.vehicleInfo?.year).replaceAll("{custom:plateLookupMake}", this.customAlertData?.vehicleInfo?.make).replaceAll("{custom:plateLookupModel}", this.customAlertData?.vehicleInfo?.model);
return text;
},
NoServiceZipHeader(){
let text = this.getCmsContent("NoServiceZipWidget", "HeadlineText").replaceAll("{custom:zip}", this.zip);
return text;
},
NoServiceZipBody(){
return this.getCmsContent("NoServiceZipWidget", "BodyText");
}
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
@ -137,43 +165,58 @@ export default {
async forwardButtonAction() {
const zipValidation = this.serviceZip ? await this.validateZip(this.serviceZip) : await this.validateZip(this.zip);
if (!zipValidation.data.isServiceable) {
this.customAlertData.zip = this.zip;
this.$refs.funnelFooter.removeLoader();
this.vinDoesNotMatchCarId = false;
this.vinNotValid = false;
this.newServiceZipRequired = true;
return;
}
}
const vinLookup = await this.lookupVin(this.licensePlate, zipValidation.data.state).catch(() => {
this.$refs.funnelFooter.removeLoader();
this.vinDoesNotMatchCarId = false;
this.vinNotValid = true;
return;
});
if (vinLookup.data.vehicle.carId !== store.getters.vehicle.carId) {
if ((vinLookup.data.vehicle.carId !== store.getters.vehicle.carId) && (vinLookup.data.vehicle.carId !== this.carIdEntered)) {
this.carIdEntered = vinLookup.data.vehicle.carId;
this.customAlertData.vehicleInfo = vinLookup.data.vehicle;
this.newCarId = true;
const glassOptions = await baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: vinLookup.data.vehicle.carId }
);
this.glassOptionsMismatch = compareGlassOptions(glassOptions.data, store.getters.damage.glassToReplace);
this.$refs.funnelFooter.updateButtonText(`Continue with ${vinLookup.data.vin} ${vinLookup.data.vehicle.year} ${vinLookup.data.vehicle.make} ${vinLookup.data.vehicle.model}`);
this.$refs.funnelFooter.removeLoader();
this.vinNotValid = false;
this.vinDoesNotMatchCarId = true;
return;
}
this.updateCustomerInfo(vinLookup.data.vin, vinLookup.data.vehicle, zipValidation.data.state);
const partsData = await baseMixin.methods.dispatchNonBlockingStoreAction(
this.storeActions.GET_PARTS_OR_QUESTIONS,
{
carId: store.getters.vehicle.carId,
glassArray: store.getters.damage.glassToReplace,
zipCode: this.zip,
vin: vinLookup.vin
carId: vinLookup.data.vehicle.carId,
glassArray: store.getters.damage.glassToReplace ? store.getters.damage.glassToReplace : [],
zipCode: this.serviceZip ? this.serviceZip : this.zip,
vin: vinLookup.data.vin
},
false
);
this.navigateForward(partsData);
},
navigateForward(partsData){
if(partsData.data.partsOrQuestions[0].partQuestions && partsData.data.partsOrQuestions[0].partQuestions.length > 0){
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_PARTS_QUESTION, this.$route, {}, {}, partsData.data);
if(this.newCarId && this.glassOptionsMismatch){
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD, this.$route, {}, { displayVehicleChangeAlert: true }, partsData.data);
return;
} else if((!partsData.data.partsOrQuestions[0].partQuestions || partsData.data.partsOrQuestions[0].partQuestions.length < 1) && partsData.data.partsOrQuestions[0].parts.length > 1) {
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_PARTS, this.$route, {}, {}, partsData.data);
return;
} else {
this.$router.navigate(this.navigationScenarios.CONTINUING_WITH_SINGLE_PART, this.$route);
navigateToHeritageFunnel();
return;
}
},
validateZip(zip) {
@ -188,27 +231,32 @@ export default {
{ licensePlate: plate, licenseState: state }
);
},
updateStore() {
// if(vehicleDamage){
// store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
// }
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_YEAR, null);
store.commit(storeMutations.UPDATE_MAKE, null);
store.commit(storeMutations.UPDATE_MODEL, null);
store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, null);
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, null);
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, null);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP, null);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, null);
updateCustomerInfo(vin, vehicleInfo, registrationState) {
if(this.newCarId && this.glassOptionsMismatch){
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, vehicleInfo.year);
store.commit(storeMutations.UPDATE_MAKE, vehicleInfo.make);
store.commit(storeMutations.UPDATE_MODEL, vehicleInfo.model);
store.commit(storeMutations.UPDATE_STYLE, vehicleInfo.style);
store.commit(storeMutations.UPDATE_CAR_ID, vehicleInfo.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicleInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor);
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, this.licensePlate);
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, registrationState);
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.zip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP, this.serviceZip);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
},
},
watch: {
licensePlate() {
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
}
},
components: {
Form,
funnelHeader,

View file

@ -1,10 +1,12 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage="false" />
<funnelSubHeader ref="funnelSubHeader" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<h1>Part Questions Page Placeholder</h1>
<funnelFooter ref="funnelFooter" @back-clicked="backButtonAction" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction"
/>
</div>
</template>
@ -49,18 +51,7 @@ export default {
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.$refs.funnelHeader.initializeComponent(
resultMap.cmsContent.FunnelHeaderWidget
);
vm.$refs.vehicleBanner.initializeComponent(
resultMap.cmsContent.VehicleBannerWidget
);
vm.$refs.funnelSubHeader.initializeComponent(
resultMap.cmsContent.FunnelSubHeaderWidget
);
vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget
);
vm.setCmsContent(resultMap.cmsContent);
});
},
methods: {

View file

@ -36,6 +36,11 @@ jest.mock("@/store", () => ({
vehicle: {
carId: "C00000000",
image: "test.jpg",
payment: {
insuranceCoverage: {
isVerified: false
}
}
},
eventBusItem: jest.fn(),
damage: {
@ -134,6 +139,7 @@ describe("vehicle-damage.vue", () => {
store: {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
},
},
},
@ -201,6 +207,7 @@ describe("vehicle-damage.vue", () => {
store: {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
},
},
},
@ -291,6 +298,7 @@ describe("vehicle-damage.vue", () => {
store: {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
},
},
},
@ -336,6 +344,7 @@ describe("vehicle-damage.vue", () => {
store: {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
},
},
},
@ -734,6 +743,7 @@ function setupMocks({
store: {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
},
},
},

View file

@ -47,7 +47,8 @@
/>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
:isDisabled="!meta.valid"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden=shouldHideBackButton
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
@ -145,9 +146,11 @@ export default {
}
return false;
},
resetDependentState() {
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
backButtonAction() {
// route to move backwards
this.$router.navigate(
@ -155,19 +158,20 @@ export default {
this.$route
);
},
getDamageLocationsFromStore() {
var glassSelections = [];
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD }) ||
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD }) ||
store.getters.damage.isRepair) {
glassSelections.push(damageLocationsSelected.WINDSHIELD);
}
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.DRIVER ||
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.DRIVER ||
glass.location === damageLocationsSelected.PASSENGER })) {
glassSelections.push(damageLocationsSelected.SIDEDOOR);
}
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.REAR })) {
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.REAR })) {
glassSelections.push(damageLocationsSelected.REARWINDOW);
}
@ -180,19 +184,19 @@ export default {
if (store.getters.damage.isRepair === undefined) return windshieldOptions;
if (!store.getters.damage.isRepair) {
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.SINGLE })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE);
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.SINGLE);
}
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.DRIVER })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE);
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.DRIVER);
}
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.PASSENGER })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE);
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.PASSENGER);
@ -210,11 +214,11 @@ export default {
getDoorSidesFromStore() {
var doorSides = [];
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.DRIVER })){
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.DRIVER })){
doorSides.push(damageLocationsSelected.DRIVERSIDE);
}
if (store.getters.damage.glassToReplace.some(glass => { return glass.location === damageLocationsSelected.PASSENGER })){
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.PASSENGER })){
doorSides.push(damageLocationsSelected.PASSENGERSIDE);
}
@ -224,7 +228,7 @@ export default {
getDriverSideReplaceOptionsFromStore() {
var driverSideReplaceOptions = [];
store.getters.damage.glassToReplace.forEach(glass => {
store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.DRIVER){
driverSideReplaceOptions.push(glass.name);
}
@ -236,7 +240,7 @@ export default {
getPassengerSideReplaceOptionsFromStore() {
var passengerSideReplaceOptions = [];
store.getters.damage.glassToReplace.forEach(glass => {
store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.PASSENGER){
passengerSideReplaceOptions.push(glass.name);
}
@ -248,7 +252,7 @@ export default {
getRearReplaceOptionsFromStore(){
var rearReplaceOptions = [];
store.getters.damage.glassToReplace.forEach(glass => {
store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.REAR){
rearReplaceOptions.push(glass.name);
}
@ -275,13 +279,20 @@ export default {
},
navigateForward(partsData){
// CSR-98 TEMP
const vehicleYearsToShowHeritageFunnel = [ "2001", "2002", "2010" ];
// Temporary easter egg to navigate to heritage funnel.
const vehicleYearsToShowHeritageFunnel = [ 2001, 2002, 2010, 2016 ];
if (vehicleYearsToShowHeritageFunnel.includes(store.getters.vehicle.year)) {
navigateToHeritageFunnel();
return;
}
// If vin already exists, navigate directly to vin-lookup
if(this.$store.getters.vehicle.vin){
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, this.$route);
return;
}
//found problem questions
if (partsData.data.partsOrQuestions.some(pq => pq.partQuestions != null && pq.partQuestions.length > 0)){
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_DAMAGE_WITH_PART_QUESTIONS, this.$route, {}, {}, partsData.data);
@ -382,7 +393,7 @@ export default {
return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair;
},
hasSplitSingleConflict() {
if (!this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false;
if (!this.selectedDamageLocations || !this.selectedDamageLocations.includes("Windshield") || !this.selectedWindshieldOptions.selectedWindshieldDamageType || !this.selectedWindshieldOptions.selectedWindshieldDamageType.includes("Replace") || !this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false;
return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield =>
{
@ -398,6 +409,9 @@ export default {
})
);
},
shouldHideBackButton(){
return this.$store.getters.payment.insuranceCoverage.isVerified;
}
},
components: {

View file

@ -17,11 +17,16 @@ import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import { experimentUniverses } from "@/constants/experiments";
import { getDeviceIdValue, getSessionIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import baseMixin from "@/mixins/base-mixin";
import store from "@/store";
export default {
@ -36,8 +41,16 @@ export default {
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const yearQuestionInitialDataPromise =
yearQuestion.methods.loadInitialData();
const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
// Log experiment exposure
const logExperimentExposurePromise = baseMixin.methods.dispatchNonBlockingStoreAction(storeActions.LOG_EXPERIMENT_EXPOSURE,
{
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: to.query.fmgPage,
universeName: experimentUniverses.CONCEPT_FUNNEL
}, false);
// Settle promises and get results
const promiseResultMap = [
@ -49,6 +62,10 @@ export default {
resultKey: "yearQuestionInitialData",
promise: yearQuestionInitialDataPromise,
},
{
resultKey: "logExperimentExposure",
promise: logExperimentExposurePromise,
},
];
let resultMap = await settleAllPromises(promiseResultMap);
@ -64,8 +81,8 @@ export default {
watch: {
selectedYear(year) {
this.$store.commit(this.storeMutations.UPDATE_YEAR, year);
const parsedYear = parseInt(year);
this.$store.commit(this.storeMutations.UPDATE_YEAR, parsedYear);
this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_YEAR,
this.$route

View file

@ -1,41 +1,272 @@
<template>
<div class="container-fluid shadow rounded-3 px-5 position-relative make-tall">
<funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" />
<funnelSubHeader ref="funnelSubHeader" />
<vinInformation
class="mb-5"
/>
<funnel-footer
ref="funnelFooter"
:isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall px-5">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="VinNumber" v-model="vin" inputId="vin" disableAutoFill validationRules="vin-required|vin-format" :isDisabled=isVinFieldReadOnly />
</div>
</div>
<div class="row my-2">
<div class="col">
<vinInformation />
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="ServiceZIP" v-model="zip" inputId="zip" mask="#####" disableAutoFill validationRules="zip-required" />
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="EmailAddress" v-model="email" inputId="email" disableAutoFill validationRules="email-address-required|email-address-format" />
</div>
</div>
<alert
class="my-3"
v-model="customAlertData"
v-if="matchedDifferentVehicle"
alertClass="alert-danger"
cmsWidgetName="MatchedDifferentVehicle"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="noMatchAlert"
alertClass="alert-warning"
cmsWidgetName="NoMatchAlertWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="noServiceZip"
alertClass="alert-warning"
cmsWidgetName="NoServiceZipWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinFound"
alertClass="alert-warning"
cmsWidgetName="VinFoundWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinFoundReadOnly"
alertClass="alert-warning"
cmsWidgetName="VinFoundReadOnlyWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="foundWindshieldAlert"
alertClass="alert-warning"
cmsWidgetName="FoundWindshieldAlert"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinNotFound"
alertClass="alert-warning"
cmsWidgetName="VinNotFound"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="perfectMatchNewVinAlert"
alertClass="alert-warning"
cmsWidgetName="PerfectMatchNewVinAlert"
/>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages";
import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
// DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.ZIP_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT));
defineRule("vin-required", required(errorMessages.VIN_REQUIRED));
defineRule("vin-format", regex(/^[A-HJ-NPR-Z0-9]{17}$/, errorMessages.VIN_FORMAT));
export default {
name: "vin-lookup",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
props: {
validationRules: String,
},
data() {
return {
matchedDifferentVehicle: false,
noMatchAlert: false,
noServiceZip: false,
vinFound: false,
vinFoundReadOnly: false,
foundWindshieldAlert: false,
vinNotFound: false,
perfectMatchNewVinAlert: false,
vin: '',
zip: '',
email: '',
customAlertData: {},
};
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
resetDependentState() {
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, null);
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, null);
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, null);
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, null);
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
const zipValidation = await this.validateZip(this.zip);
if (!zipValidation.data.isServiceable) {
this.customAlertData.zip = this.zip;
this.$refs.funnelFooter.removeLoader();
this.noServiceZip = true;
return;
}
const vinLookup = await this.lookupVin(this.vin).catch(() => {
this.$refs.funnelFooter.removeLoader();
this.noMatchAlert = true;
return;
});
if (vinLookup.data.carId !== store.getters.vehicle.carId) {
this.customAlertData.vehicleInfo = vinLookup.data.vehicle;
this.$refs.funnelFooter.removeLoader();
this.foundWindshieldAlert = true;
return;
}
const carInfo = this.vinDoesNotMatchCarId ? vinLookup.data : store.getters.vehicle;
this.updateStore(carInfo)
const partsData = await baseMixin.methods.dispatchNonBlockingStoreAction(
this.storeActions.GET_PARTS_OR_QUESTIONS,
{
carId: store.getters.vehicle.carId,
glassArray: store.getters.damage.glassToReplace,
zipCode: this.zip,
vin: vinLookup.vin
},
false
);
this.navigateForward(partsData);
},
navigateForward(partsData){
if(partsData.data.partsOrQuestions[0].partQuestions && partsData.data.partsOrQuestions[0].partQuestions.length > 0){
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_PARTS_QUESTION, this.$route, {}, {}, partsData.data);
return;
} else if((!partsData.data.partsOrQuestions[0].partQuestions || partsData.data.partsOrQuestions[0].partQuestions.length < 1) && partsData.data.partsOrQuestions[0].parts.length > 1) {
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_PARTS, this.$route, {}, {}, partsData.data);
return;
} else {
this.$router.navigate(this.navigationScenarios.CONTINUING_WITH_SINGLE_PART, this.$route);
}
},
validateZip(zip) {
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.VALIDATE_ZIP,
{ zip }
);
},
lookupVin(vin) {
return baseMixin.methods.dispatchNonBlockingStoreAction(
storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin }
);
},
updateStore(carInfo) {
// if(vehicleDamage){
// store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
// }
store.commit(storeMutations.UPDATE_VEHICLE_VIN, this.vin);
store.commit(storeMutations.UPDATE_YEAR, carInfo.year);
store.commit(storeMutations.UPDATE_MAKE, carInfo.make);
store.commit(storeMutations.UPDATE_MODEL, carInfo.model);
store.commit(storeMutations.UPDATE_STYLE, carInfo.style);
store.commit(storeMutations.UPDATE_CAR_ID, carInfo.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, carInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, carInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, carInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, carInfo.imageColor);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP, this.zip);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
},
},
computed: {
isVinFieldReadOnly(){
return this.$store.getters.payment.insuranceCoverage.isVerified;
}
},
components: {
Form,
funnelHeader,
vehicleBanner,
funnelSubHeader,
vinInformation,
textboxQuestion,
alert,
funnelFooter,
vinInformation,
},
};
</script>
<style lang="scss">
</style>

View file

@ -3,6 +3,7 @@ import { storeActions } from "@/constants/store-actions.js";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { queryStrings } from "@/constants/query-strings";
export default {
data() {
@ -11,10 +12,10 @@ export default {
};
},
methods: {
setCmsContent(cmsContent){
setCmsContent(cmsContent) {
this.$root.cmsContentByWidget = cmsContent;
},
getCmsContent(widgetName, fieldName){
getCmsContent(widgetName, fieldName) {
return this.$root.cmsContentByWidget?.[widgetName]?.[fieldName] ? this.$root.cmsContentByWidget[widgetName][fieldName] : '';
},
dispatchNonBlockingStoreAction(type, payload, encodePayload = true) {
@ -25,10 +26,10 @@ export default {
return store.dispatch(type, payload);
},
savePageDataToStore(page, data){
savePageDataToStore(page, data) {
store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data });
},
onSubmit() {}, // DO NOT REMOVE; needed to prevent default form submit behavior
onSubmit() { }, // DO NOT REMOVE; needed to prevent default form submit behavior
onInvalidSubmit({ values, errors, results }) {
// identify the first error field and put focus on it
// get error names array
@ -40,6 +41,28 @@ export default {
el && el.focus();
}
},
pushEventToGA(category, action, label, value, pageName) {
const eventToBePushed = {
'event': 'ga_event',
'category': category,
'action': action,
'label': label,
'value': value,
'path': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}`
}
pushToDataLayerIfDefined(eventToBePushed);
},
pushPageViewToGA(pageName) {
const pageViewEvent = {
'event': 'logPageview',
'pagePath': `/fmg/?${queryStrings.FMG_PAGE}=${pageName}`,
'pageTitle': pageName
};
pushToDataLayerIfDefined(pageViewEvent);
}
},
computed: {
storeActions() {
@ -65,3 +88,9 @@ function encodeUriData(payload) {
});
}
}
function pushToDataLayerIfDefined(data) {
if (window.dataLayer !== undefined) {
window.dataLayer.push(data);
}
}

View file

@ -4,12 +4,13 @@ import { storeActions } from "@/constants/store-actions";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { routingTable } from "@/router/router-constants/routing-table.js";
import { globalEvents, globalEventTypes } from "@/constants/events";
import { queryStrings } from "@/constants/query-strings";
// Heritage integration
import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper";
import { updateOrCreateFunnelCookie,getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { updateOrCreateFunnelCookie, getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper";
import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel} from "@/helpers/heritage-integration/navigation-helper";
import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin";
import eventBus from "@/helpers/event-bus/event-bus";
@ -35,84 +36,79 @@ const routes = [
name: "root",
async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string.
if (to.query.fmgPage === undefined) {
await GoToFunnelStartOn404(next);
} else {
try {
try {
// If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
await GoToFunnelStartOn404(next);
}
// On entering the funnel "fresh", read cookie information, decide what to do next.
if (from.redirectedFrom === undefined) {
const loadOrderResponse = await loadOrderIfPresent();
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse);
// If getPageToRouteExistingOrderTo determines that the return user needs to
// go back to heritage funnel, send them there and stop our current navigation.
if (pageToRedirectTo === 'heritage') {
await navigateToHeritageFunnel();
return next(false);
}
// Assign our fmgPage so it will load normally like the other pages.
to.query.fmgPage = pageToRedirectTo;
}
// Process funnel cookie.
updateOrCreateFunnelCookie();
// If we already have our route, go to it.
if (router.hasRoute(to.query.fmgPage)) {
// Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function.
let component = router.getRoutes().filter((x) => x.name === to.query.fmgPage)[0].components;
// If the component hasn't been loaded fully, load it before we check prerequisites.
if (component.default.methods === undefined) {
component = await component.default();
}
if (!arePagePrerequisitesValid(component)) {
await GoToFunnelStartOn404(next);
}
return next({ name: to.query.fmgPage, query: to.query, params: to.params });
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
const routeData = await GetRouteInfoFromPageName(to.query.fmgPage);
// Add our dynamic route.
router.addRoute({
path: routeData[0].path, // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component,
});
// Call the next components arePagePrerequisitesValid method before load.
// If it returns false, use the 404 logic.
const nextComponent = await router.getRoutes().filter((x) => x.name === routeData[0].name)[0].components.default();
if (!arePagePrerequisitesValid(nextComponent)) {
await GoToFunnelStartOn404(next);
}
// Assign current query string parameters, as well as our fmgPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { fmgPage: routeData[0].name }),
params: to.params
});
} catch (error) {
console.log(error);
// If we don't have a route, go to our 404 page.
// If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
await GoToFunnelStartOn404(next);
}
// On entering the funnel "fresh", read cookie information, decide what to do next.
if (from.redirectedFrom === undefined) {
const loadOrderResponse = await loadOrderIfPresent();
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse);
// If getPageToRouteExistingOrderTo determines that the return user needs to
// go back to heritage funnel, send them there and stop our current navigation.
if (pageToRedirectTo === 'heritage') {
await navigateToHeritageFunnel();
return next(false);
}
// Assign our fmgPage so it will load normally like the other pages.
to.query.fmgPage = pageToRedirectTo;
}
// Process funnel cookie.
updateOrCreateFunnelCookie();
// If we already have our route, go to it.
if (router.hasRoute(to.query.fmgPage)) {
// Since our route is already in scope, we can grab the component from it and call the arePagePrerequisitesValid function.
let component = router.getRoutes().filter((x) => x.name === to.query.fmgPage)[0].components;
// If the component hasn't been loaded fully, load it before we check prerequisites.
if (component.default.methods === undefined) {
component = await component.default();
}
if (!arePagePrerequisitesValid(component)) {
await GoToFunnelStartOn404(next);
}
return next({ name: to.query.fmgPage, query: to.query, params: to.params });
}
// Get route info for the given url. Names will have a 1:1 relationship with names in the Cms.
const routeData = await GetRouteInfoFromPageName(to.query.fmgPage);
// Add our dynamic route.
router.addRoute({
path: routeData[0].path, // Always the same path, because we control it with query strings.
name: routeData[0].name,
component: routeData[0].component,
});
// Call the next components arePagePrerequisitesValid method before load.
// If it returns false, use the 404 logic.
const nextComponent = await router.getRoutes().filter((x) => x.name === routeData[0].name)[0].components.default();
if (!arePagePrerequisitesValid(nextComponent)) {
await GoToFunnelStartOn404(next);
}
// Assign current query string parameters, as well as our fmgPage one.
next({
name: routeData[0].name,
query: Object.assign(to.query, { fmgPage: routeData[0].name }),
params: to.params
});
} catch (error) {
console.log(error);
// If we don't have a route, go to our 404 page.
await GoToFunnelStartOn404(next);
}
},
},
@ -125,6 +121,10 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ----------------------------------------------------------
router.afterEach((to, from) => {
baseMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]);
});
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => {
navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData);
}
@ -205,6 +205,13 @@ function navigateToUrl(url, optionalQuery = {}) {
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
}
/////////////////////////////////////////////////////
// TEMP CODE FOR TESTING WITH SPECIFIC EXPERIMENTS //
/////////////////////////////////////////////////////
if (externalUrl.search.indexOf("corid=") != -1)
externalUrl.search = externalUrl.search + '&experiments=CollectEmailOnQuote=CollectEmailOnQuote_V1=YesCollectEmail_TEST1=true,RemoveServiceAreaPage=ServAreaRemoval_V7=ServAreaNoRemove_V7_TEST=true,VINeducationV2=VINeducation_V2=NoShowVINmodalV2_CONTROL=true,ServicePackages=ServicePackages_V1=NoShowPackages_CONTROL=true,PhotoUploadRedesign=PhotoUploadRedesign_V1=CurrentPhotoUpload_CONTROL=true,ScheduleDetailsServiceType=ScheduleBeforeServiceType_V1=ServTypeThenSched_CONTROL=true';
///////////// END TEMP CODE /////////////////////////
window.location.assign(externalUrl);
}

View file

@ -11,6 +11,7 @@ const fmgPageValues = {
LICENSE_PLATE_LOOKUP: "license-plate-lookup",
REVEAL: "reveal",
ESTIMATE: "estimate",
ADDRESS_VEHICLES: "address-vehicles",
};
export { fmgPageValues };

View file

@ -4,7 +4,9 @@ const navigationScenarios = {
SELECTED_MAKE: "SELECTED_MAKE",
SELECTED_STYLE: "SELECTED_STYLE",
CLICKED_BACK: "CLICKED_BACK",
CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN",
CLICKED_FORWARD: "CLICKED_FORWARD",
CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN",
SELECTED_PARTS: "SELECTED_PARTS",
SELECTED_DAMAGE_WITH_SINGLE_PART: "SELECTED_DAMAGE_WITH_SINGLE_PART",
SELECTED_DAMAGE_WITH_MULTIPLE_PARTS: "SELECTED_DAMAGE_WITH_MULTIPLE_PARTS",

View file

@ -1,6 +1,5 @@
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { applicationConfig } from "@/constants/application-config";
const routingTable = [
{
@ -59,6 +58,10 @@ const routingTable = [
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
},
// UNDO THESE CHANGES BEFORE MERGING TO develop
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
},
{
scenario: navigationScenarios.SELECTED_DAMAGE_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP
@ -109,6 +112,10 @@ const routingTable = [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
@ -136,6 +143,10 @@ const routingTable = [
scenario: navigationScenarios.CONTINUING_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
],
},
{

View file

@ -45,10 +45,16 @@ const getDefaultState = () => {
glassParts: null,
otherParts: null
},
payment:{
isInsurance: null,
insuranceCoverage: {
isVerified: null
}
},
referralNumber: null,
referralDate: null,
referralCorrelationId: null,
parentAccountNumber: null,
parentAccountNumber: 0,
},
applicationUser: {
eventBus: [],
@ -93,7 +99,7 @@ export const mutations = {
updateVehicleVin(state, vin) {
state.order.vehicle.vin = vin;
},
updateIsRepair(state, isRepair){
updateIsRepair(state, isRepair) {
state.order.damage.isRepair = isRepair;
},
updateNumberOfChips(state, numberOfChips) {
@ -120,6 +126,27 @@ export const mutations = {
updateParentAcctNumber(state, parentAcctNumber) {
state.order.parentAccountNumber = parentAcctNumber;
},
updateIsInsurance(state, isInsurance) {
state.order.payment.isInsurance = isInsurance;
},
updateInsuranceVerifiedStatus(state, isVerified) {
state.order.payment.insuranceCoverage.isVerified = isVerified;
},
updateRegistrationLicensePlate(state, licensePlate){
state.order.vehicle.registration.licensePlate = licensePlate;
},
updateRegistrationState(state, registrationState){
state.order.vehicle.registration.state = registrationState;
},
updateRegistrationZipCode(state, reistrationZipCode){
state.order.vehicle.registration.zipCode = reistrationZipCode;
},
updateServiceLocationZip(state, serviceLocationZip){
state.order.serviceLocation.zip = serviceLocationZip;
},
updateCustomerEmailAddress(state, customerEmailAddress){
state.order.customer.emailAddress = customerEmailAddress;
},
// EVENT BUS MUTATIONS
@ -176,23 +203,30 @@ export const mutations = {
state.order.referralNumber = orderInformation.referralNumber;
state.order.referralDate = orderInformation.referralDate;
state.order.referralCorrelationId = orderInformation.referralCorrelationId;
state.order.vehicle = {
state.order.vehicle = Object.assign(state.order.vehicle, {
year: orderInformation.vehicle?.year,
make: orderInformation.vehicle?.make,
model: orderInformation.vehicle?.model,
style: orderInformation.vehicle?.style,
vin: orderInformation.vehicle?.vin,
carId: orderInformation.vehicle?.carId,
category: orderInformation.vehicle?.category,
imageUrl: orderInformation.vehicle?.imageUrl,
imageVifNumber: orderInformation.vehicle?.imageVifNumber,
imageColor: orderInformation.vehicle?.imageVifColor
};
imageColor: orderInformation.vehicle?.imageVifColor,
});
state.order.damage.glassToReplace = orderInformation.glassToReplace;
state.order.damage.isRepair = orderInformation.isRepair;
state.order.damage.numberOfChips = orderInformation.numberOfChips;
state.order.lineItems.glassParts = orderInformation.parts;
state.order.parentAccountNumber = orderInformation.parentAccountNumber;
state.order.serviceLocation.zipCode = orderInformation.zipCode; // TODO CSR-416 Make sure this is correct
state.order.serviceLocation.zipCode = orderInformation.zipCode;
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified;
}
}
@ -210,11 +244,10 @@ export const getters = {
eventBus: (state) => state.applicationUser.eventBus,
damage: (state) => state.order.damage,
lineItems: (state) => state.order.lineItems,
pageData: (state) => (page) => {
return state.applicationUser.pageData[page];
},
pageData: (state) => (page) => { return state.applicationUser.pageData[page]; },
applicationUser: (state) => state.applicationUser,
order: (state) => state.order,
payment: (state) => state.order.payment,
}
// Export Actions
@ -248,7 +281,7 @@ export const actions = {
method: endpoints.LookupVinByPlate.method,
endpoint: endpoints.LookupVinByPlate.url,
payload: {
licensePlate: licensePlate,
licensePlate: licensePlate,
licenseState: licenseState
},
});
@ -304,12 +337,12 @@ export const actions = {
},
getDamageOptions(context, { carId }) {
return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method,
methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {},
});
},
validateZip(context, {zip}) {
validateZip(context, { zip }) {
return globalMethods.callHttpClient({
methods: endpoints.ValidateZip.method,
endpoint: `${endpoints.ValidateZip.url}/${zip}`
@ -375,6 +408,19 @@ export const actions = {
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
},
logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) {
return globalMethods.callHttpClient({
method: endpoints.LogExperimentExposureIfAssigned.method,
endpoint: endpoints.LogExperimentExposureIfAssigned.url,
payload: {
userId: userId,
sessionKey: sessionKey,
pageName: pageName,
universeName: universeName
}
});
},
// Parts API Actions
getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) {
return globalMethods.callHttpClient({
@ -414,16 +460,18 @@ export const actions = {
});
},
loadOrder(context, { referralNumber, referralDate, referralCorrelationId }) {
loadOrder(context, { referralNumber, referralDate, referralCorrelationId, accountNumber}) {
return globalMethods.callHttpClient({
method: endpoints.LoadOrder.method,
endpoint: endpoints.LoadOrder.url,
payload: {
referralNumber: referralNumber,
referralDate: referralDate,
referralCorrelationId: referralCorrelationId
referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber
},
}).then((response) => {
context.commit(storeMutations.RESET_STATE);
context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data);
return response;
});

View file

@ -222,6 +222,7 @@ describe("Mutations", () => {
numberOfChips: 0,
parts: [],
parentAccountNumber: "123456789",
insuranceInfo: {}
});
// Assert
@ -232,6 +233,17 @@ describe("Mutations", () => {
expect(storeState.order.vehicle.model).toEqual("ILX");
});
it("updateInsuranceVerifiedStatus, should set isVerified flag", () => {
// Arrange
const storeState = state;
// Act
mutations.updateInsuranceVerifiedStatus(storeState, true);
// Assert
expect(storeState.order.payment.insuranceCoverage.isVerified).toEqual(true);
});
});
describe("Actions", () => {
@ -690,4 +702,15 @@ describe("Getters", () => {
});
it("Payment getter, should return payment data", () => {
// Arrange
const storeState = state;
//Act
mutations.updateInsuranceVerifiedStatus(storeState, true );
//Assert
expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true);
});
});

View file

@ -1,92 +1,126 @@
.has-error {
&.list-button,
&.list-card {
border: 1px solid $red;
color: $red;
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 0 2.5px $red;
}
input[type=checkbox]:focus + label:hover,
input[type=radio]:focus + label {
box-shadow: 0 0 0 1px $red;
}
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px !important;
border: 1px solid $red;
}
}
input[type=checkbox]:checked + label,
input[type=radio]:checked + label {
box-shadow: 0 0 0 2.5px transparent !important;
}
&.list-button-horizontal {
color: $red;
label {
html {
.has-error {
&.list-button,
&.list-card {
border: 1px solid $red;
color: $red;
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 0 2.5px $red;
}
input[type=checkbox]:checked + label {
box-shadow: 0 0 0 1px $red;
}
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px;
}
}
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 1px $red !important;
}
}
&.ui-radio,
&.ui-checkbox {
input[type=checkbox],
input[type=radio],
input[type=radio]+label:before,
input[type=checkbox]+label:before {
border: 1px solid $red;
}
input[type=checkbox]:checked + label:before {
border: 1px solid $blue;
}
}
&.textbox-question,
&.dropdown-question {
p {
&.list-button-horizontal {
color: $red;
}
input,
select {
border: 1px solid $red;
&:focus {
border: 1px solid transparent;
label {
border: 1px solid $red;
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
}
}
input[type=checkbox]:focus + label,
input[type=radio]:focus + label {
box-shadow: 0 0 1px $red;
}
}
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
&.ui-radio,
&.ui-checkbox {
input[type=checkbox],
input[type=radio],
input[type=radio]+label:before,
input[type=checkbox]+label:before {
border: 1px solid $red;
}
input[type=checkbox]:checked + label:before {
border: 1px solid $blue;
}
}
&.textbox-question,
&.dropdown-question {
p {
color: $red;
}
input,
select {
border: 1px solid $red;
&:focus {
border: 1px solid transparent;
}
}
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 16px 12px;
}
}
}
//Restore to default style if alert box is present
.alertError {
.has-error {
&.list-button,
&.list-card {
border: 1px solid $gray-500;
input:not(:focus) {
+ label {
box-shadow: 0 0 0 1px $gray-500;
border-radius: .5rem;
}
}
input:checked:focus {
+ label {
box-shadow: 0 0 0 1px $blue;
border-radius: .5rem;
}
}
input:checked:not(:focus) {
+ label {
box-shadow: 0 0 0 1px $blue;
border-radius: .5rem;
}
}
input:focus {
border: 1px solid $gray-500;
+ label {
box-shadow: 0 0 0 2.5px transparent;
}
}
&:hover {
box-shadow: 0 0 0 4px $blue-300;
+ label {
box-shadow: 0 0 0 2.5px transparent;
border: 1px solid $blue;
}
}
}
}
}
.form-test-error {
color: $red;
font-size: .875rem;
font-weight: 500;
}
.form-test-invalid {
&.btn.btn-primary {
color: $gray;
background: $gray-200;
cursor: pointer;
pointer-events: all;
}
&.btn.btn-primary:hover,
&.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200;
box-shadow: none;
}
}
}
.form-test-error {
color: $red;
font-size: .875rem;
font-weight: 500;
}
.form-test-invalid {
&.btn.btn-primary {
color: $gray;
background: $gray-200;
cursor: pointer;
pointer-events: all;
}
&.btn.btn-primary:hover,
&.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200 !important;
box-shadow: none !important;
}
}

View file

@ -1,4 +1,4 @@
import { shallowMount } from "@vue/test-utils";
import { shallowMount } from "@vue/test-utils";
import alert from "./alert";
describe("alert.vue", () => {
@ -9,6 +9,13 @@ describe("alert.vue", () => {
propsData: {
isDismissible: true
},
computed: {
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin]
});
@ -24,6 +31,13 @@ describe("alert.vue", () => {
propsData: {
alertClass: 'warning'
},
computed: {
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin]
});

View file

@ -5,7 +5,15 @@
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
>
<p class="m-0 fw-bold small alert-heading">{{ alertHeadline }}</p>
<p class="m-0 text-body small">{{ alertCopy }}</p>
<p v-if="splitAlertCopyForLink.length">
<template v-for="copy in splitAlertCopyForLink" :key="copy">
<span v-if="copy.includes('routerLink:')" class="m-0 text-body small">
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link>
</span>
<span v-else class="m-0 text-body small" v-html="copy"></span>
</template>
</p>
<p v-else class="m-0 text-body small" v-html="alertCopy"></p>
<button
type="button"
class="btn-close p-2"
@ -26,6 +34,7 @@
</template>
<script>
export default {
name: "alert",
props: {
@ -49,6 +58,10 @@ export default {
alertCopy(){
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy;
},
splitAlertCopyForLink(){
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.alertCopy.split(/{(.*?)}/g);
}
},
};
</script>
@ -115,5 +128,9 @@ export default {
height: 1rem;
}
}
& p {
font-size: 14px;
margin-bottom: 0px;
}
}
</style>

View file

@ -1,7 +1,7 @@
<template>
<div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
>
@ -11,7 +11,6 @@
:name="groupName"
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
/>
@ -143,11 +142,11 @@ export default {
height: 0;
position: absolute;
&:focus-visible + label {
box-shadow: 0 0 0 2px $blue;
box-shadow: 0 0 0 2.5px $blue;
z-index: 2;
}
&:focus + label {
box-shadow: 0 0 0 2px $blue;
box-shadow: 0 0 0 2.5px $blue;
z-index: 3;
}
&:checked + label {
@ -156,6 +155,9 @@ export default {
outline: none;
z-index: 2;
}
&:checked:focus + label {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + label p:first-child {
font-weight: 500;
}

View file

@ -1,7 +1,7 @@
<template>
<div
class="list-group list-button d-flex flex-column w-100 mb-2"
:class="[errors.length > 0 ? 'has-error' : '', hasError ? 'has-error' : '']"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@mouseup="handleClick(value)"
@keyup.space="handleClick(value)"
>
@ -11,7 +11,6 @@
:name="groupName"
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue"
@change="!selectingInitiatesLoad ? handleCheckChange() : ''"
>
@ -97,7 +96,7 @@ export default {
}
this.handleChange(value);
},
handleCheckChange(newValue, oldValue){
handleCheckChange(value, oldValue){
const isInitialization = typeof(oldValue) === 'function';
if (!isInitialization) {
const emitEvent = {

View file

@ -4,8 +4,7 @@
class="list-card w-100 rounded-3 d-flex align-items-center h-100"
:class="[
isWide ? 'horizontal' : '',
errors.length > 0 ? 'has-error' : '',
hasError ? 'has-error' : '',
(errors.length > 0 || hasError) ? 'has-error' : '',
]"
@mouseup="handleChange(value)"
@keyup.space="handleChange(value)"
@ -16,7 +15,6 @@
:name="groupName"
:value="value"
:aria-required="isRequired"
:data-focus-target="groupName"
v-model="checkValue"
@change="handleCheckChange(value)"
/>
@ -315,8 +313,8 @@ export default {
&:checked + label::after {
content: "";
margin: -0.25rem 0 0 0;
left: 0.875rem;
margin: -0.15rem 0 0 0;
left: 1.175rem;
}
&:checked + label {

View file

@ -3,7 +3,13 @@ process.env.VUE_APP_CONSUMER_API_GATEWAY =
process.env.VUE_APP_HERITAGE_FUNNEL =
"http://localhost:38000/default.aspx";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY =
"AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo"
"AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo";
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+ '&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x';f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-M6XCRH');";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC = "https://www.googletagmanager.com/ns.html?id=GTM-M6XCRH&gtm_auth=amlAYNhxUxuskQo7jmjadg&gtm_preview=env-38&gtm_cookies_win=x";
module.exports = {
outputDir: "dist/fmg",

View file

@ -2,6 +2,10 @@ process.env.VUE_APP_CONSUMER_API_GATEWAY = "__VUE_APP_CONSUMER_API_GATEWAY__";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = "__VUE_APP_GOOGLE_PLACES_API_KEY__";
process.env.VUE_APP_HERITAGE_FUNNEL = "__VUE_APP_HERITAGE_FUNNEL__";
// GA & GTM
process.env.VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY = "__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__";
process.env.VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC = "__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__"
module.exports = {
outputDir: "dist/fmg",
publicPath: "/fmg",