Merge branch 'develop' into feature/CSR-1855

This commit is contained in:
CarlNation 2024-02-28 06:48:54 -05:00
commit 93a9f5ff5f
13 changed files with 502 additions and 55 deletions

View file

@ -24,12 +24,13 @@ module.exports = {
"!src/layouts/review/*.vue", // Temp test exclusion while in development
"!src/layouts/payment/*.vue", // Temp test exclusion while in development
"!src/layouts/payment-pia-return/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: {
global: {
statements: 78,
statements: 77,
},
},
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit

View file

@ -46,7 +46,11 @@
:maxlength="maxLength ? maxLength : '999'"
@focus="$emit('focus', $event.target.value)"
@keydown="keyDownHandler" />
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
<button
v-if="includeSearchIcon"
type="submit"
aria-label="Search button"
@click="focusSearchInput" />
<template v-if="includeImageQuestion">
<template v-if="!isDisabled">
<label class="camera-icon-input" v-show="!isImageProcessing">
@ -169,6 +173,11 @@ export default {
};
},
methods: {
focusSearchInput() {
//Focus cursor in input when search icon is clicked
const field = document.querySelector("input");
field.focus();
},
async imageChanged(e) {
let file = e.target.files[0];

View file

@ -120,7 +120,7 @@ export default {
<style lang="scss" scoped>
.menu-modal-container {
position: absolute;
padding: 1.47rem 1rem 1.47rem 1.47rem;
padding: 1.47rem 0 1.47rem 1.47rem;
right: 0;
button {
border: none;
@ -186,7 +186,7 @@ export default {
}
.menu-modal-container {
position: absolute;
padding: 1.47rem 1rem 1.47rem 1.47rem;
padding: 1.47rem 0 1.47rem 1.47rem;
right: 0;
top: -5rem;
button {

View file

@ -0,0 +1,3 @@
describe("insurance", () => {
it.todo("Should render a normal string");
});

View file

@ -0,0 +1,326 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<loadingModal ref="loadingModal" />
<div class="container-fluid page-container-grouped-styles">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-4" />
<textboxQuestion
class="mb-2 mt-5"
cmsWidgetName="InsuranceCoQuestionWidget"
includeSearchIcon
cornerStyle="rounded" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import { errorMessages } from "@/constants/error-messages";
import {
getDamageString,
getIsWindshieldOnly,
isGlassAvailableForCarId,
} from "@/helpers/damage-helper";
import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { routerParams } from "@/router/router-constants/router-params";
import store from "@/store";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
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-hA-Hj-nJ-NpPr-zR-Z0-9]{17}$/, errorMessages.VIN_FORMAT));
export default {
name: "insurance",
mixins: [vinPagesMixin],
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);
});
},
data() {
return {
vin: this.getVinFromStore(),
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
emailAddress: this.getEmailFromStore(),
isCarIdDifferent: false,
customAlertData: {},
previouslyEnteredCarId: "",
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
isSelectedGlassAvailableForVehicle: true,
displayInvalidZipAlert: false,
displayNonServiceableZipAlert: false,
displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false,
displayVinScanFailedAlert: false,
};
},
methods: {
arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null;
},
getEmailFromStore() {
return this.$store.getters.order.customer.emailAddress;
},
getVinFromStore() {
return this.$store.getters.vehicle.vin;
},
getZipFromStore() {
return this.$store.getters.order.serviceLocation.zipCode;
},
attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.SUBMITTED,
this.GaLabels.VIN_LOOKUP,
true
);
});
},
backButtonAction() {
// route to move backwards
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
this.resetAlerts();
// If this is a new VIN Lookup, do both a Vehicle Lookup and a Zip Validation
if (!this.vinPopulatedOnPageLoad) {
const vehicleLookupResponse = this.dispatchStoreActionWithLogging(
storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin: this.vin },
"vin-lookup"
);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "vehicleLookupResponse",
promise: vehicleLookupResponse,
},
{
resultKey: "zipCodeData",
promise: this.getZipCodeData(this.serviceZipCode),
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// If a Service Zip is entered and it is an invalid zip code (ex. 11111) then show an alert
const isZipValid = resultMap.zipCodeData.isValid;
if (this.serviceZipCode && !isZipValid) {
this.displayInvalidZipAlert = true;
return this.$refs.navbar.removeLoader();
}
this.displayInvalidZipAlert = false;
// If either lookup fails, remove the loader and stop processing the page.
if (!resultMap.vehicleLookupResponse || !resultMap.zipCodeData.isServiceable) {
// If the vehicle result is undefined, the vin entered was invalid.
if (!resultMap.vehicleLookupResponse) {
this.displayVinNotFoundAlert = true;
}
// Check if Service Zip entered is serviceable, if not display an alert
if (!resultMap.zipCodeData.isServiceable) {
this.displayNonServiceableZipAlert = true;
}
// Remove loader and stop processing the page.
return this.$refs.navbar.removeLoader();
}
// Check if the CarId is different from the lookup vs what is in state currently.
this.isCarIdDifferent =
resultMap.vehicleLookupResponse.carId !== this.$store.getters.vehicle.carId;
if (
this.isCarIdDifferent &&
resultMap.vehicleLookupResponse.carId !== this.previouslyEnteredCarId
) {
this.previouslyEnteredCarId = resultMap.vehicleLookupResponse.carId;
this.customAlertData.vehicleInfo = resultMap.vehicleLookupResponse;
this.displayMatchedDifferentVehicleAlert = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
resultMap.vehicleLookupResponse.carId,
"vin-lookup"
);
// Update button "Continue with..."
this.$refs.navbar.updateButtonText(
`Continue with ${resultMap.vehicleLookupResponse.year} ${resultMap.vehicleLookupResponse.make} ${resultMap.vehicleLookupResponse.model}`
);
return this.$refs.navbar.removeLoader();
}
// Save vin, vehicle, customer and service information
await this.dispatchStoreAction(
storeActions.SAVE_VIN_LOOKUP,
{
isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle,
vehicleInfo: Object.assign(resultMap.vehicleLookupResponse, {
vin: this.vin,
}),
},
false
);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
state: resultMap.zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: resultMap.zipCodeData.zipCodeCtu,
},
false
);
return await this.navigateForward();
}
// If a VIN has already been found. Validate the Service Zip (in case of changes)
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
// Check if Service Zip entered is serviceable then save the ZIP info
if (zipCodeData.isServiceable) {
//Only save the zipCode, state, and zipCodeCtu if the zip changed or we lack zipCodeCtu
if (
this.$store.getters.order.serviceLocation.zipCode != this.serviceZipCode ||
!this.$store.getters.order.serviceLocation.zipCodeCtu
) {
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
{
state: zipCodeData.state,
zipCode: this.serviceZipCode,
zipCodeCtu: zipCodeData.zipCodeCtu,
},
false
);
}
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
return await this.navigateForward();
}
// If the Service Zip is NOT serviceable then show an alert
if (zipCodeData.isValid) {
this.displayNonServiceableZipAlert = true;
} else {
this.displayInvalidZipAlert = true;
}
return this.$refs.navbar.removeLoader();
},
async navigateForward() {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateWithSaving(
this.navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true }
);
} else {
await this.navigateForwardWithSingleCarMatch();
}
},
displayVinScanAlert() {
this.displayVinScanFailedAlert = true;
},
getVinFromImage(image) {
return new Promise((resolve, reject) => {
this.dispatchStoreActionWithLogging(
storeActions.LOOKUP_VIN_BY_IMAGE,
image,
"vin-lookup"
)
.then((response) => {
if (response.data.length > 0) {
resolve(response.data[0]);
} else {
reject("No VINs detected.");
}
})
.catch(() => {
reject("An error occurred during the lookup.");
});
});
},
resetAlerts() {
this.displayMatchedDifferentVehicleAlert = false;
this.displayNonServiceableZipAlert = false;
this.displayInvalidZipAlert = false;
this.displayVinNotFoundAlert = false;
this.displayVinScanFailedAlert = false;
},
},
mounted() {
this.attachCustomEvents();
},
components: {
funnelHeader,
navbar,
funnelSubHeader,
textboxQuestion,
Form,
loadingModal,
},
};
</script>

