Merge remote-tracking branch 'origin/develop' into feature/CSR-1012

This commit is contained in:
Matt Caimi 2023-02-13 13:28:36 -05:00
commit 9fa85f6295
21 changed files with 283 additions and 150 deletions

View file

@ -5,7 +5,10 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
:class="
isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'
">
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
<div
v-if="questionText && answers && answers.length > 0"
class="question-text d-flex"
:class="{ 'small-question-text': isSmallQuestionText }">
<span class="fw-bold w-100">{{ questionText }}</span>
</div>
@ -124,6 +127,7 @@ export default {
useTextForValue: Boolean,
valueToLogType: String,
additionalButtonStyling: String,
isSmallQuestionText: Boolean,
},
beforeMount() {
if (this.buttonTypeObject) {
@ -137,13 +141,15 @@ export default {
},
computed: {
getFieldSetClasses() {
if (this.isOverflowScrollable) {
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
} else if (this.buttonTypeString == "listCard") {
return "w-100";
} else {
return "";
}
const baseClasses = this.isOverflowScrollable
? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"
: this.buttonTypeString == "listCard"
? "w-100"
: "";
const withSmallQuestionClass = this.isSmallQuestionText
? baseClasses + " small-question-text"
: baseClasses;
return withSmallQuestionClass;
},
getComponentLoopWrapperClasses() {
let classes;
@ -210,7 +216,7 @@ export default {
},
methods: {
formatString(str) {
return str?.replaceAll(" ", "-");
return String(str).replaceAll(" ", "-");
},
setLastValuePushedToGa(lastValuePushedToGa) {
this.lastValuePushedToGa = lastValuePushedToGa;
@ -260,19 +266,18 @@ export default {
text-align: center;
}
}
.vehicle-parts {
.question-text {
.small-question-text {
&.question-text {
span {
font-size: 0.875rem;
text-align: left;
margin: 0 0 0.5rem 0;
margin: 1rem 0 0.5rem 0;
}
}
.question-text {
&.question-text {
margin: 0;
}
fieldset {
& fieldset {
.ui-radio {
margin: 0;
}

View file

@ -28,7 +28,11 @@ export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHerita
// 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)) {
if (
toRoute.query[queryStrings.FMG_PAGE] !== undefined &&
toRoute.query[queryStrings.FMG_PAGE] !== fmgPageValues.QUOTE &&
!isVinRelatedPage(toRoute)
) {
return overrideYmmsDirectionIfNeeded(toRoute);
}
@ -107,6 +111,7 @@ async function getLatestPageForRedirection() {
const capabilityQuestionsComponent = await getLazyLoadedComponent(
fmgPageValues.CAPABILITY_QUESTIONS
);
const quoteComponent = await getLazyLoadedComponent(fmgPageValues.QUOTE);
const skipVin = await skipVinLookup();
@ -121,7 +126,9 @@ async function getLatestPageForRedirection() {
} else if (!estimateComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_DAMAGE;
} else {
if (capabilityQuestionsComponent.methods.arePagePrerequisitesValid()) {
if (quoteComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.QUOTE;
} else if (capabilityQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.CAPABILITY_QUESTIONS;
} else if (moldingQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.MOLDING_QUESTIONS;

View file

@ -44,7 +44,7 @@ export async function loadSessionIfPresent(isConceptInsurance) {
funnelCookie.ReferralCorrelationId,
isConceptInsurance
)
).data;
)?.data;
}
/*

View file

@ -56,7 +56,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);

View file

@ -120,7 +120,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
@ -223,6 +223,25 @@ export default {
} else if (this.$store.getters.order.referralNumber?.length === 6) {
await this.navigateForwardWithSingleCarMatch();
} else if (this.isRepair) {
const supportingItemsPromise = await this.dispatchStoreAction(
storeActions.GET_SUPPORTING_ITEMS
);
const promiseResultMap = [
{
resultKey: "supportingItems",
promise: supportingItemsPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS,
resultMap.supportingItems,
false
);
// call saveSession here - navigateWithSaving saves too late in the flow
await saveSession({});
return this.$router.navigateWithoutSaving(

View file

@ -120,7 +120,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);

View file

@ -0,0 +1,128 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { applicationConfig } from "@/constants/application-config";
import quote from "@/layouts/quote/quote.vue";
import store from "@/store";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateToHeritageFunnel: jest.fn(),
}));
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
store.getters = {
order: {
accountNumber: applicationConfig.CASH_ACCOUNT_NUMBER,
},
payment: {
insuranceCoverage: {},
isInsurance: false,
},
};
afterEach(() => {
// reset store after each test
store.getters = {
order: {
accountNumber: applicationConfig.CASH_ACCOUNT_NUMBER,
},
payment: {
insuranceCoverage: {},
isInsurance: false,
},
};
});
describe("quote.vue", () => {
test("IsInsurance false should navigateWithSaving", async () => {
//Arrange
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateWithSaving: jest.fn(),
},
route: { quote },
},
});
store.getters.payment = {
insuranceCoverage: {},
isInsurance: false,
};
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
//Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
});
test("IsInsurance true should navigateToHeritageFunnel", async () => {
//Arrange
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateWithSaving: jest.fn(),
},
route: { quote },
},
});
store.getters.payment = {
insuranceCoverage: {},
isInsurance: true,
};
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
//Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(navigateToHeritage.navigateToHeritageFunnel).toHaveBeenCalled();
});
});
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions,
});
mountOptions.global.mocks["$store"] = store;
mountOptions["attachTo"] = document.body;
const wrapper = shallowMount(quote, mountOptions);
return { wrapper };
}

View file

@ -183,6 +183,7 @@ export default {
false
);
}
if (this.$store.getters.order.accountNumber != applicationConfig.CASH_ACCOUNT_NUMBER) {
this.supportingItems = this.filterOutFees(this.supportingItems);
}
@ -193,7 +194,15 @@ export default {
);
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
const payment = this.$store.getters.payment;
if (payment.isInsurance) {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_CASH,
this.$route
);
}
},
},
components: {

View file

@ -43,7 +43,7 @@ export default {
},
watch: {
availableLineItems() {
if (this.$store.getters.lineItems.supportingItems) {
if (this.$store.getters.lineItems.vaps) {
this.selectDefaultPackage();
}
},
@ -213,18 +213,20 @@ export default {
selectDefaultPackage() {
const vapsFromStore = this.$store.getters.lineItems.vaps;
let lowestTierForPackage = packageNames.TIER_ONE;
vapsFromStore.every((vapsItem) => {
let lowestTierForThisItem = this.getLowestTierForThisItem(vapsItem);
if (lowestTierForThisItem === packageNames.TIER_THREE) {
lowestTierForPackage = packageNames.TIER_THREE;
return false;
} else if (lowestTierForThisItem === packageNames.TIER_TWO) {
lowestTierForPackage = packageNames.TIER_TWO;
return true;
} else {
return true;
}
});
if (vapsFromStore.length > 0) {
vapsFromStore.every((vapsItem) => {
let lowestTierForThisItem = this.getLowestTierForThisItem(vapsItem);
if (lowestTierForThisItem === packageNames.TIER_THREE) {
lowestTierForPackage = packageNames.TIER_THREE;
return false;
} else if (lowestTierForThisItem === packageNames.TIER_TWO) {
lowestTierForPackage = packageNames.TIER_TWO;
return true;
} else {
return true;
}
});
}
this.selectedPackageName = lowestTierForPackage;
},
getLowestTierForThisItem(vapsItem) {

View file

@ -1,66 +0,0 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<h1>Reveal Confetti Page Placeholder</h1>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
@back-clicked="backButtonAction" />
</div>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
// 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 store from "@/store";
export default {
name: "quote",
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);
});
},
methods: {
backButtonAction() {
// route to move backwards
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
arePagePrerequisitesValid() {
return true;
},
resetDependentState() {
// Set
store.commit(storeMutations.UPDATE_GLASS_PARTS, null);
// Invokes
store.dispatch(storeActions.RESET_PARTS_AND_DEPS);
},
},
data() {},
components: {
funnelHeader,
funnelFooter,
},
};
</script>

View file

@ -1,6 +1,7 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<serviceZipModalQuestion
@ -17,6 +18,7 @@
workspaceRequirementsWidgetName="WorkspaceRequirementsWidget" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
@ -31,11 +33,11 @@ import mobileLocationModalQuestions from "@/layouts/service-location/mobile-loca
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
//Supporting Files
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form } from "vee-validate";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
export default {
name: "service-location",
@ -119,8 +121,13 @@ export default {
arePagePrerequisitesValid() {
return true;
},
backButtonAction() {},
forwardButtonAction() {},
backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
},
},
components: {
serviceZipModalQuestion,
@ -129,6 +136,7 @@ export default {
funnelFooter,
funnelSubHeader,
Form,
loadingModal,
},
};
</script>

View file

@ -25,6 +25,7 @@
textPosition="text-start"
:loaderEnabled="false"
isRequired
isSmallQuestionText
:groupName="`${glassLocation}-${glassName}-${selectedTint}`"
:validationRules="partValidationRules" />
</div>

View file

@ -1,6 +1,6 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="page-container-grouped-styles vehicle-parts">
<div class="page-container-grouped-styles vehicle-parts small-question-text">
<loadingModal ref="loadingModal" />
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner

View file

@ -1,4 +1,3 @@
fmg
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles">
@ -145,7 +144,7 @@ defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIR
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);

View file

@ -64,17 +64,14 @@ export default {
};
},
getTierOnePackagePrice(lineItems) {
let totalPrice = 0;
lineItems.forEach((lineItem) => {
const partType = lineItem.partType.toUpperCase();
if (
partType != partTypeStrings.FRONT_WIPER &&
partType != partTypeStrings.REAR_WIPER &&
partType != partTypeStrings.RAIN_DEFENSE
) {
totalPrice += this.getTotalLineItemPrice(lineItem);
}
let lineItemsToPrice = lineItems.filter((lineItem) => {
return (
lineItem.partType != partTypeStrings.FRONT_WIPER &&
lineItem.partType != partTypeStrings.REAR_WIPER &&
lineItem.partType != partTypeStrings.RAIN_DEFENSE
);
});
let totalPrice = this.getTotalPriceOfAllLineItemsAndChildParts(lineItemsToPrice);
return totalPrice;
},
filterOutFees(lineItems) {
@ -86,6 +83,18 @@ export default {
});
return filteredLineItems;
},
getTotalPriceOfAllLineItemsAndChildParts(lineItems) {
let totalPrice = 0;
lineItems.forEach((lineItem) => {
totalPrice += this.getTotalLineItemPrice(lineItem);
if (lineItem.childParts) {
totalPrice += this.getTotalPriceOfAllLineItemsAndChildParts(
lineItem.childParts
);
}
});
return totalPrice;
},
getTotalLineItemPrice(lineItem) {
return lineItem.kitPrice + lineItem.laborAmount + lineItem.sellingPrice;
},

View file

@ -485,11 +485,10 @@ export default {
this.hasGlassLocationWithMultipleParts(allPartsOrQuestions);
const hasChildPartQuestions = this.hasChildPartQuestions(allPartsOrQuestions);
const hasCapabilityQuestions = this.hasCapabilityQuestions(allPartsOrQuestions);
const skipVinLookup = await store.dispatch(storeActions.IS_VIN_OPTIONAL_VEHICLE);
const vin = self.$store.getters.vehicle.vin;
let backNavigationScenario =
!vin || vin === "" || skipVinLookup
!vin || vin === ""
? navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS
: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS;

View file

@ -11,10 +11,10 @@ const fmgPageValues = {
MOLDING_QUESTIONS: "molding-questions",
CAPABILITY_QUESTIONS: "capability-questions",
LICENSE_PLATE_LOOKUP: "license-plate-lookup",
REVEAL: "reveal",
ESTIMATE: "estimate",
ADDRESS_VEHICLES: "address-vehicles",
QUOTE: "quote",
SERVICE_LOCATION: "service-location",
HERITAGE: "heritage",
};

View file

@ -10,6 +10,7 @@ const navigationScenarios = {
// General
CLICKED_BACK: "CLICKED_BACK",
CLICKED_FORWARD: "CLICKED_FORWARD",
CLICKED_FORWARD_WITH_CASH: "CLICKED_FORWARD_WITH_CASH",
// YMMS
SELECTED_YEAR: "SELECTED_YEAR",

View file

@ -79,15 +79,6 @@ const routingTable = function (store) {
},
],
},
{
fmgPageValue: fmgPageValues.REVEAL,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
],
},
{
fmgPageValue: fmgPageValues.VIN_LOOKUP,
maps: [
@ -305,10 +296,6 @@ const routingTable = function (store) {
{
fmgPageValue: fmgPageValues.VEHICLE_PARTS,
maps: [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS,
@ -420,6 +407,19 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_BACK_WITH_CAPABILITY_QUESTIONS,
destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CASH,
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
},
],
},
{
fmgPageValue: fmgPageValues.SERVICE_LOCATION,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
],
},
];

View file

@ -10,6 +10,7 @@ import { experimentTriggers } from "@/constants/experiments";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import router from "@/router";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
// Export State
const getDefaultState = () => {
@ -1059,23 +1060,32 @@ export const actions = {
referralCorrelationId: referralCorrelationId,
},
})
.then((response) => {
// Flatten location and name properties
response.data.order.damage?.glassToReplace?.map((glass) => {
glass.glassLocation = glass.location;
glass.glassName = glass.name;
delete glass.location;
delete glass.name;
return glass;
});
.then(
(response) => {
// Flatten location and name properties
response.data.order.damage?.glassToReplace?.map((glass) => {
glass.glassLocation = glass.location;
glass.glassName = glass.name;
delete glass.location;
delete glass.name;
return glass;
});
// clear the state if the existing EON does not equal what is returned from loadSession
if (context.state.order.eon && context.state.order.eon != response.data.eon) {
// clear the state if the existing EON does not equal what is returned from loadSession
if (context.state.order.eon && context.state.order.eon != response.data.eon) {
context.commit(storeMutations.RESET_STATE);
}
context.commit(
storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION,
response.data
);
return response;
},
(error) => {
deleteFunnelCookie();
context.commit(storeMutations.RESET_STATE);
}
context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data);
return response;
});
);
},
// Business domain actions
@ -1422,7 +1432,7 @@ export const actions = {
`${availableLineItemsFormattedForRequest}`;
const lineItemServerData = context.getters.order.lineItems.serverData;
if (lineItemServerData) {
queryString += `&ServerData=${lineItemServerData}`;
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
}
const response = await globalMethods.callHttpClient({
method: endpoints.PriceOrderItems.method,

View file

@ -182,7 +182,9 @@ html {
.form-test-error {
color: $red;
font-size: 0.875rem;
font-weight: 500;
.small {
font-weight: 500;
}
}
.form-test-invalid {