View file

@ -512,7 +512,7 @@ describe("payment.vue", () => {
});
});
describe("handleIFrameContentWindwMessage", () => {
describe("handleIFrameContentWindowMessage", () => {
test("Navigates back if afterpay is closed", () => {
// Arrange
const event = {
@ -524,7 +524,7 @@ describe("payment.vue", () => {
wrapper.vm.backButtonAction = jest.fn();
// Act
wrapper.vm.handleIFrameContentWindwMessage(event);
wrapper.vm.handleIFrameContentWindowMessage(event);
// Assert
expect(wrapper.vm.backButtonAction).toBeCalled();
@ -539,7 +539,7 @@ describe("payment.vue", () => {
const wrapper = setupMocks({});
// Act
wrapper.vm.handleIFrameContentWindwMessage(event);
wrapper.vm.handleIFrameContentWindowMessage(event);
// Assert
expect(wrapper.vm.shouldBlockInteraction).toBe(true);

View file

@ -643,17 +643,19 @@ export default {
this.submitHopForm();
},
handleIFrameContentWindwMessage(event) {
if (event.data.indexOf("afterpayClosed") > -1) {
this.backButtonAction();
}
if (event.data.indexOf("creditCardSubmit") > -1) {
this.setUIBlock(true);
handleIFrameContentWindowMessage(event) {
if (typeof event.data === "string") {
if (event.data.indexOf("afterpayClosed") > -1) {
this.backButtonAction();
}
if (event.data.indexOf("creditCardSubmit") > -1) {
this.setUIBlock(true);
}
}
},
setIFrameListener() {
window.addEventListener("message", (event) =>
this.handleIFrameContentWindwMessage(event)
this.handleIFrameContentWindowMessage(event)
);
},
setUIBlock(val) {

View file

@ -140,42 +140,6 @@ describe("quote.vue", () => {
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
});
test("IsInsurance true should navigateToHeritageFunnel", async () => {
//Arrange
store.getters = {
payment: {
insuranceCoverage: {},
isInsurance: true,
},
order: {
lineItems: [],
payment: {
parentAccountNumber: null,
},
},
};
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateWithSaving: jest.fn(),
},
route: { quote },
mixins: [mockMixin],
},
});
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
//Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(navigateToHeritage.navigateToHeritageFunnel).toHaveBeenCalled();
});
test("should pass arePagePrerequisitesValid with a repair order", () => {
//Arrange
store.getters = {

View file

@ -327,11 +327,10 @@ export default {
const payment = this.$store.getters.payment;
if (payment.isInsurance) {
navigateToHeritageFunnel({
shouldSaveSession: true,
pageNameToLog: "quote",
loadingModal: this.$refs.loadingModal,
});
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_INSURANCE,
this.$route
);
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_CASH,

View file

@ -11,6 +11,7 @@ const fmgPageValues = {
ESTIMATE: "estimate",
ADDRESS_VEHICLES: "address-vehicles",
QUOTE: "quote",
INSURANCE: "insurance",
SERVICE_LOCATION: "service-location",
HERITAGE: "heritage",
SCHEDULE: "schedule",

View file

@ -11,6 +11,7 @@ const navigationScenarios = {
CLICKED_BACK: "CLICKED_BACK",
CLICKED_FORWARD: "CLICKED_FORWARD",
CLICKED_FORWARD_WITH_CASH: "CLICKED_FORWARD_WITH_CASH",
CLICKED_FORWARD_WITH_INSURANCE: "CLICKED_FORWARD_WITH_INSURANCE",
// Vin selection
CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN",

View file

@ -389,6 +389,23 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CASH,
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_INSURANCE,
destinationFmgPageValue: fmgPageValues.INSURANCE,
},
],
},
{
fmgPageValue: fmgPageValues.INSURANCE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.SCHEDULE,
},
],
},
{

View file

@ -2929,6 +2929,130 @@ describe("Actions", () => {
});
});
describe("validateOrderPromoAndSaveServerData", () => {
it("should add GUIDs if not there to provided lineItemsToUse.vaps before sending the http call", async () => {
// Arrange
const context = state;
const promoCode = "testPromo";
const lineItemsToUse = { vaps: [{ partNumber: 1 }], promos: [2] };
const addableVaps = [{ partNumber: "addableVap" }];
context["getters"] = {
order: {
serviceLocation: {
appointmentType: "test",
state: "test",
zipCodeCtu: "test",
},
vehicle: {
carId: "test",
year: "test",
},
referralCorrelationId: "test",
eon: "test",
damage: {
isRepair: true,
glassToReplace: null,
},
payment: {
parentAccountNumber: "test",
},
referralSequenceNumber: "test",
lineItems: {
serverData: "test",
},
},
};
globalMethods.callHttpClient = jest.fn().mockResolvedValue({
data: {},
});
crypto.randomUUID = jest.fn(() => "GUID");
// Act
actions.validateOrderPromoAndSaveServerData(context, {
payload: {
promoCode: promoCode,
lineItemsToUse: lineItemsToUse,
addableVaps: addableVaps,
},
pageNameToLog: "test",
});
const firstCallArgs = globalMethods.callHttpClient.mock.calls[0];
// Assert
const vapsItem = firstCallArgs[0].payload.order.lineItemsOnOrder.filter(
(item) => item.partNumber == 1
)[0];
expect(vapsItem.id).toEqual("GUID");
});
it("should not duplicate ids during syncing if there are two identical vaps items", async () => {
// Arrange
const context = state;
const promoCode = "testPromo";
// AddableVaps will sync its ids to vaps already on the order
const lineItemsToUse = {
vaps: [
{ partNumber: "SBB22", id: "GUID1" },
{ partNumber: "SBB22", id: "GUID2" },
],
promos: [2],
};
const addableVaps = [{ partNumber: "SBB22" }, { partNumber: "SBB22" }];
context["getters"] = {
order: {
serviceLocation: {
appointmentType: "test",
state: "test",
zipCodeCtu: "test",
},
vehicle: {
carId: "test",
year: "test",
},
referralCorrelationId: "test",
eon: "test",
damage: {
isRepair: true,
glassToReplace: null,
},
payment: {
parentAccountNumber: "test",
},
referralSequenceNumber: "test",
lineItems: {
serverData: "test",
},
},
};
globalMethods.callHttpClient = jest.fn().mockResolvedValue({
data: {},
});
crypto.randomUUID = jest.fn(() => "GUID");
// Act
actions.validateOrderPromoAndSaveServerData(context, {
payload: {
promoCode: promoCode,
lineItemsToUse: lineItemsToUse,
addableVaps: addableVaps,
},
pageNameToLog: "test",
});
const firstCallArgs = globalMethods.callHttpClient.mock.calls[0];
// Assert
const vapsItems = firstCallArgs[0].payload.addableVaps.filter(
(item) => item.partNumber == "SBB22"
);
expect(vapsItems[1].id).toEqual("GUID1");
expect(vapsItems[0].id).toEqual("GUID2");
});
it("should add GUIDs if not there to provided addableVaps before sending the http call", async () => {
// Arrange
const context = state;