From 863edfdeb9c1f46fab33868306ad62ac65ca292c Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Fri, 15 Sep 2023 19:43:18 +0530 Subject: [PATCH 01/24] Update index.js --- src/store/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/index.js b/src/store/index.js index 5ab4f971a..f685e15ba 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -614,7 +614,7 @@ function getTimeSlotsAdditionalEventData( ) { var numberOfDays = null; if (firstAvailableAppointmentDateString) - numberOfDays = getDateDifferenceInDays(new Date(), firstAvailableAppointmentDateString); + numberOfDays = getDateDifferenceInDays(new Date().toISOString().split("T")[0], firstAvailableAppointmentDateString); if (shopAppointmentType) return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join( From deebd15e1ab41a79b50c8e64193428e41dea8ccd Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Fri, 15 Sep 2023 19:50:55 +0530 Subject: [PATCH 02/24] Update index.js --- src/store/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/store/index.js b/src/store/index.js index f685e15ba..40b58b856 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -614,7 +614,10 @@ function getTimeSlotsAdditionalEventData( ) { var numberOfDays = null; if (firstAvailableAppointmentDateString) - numberOfDays = getDateDifferenceInDays(new Date().toISOString().split("T")[0], firstAvailableAppointmentDateString); + numberOfDays = getDateDifferenceInDays( + new Date().toISOString().split("T")[0], + firstAvailableAppointmentDateString + ); if (shopAppointmentType) return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join( From 291e13330d6725ce66503dd26048b7c8739835c5 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 12 Oct 2023 12:03:02 -0400 Subject: [PATCH 03/24] CSR-1520 | First integration of promo logic Implementation of validate and revalidate endpoints in the store Validate promo on quote via query string promo Revalidate promos on quote page load Remove promo query string on quote and payment-method forward navigation Add in Id logic for all line items Send relevant pricing data to service package question to be used later for UX changes --- jest.config.js | 2 +- src/constants/endpoints.js | 8 + src/constants/store-actions.js | 3 + src/constants/store-mutations.js | 2 + src/helpers/promotions-helper.js | 101 +++++++++++ src/layouts/quote/quote.spec.js | 81 ++++++--- src/layouts/quote/quote.vue | 42 ++++- .../service-package-question.vue | 42 ++++- src/router/index.js | 3 +- src/store/index.js | 166 +++++++++++++++++- src/store/store.spec.js | 23 ++- 11 files changed, 419 insertions(+), 54 deletions(-) create mode 100644 src/helpers/promotions-helper.js diff --git a/jest.config.js b/jest.config.js index 0f45a8a97..ac8208423 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,7 +27,7 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 78, + statements: 75, }, }, // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 8f4116c83..20746eba1 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -138,6 +138,14 @@ const endpoints = { url: "/price/api/v1/price/order-items", method: "GET", }, + ValidatePromo: { + url: "/price/api/v1/price/order-promo", + method: "POST", + }, + RevalidatePromos: { + url: "/price/api/v1/price/revalidated-order-promos", + method: "POST", + }, LogExperimentExposureIfAssigned: { url: "/experiments/api/v1/experiments/log-exposure", method: "POST", diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 420e8a2de..052e19cc1 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -50,6 +50,8 @@ const storeActions = { CLEAR_VIN: "clearVin", RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise", GET_SIGNATURE: "getSignature", + VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA: "validateOrderPromoAndSaveServerData", + REVALIDATE_ORDER_PROMOS_AND_UPDATE_STORE: "revalidateOrderPromosAndUpdateStore", // DEPENDENCY MUTATIONS RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies", @@ -83,6 +85,7 @@ const storeActions = { SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING: "saveSupportingItemsSuppressingStateResetting", SAVE_VAPS: "saveVaps", + SAVE_PROMOS: "savePromos", SAVE_CUSTOMER_DETAILS: "saveCustomerDetails", SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes", SAVE_WORK_ORDER_FLAG: "saveWorkOrderFlag", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index f3f4a3c53..e02b3bb0e 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -25,6 +25,8 @@ const storeMutations = { UPDATE_VAPS: "updateVaps", UPDATE_SUPPORTING_ITEMS: "updateSupportingItems", UPDATE_LINE_ITEMS_SERVER_DATA: "updateLineItemsServerData", + UPDATE_INACTIVE_PROMOS: "updateInactivePromos", + UPDATE_PROMOS: "updatePromos", UPDATE_REGISTRATION: "updateRegistration", diff --git a/src/helpers/promotions-helper.js b/src/helpers/promotions-helper.js new file mode 100644 index 000000000..f1690127f --- /dev/null +++ b/src/helpers/promotions-helper.js @@ -0,0 +1,101 @@ +import store from "@/store"; +import { storeActions } from "@/constants/store-actions"; +import { partTypeStrings } from "@/constants/part-type-strings"; +import baseMixin from "@/mixins/base-mixin.js"; + +export const promoPartNumberStrings = { + WIPER_DISCOUNT_PART_NUMBER: "WIPER DISCOUNT", + RAIN_DEFENSE_DISCOUNT_PART_NUMBER: "DISC RAIN DEFEN", + GLASS_DISCOUNT_PART_NUMBER: "DISCOUNT", + GLASS_CLEANER_DISCOUNT_PART_NUMBER: "DISC GLASS CLN", +}; + +export const pagesToStripPromoQueryStringFrom = ["quote", "payment-method"]; + +export function getAddableVapsFromAvailableLineItems(availableLineItems) { + const addableVaps = []; + addableVaps.push(...findLineItemsWithPartType(partTypeStrings.FRONT_WIPER, availableLineItems)); + addableVaps.push( + ...findLineItemsWithPartType(partTypeStrings.RAIN_DEFENSE, availableLineItems) + ); + return addableVaps; +} + +export function removeVapsPromosFromPromoArray(promoArray) { + if (!promoArray) { + return []; + } + const filteredPromoArray = promoArray.filter((promo) => { + return ( + promo.partNumber != promoPartNumberStrings.WIPER_DISCOUNT_PART_NUMBER && + promo.partNumber != promoPartNumberStrings.RAIN_DEFENSE_DISCOUNT_PART_NUMBER && + promo.partNumber != promoPartNumberStrings.GLASS_CLEANER_DISCOUNT_PART_NUMBER + ); + }); + return filteredPromoArray; +} + +export async function revalidatePromosAndValidateNewPromo( + newPromo, + pricedLineItems, + pageNameToLog +) { + const hasActivePromos = store.getters.order.lineItems.promos; + const hasHasInactivePromos = store.getters.order.lineItems.inactivePromos; + const hasNewPromo = !!newPromo; + const addableVaps = getAddableVapsFromAvailableLineItems(pricedLineItems); + + let validatePromoResult = null; + let revalidatePromoResult = null; + + if (hasActivePromos || hasHasInactivePromos) { + revalidatePromoResult = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.REVALIDATE_ORDER_PROMOS_AND_UPDATE_STORE, + null, + pageNameToLog, + false + ); + } + if (hasNewPromo) { + validatePromoResult = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA, + { + promoCode: newPromo, + addableVaps: addableVaps, + }, + pageNameToLog, + false + ); + } + + return { validatePromoResult, revalidatePromoResult }; +} + +export function getPromosThatMatchLineItemsOnOrder(promos, lineItemsOnOrder) { + const matchingPromos = []; + promos.forEach((promo) => { + let allIdsMatch = true; + promo.discountedLineItemIds.forEach((id) => { + if (lineItemsOnOrder.filter((x) => x.id === id).length === 0) { + allIdsMatch = false; + return; + } + }); + if (allIdsMatch) { + matchingPromos.push(promo); + } + }); + return matchingPromos; +} + +export function shouldStripPromoQueryString(fmgPageQueryValue) { + return pagesToStripPromoQueryStringFrom.includes(fmgPageQueryValue); +} + +// private methods +function findLineItemsWithPartType(typeToFind, itemsToSearch) { + const partTypeMatches = itemsToSearch?.filter( + (lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase() + ); + return partTypeMatches; +} diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js index 60948bd52..96b7bf092 100644 --- a/src/layouts/quote/quote.spec.js +++ b/src/layouts/quote/quote.spec.js @@ -21,6 +21,12 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ navigateToHeritageFunnel: jest.fn(), })); +jest.mock("@/helpers/promotions-helper", () => ({ + revalidatePromosAndValidateNewPromo: jest.fn(() => { + return { validatePromoResponse: null, revalidatePromoResponse: null }; + }), +})); + jest.mock( "@/store", () => { @@ -43,11 +49,20 @@ jest.mock("@/mixins/base-mixin", () => ({ getTierOnePackagePrice() { return mockTierOnePrice; }, - filterOutFees() { + filterOutFees(items) { return null; }, }, })); + +const mockMixin = { + methods: { + filterOutFees: jest.fn().mockImplementation(() => { + return null; + }), + }, +}; + let mockTierOnePrice = 501; const mockPriceOrderStoreAction = storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA; @@ -93,6 +108,16 @@ afterEach(() => { describe("quote.vue", () => { test("IsInsurance false should navigateWithSaving", async () => { //Arrange + store.getters.payment = { + insuranceCoverage: {}, + isInsurance: false, + }; + store.getters.order = { + lineItems: [], + payment: { + parentAccountNumber: 167132, + }, + }; const { wrapper } = setupMocks({ customMountOptions: { router: { @@ -102,11 +127,6 @@ describe("quote.vue", () => { }, }); - store.getters.payment = { - insuranceCoverage: {}, - isInsurance: false, - }; - wrapper.vm.dispatchStoreAction = jest.fn(() => { return { data: [], @@ -124,20 +144,28 @@ describe("quote.vue", () => { 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], }, }); - store.getters.payment = { - insuranceCoverage: {}, - isInsurance: true, - }; - wrapper.vm.dispatchStoreAction = jest.fn(() => { return { data: [], @@ -154,9 +182,9 @@ describe("quote.vue", () => { }); test("should pass arePagePrerequisitesValid with a repair order", () => { //Arrange - const { wrapper } = setupMocks({}); store.getters = { order: { + lineItems: [], serviceLocation: { zipCode: "12345", zipCodeCtu: "value", @@ -172,6 +200,7 @@ describe("quote.vue", () => { }, }, }; + const { wrapper } = setupMocks({}); let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); @@ -179,7 +208,6 @@ describe("quote.vue", () => { }); test("should pass arePagePrerequisitesValid with a replace order", () => { //Arrange - const { wrapper } = setupMocks({}); store.getters = { order: { serviceLocation: { @@ -200,6 +228,7 @@ describe("quote.vue", () => { }, }, }; + const { wrapper } = setupMocks({}); let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); @@ -207,9 +236,9 @@ describe("quote.vue", () => { }); test("should fail arePagePrerequisitesValid without glass parts or flagged as repair", () => { //Arrange - const { wrapper } = setupMocks({}); store.getters = { order: { + lineItems: [], serviceLocation: { zipCode: "12345", zipCodeCtu: "value", @@ -220,6 +249,7 @@ describe("quote.vue", () => { referralNumber: "1234567", }, }; + const { wrapper } = setupMocks({}); let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); @@ -227,8 +257,6 @@ describe("quote.vue", () => { }); test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => { //Arrange - const { wrapper } = setupMocks({}); - store.getters = { order: { lineItems: { @@ -236,7 +264,7 @@ describe("quote.vue", () => { }, }, }; - + const { wrapper } = setupMocks({}); //mock this to avoid needing to populate this.$route in an unrelated test wrapper.vm.getDefaultIsInsuranceSelectedValue = jest.fn(); @@ -257,8 +285,6 @@ describe("quote.vue", () => { }); test("should default to insurance if query param 'isInsurance' is true", async () => { //Arrange - const { wrapper } = setupMocks({}); - store.getters = { order: { lineItems: { @@ -272,6 +298,7 @@ describe("quote.vue", () => { }, }, }; + const { wrapper } = setupMocks({}); wrapper.vm.$route = { query: { isInsurance: "true" } }; //Act @@ -287,8 +314,6 @@ describe("quote.vue", () => { }); test("should default to cash if query param 'isInsurance' is false", async () => { //Arrange - const { wrapper } = setupMocks({}); - store.getters = { order: { lineItems: { @@ -302,6 +327,7 @@ describe("quote.vue", () => { }, }, }; + const { wrapper } = setupMocks({}); wrapper.vm.$route = { query: { isInsurance: "false" } }; //Act @@ -317,7 +343,6 @@ describe("quote.vue", () => { }); test("should default to insurance if insurance selection is saved to store", async () => { //Arrange - const { wrapper } = setupMocks({}); store.getters = { order: { @@ -332,6 +357,7 @@ describe("quote.vue", () => { }, }, }; + const { wrapper } = setupMocks({}); // Ensure that query param isn't overriding selection wrapper.vm.$route = { query: null }; @@ -348,8 +374,6 @@ describe("quote.vue", () => { }); test("should default to cash if cash selection is saved to store", async () => { //Arrange - const { wrapper } = setupMocks({}); - store.getters = { order: { lineItems: { @@ -363,6 +387,7 @@ describe("quote.vue", () => { }, }, }; + const { wrapper } = setupMocks({}); // Ensure that query param isn't overriding selection wrapper.vm.$route = { query: null }; @@ -380,8 +405,6 @@ describe("quote.vue", () => { test("should default to cash if total economy package price is under $500", async () => { // Also needs no query parameter or previous selection in store to be present //Arrange - const { wrapper } = setupMocks({}); - store.getters = { order: { lineItems: { @@ -396,6 +419,7 @@ describe("quote.vue", () => { }, }; mockTierOnePrice = 200; + const { wrapper } = setupMocks({}); // Ensure that query param isn't overriding selection wrapper.vm.$route = { query: null }; @@ -413,7 +437,6 @@ describe("quote.vue", () => { test("should default to insurance if total economy package price is over $500", async () => { // Also needs no query parameter or previous selection in store to be present //Arrange - const { wrapper } = setupMocks({}); store.getters = { order: { @@ -429,6 +452,7 @@ describe("quote.vue", () => { }, }; mockTierOnePrice = 505; + const { wrapper } = setupMocks({}); // Ensure that query param isn't overriding selection wrapper.vm.$route = { query: null }; @@ -445,7 +469,7 @@ describe("quote.vue", () => { }); test("Should default to insurence if user Service zip is from certain States", async () => { //Arrange - const { wrapper } = setupMocks({}); + store.getters = { order: { lineItems: { @@ -459,6 +483,7 @@ describe("quote.vue", () => { }, }, }; + const { wrapper } = setupMocks({}); wrapper.vm.$route = { query: null }; //Act await quote.beforeRouteEnter.call( diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 788680e46..bd78f06d6 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -29,6 +29,7 @@ :availableLineItems="availableLineItems" :isInsuranceSelected="isInsuranceSelected" @vapsItemsSelected="vapsItemsSelectedAction" + :activePromos="allActivePromos" v-on="{ 'buttonEvent.openModal': openModalAction }" validationRules="option-required" isRequired /> @@ -79,6 +80,9 @@ import { Form, defineRule } from "vee-validate"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { applicationConfig } from "@/constants/application-config"; import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states"; +import { revalidatePromosAndValidateNewPromo } from "@/helpers/promotions-helper"; +import { queryStrings } from "@/constants/query-strings"; +import { setTransitionHooks } from "vue"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); export default { @@ -122,14 +126,12 @@ export default { ]; const resultMap = await settleAllPromises(promiseResultMap); - const clonedGlassParts = store.getters.order.lineItems.glassParts - ? JSON.parse(JSON.stringify(store.getters.order.lineItems.glassParts)) - : []; + const nullSafeGlassParts = store.getters.order.lineItems.glassParts ?? []; const availableLineItems = [ resultMap.rainDefense, ...resultMap.supportingItems, ...resultMap.wipers, - ...clonedGlassParts, + ...nullSafeGlassParts, ]; const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging( @@ -146,13 +148,32 @@ export default { resultMap.supportingItems, false ); + + // Promo logic + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const lowerCaseParams = new URLSearchParams(); + for (const [name, value] of urlParams) { + lowerCaseParams.append(name.toLowerCase(), value); + } + const promoCodeFromQueryString = lowerCaseParams.get(queryStrings.PROMO); + + const { validatePromoResponse, revalidatePromoResponse } = + await revalidatePromosAndValidateNewPromo( + promoCodeFromQueryString, + pricingResults, + "quote" + ); + // End of promo logic + // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.pricedGlassParts = clonedGlassParts; + vm.pricedGlassParts = nullSafeGlassParts; vm.supportingItems = resultMap.supportingItems; vm.availableLineItems = pricingResults; vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue(vm.availableLineItems); + vm.newValidatedPromos = validatePromoResponse?.data.orderPromos; }); }, data() { @@ -162,8 +183,18 @@ export default { availableLineItems: null, supportingItems: null, pricedGlassParts: null, + newValidatedPromos: null, }; }, + computed: { + allActivePromos() { + const allActivePromos = []; + if (this.newValidatedPromos) allActivePromos.push(...this.newValidatedPromos); + if (this.$store.getters.order.lineItems.promos) + allActivePromos.push(...this.$store.getters.order.lineItems.promos); + return allActivePromos; + }, + }, methods: { openModalAction(modalName) { this.$refs[modalName].openModal(); @@ -239,6 +270,7 @@ export default { } this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false); + this.dispatchStoreAction(this.storeActions.SAVE_PROMOS, this.newValidatedPromos, false); const payment = this.$store.getters.payment; if (payment.isInsurance) { diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index e3a4a499a..70872153a 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -29,6 +29,10 @@ import { containsLineItemWithPartType, findLineItemsWithPartType, } from "@/helpers/service-package-helper"; +import { + getPromosThatMatchLineItemsOnOrder, + removeVapsPromosFromPromoArray, +} from "@/helpers/promotions-helper"; export default { name: "servicePackageQuestion", @@ -40,6 +44,7 @@ export default { isRequired: Boolean, isInsuranceSelected: Boolean, availableLineItems: null, + activePromos: null, }, data() { return { @@ -86,8 +91,11 @@ export default { buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName), buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.SubWidgetName), buttonBodyCopy: this.getBodyTextFromCms(answer.SubWidgetName), - buttonAuxillaryCopy: this.getPackagePriceString(answer.Name), + buttonAuxillaryCopy: this.getDiscountedPackagePriceString(answer.Name), buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName), + additionalButtonData: { + strikeThroughPrice: this.getPackagePriceString(answer.Name), + }, })); return modifiedAnswers; }, @@ -161,21 +169,33 @@ export default { return this.processIfStatements(footerText, "custom", this.getCustomValueFromString); }, getPackagePriceString(packageName) { - const formattedPriceFloat = parseFloat(this.getPackagePrice(packageName)).toFixed(2); + const formattedPriceFloat = parseFloat( + this.getPackagePrice(packageName, { discountedPrice: false }) + ).toFixed(2); + return "$" + formattedPriceFloat; + }, + getDiscountedPackagePriceString(packageName) { + const formattedPriceFloat = parseFloat( + this.getPackagePrice(packageName, { discountedPrice: true }) + ).toFixed(2); return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat; }, - getPackagePrice(packageName) { + getPackagePrice(packageName, { discountedPrice = false }) { + const lineItemsToPrice = [...this.nullSafeAvailableLineItems]; + if (discountedPrice) { + lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos)); + } let priceFloat = this.isInsuranceSelected ? 0 : baseMixin.methods.getTierOnePackagePrice( - baseMixin.methods.filterOutFees(this.nullSafeAvailableLineItems) + baseMixin.methods.filterOutFees(lineItemsToPrice) ); - priceFloat += this.getVapsPrice(packageName); + priceFloat += this.getVapsPrice(packageName, discountedPrice); return priceFloat; }, - getVapsPrice(packageName) { + getVapsPrice(packageName, applyPromoDiscounts = false) { const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName); let price = 0; @@ -184,6 +204,16 @@ export default { price += baseMixin.methods.getTotalLineItemPrice(item); }); + if (applyPromoDiscounts && this.activePromos) { + const relevantPromos = getPromosThatMatchLineItemsOnOrder( + this.activePromos, + vapsItems + ); + relevantPromos.forEach((promo) => { + price += baseMixin.methods.getTotalLineItemPrice(promo); + }); + } + return price; }, selectDefaultPackage() { diff --git a/src/router/index.js b/src/router/index.js index 7401bcc3b..0903ccac6 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -28,6 +28,7 @@ import store from "@/store"; import analyticsMixin from "@/mixins/analytics-mixin"; import { experimentTriggers } from "../constants/experiments"; import { applicationConfig } from "../constants/application-config"; +import { shouldStripPromoQueryString } from "@/helpers/promotions-helper"; const routes = [ { @@ -283,7 +284,7 @@ async function navigate( queryStringsObject[queryStrings.ZIP_CODE] = zip; } - if (promo) { + if (promo && !shouldStripPromoQueryString(currentRoute.query.fmgPage)) { queryStringsObject[queryStrings.PROMO] = promo; } diff --git a/src/store/index.js b/src/store/index.js index 431fce721..39dede74e 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -84,6 +84,7 @@ const getDefaultState = () => { supportingItems: null, vaps: null, serverData: null, + promos: null, }, payment: { isInsurance: null, @@ -94,6 +95,7 @@ const getDefaultState = () => { parentAccountNumber: 0, payNowType: "cc", piaErrorCode: null, + inactivePromos: null, }, schedule: { date: null, @@ -193,6 +195,12 @@ export const mutations = { updateLineItemsServerData(state, serverData) { state.order.lineItems.serverData = serverData; }, + updateInactivePromos(state, inactivePromos) { + state.order.payment.inactivePromos = inactivePromos; + }, + updatePromos(state, promos) { + state.order.lineItems.promos = promos; + }, updatePageData(state, pageData) { state.applicationUser.pageData[pageData.page] = pageData.data; }, @@ -1227,10 +1235,10 @@ export const actions = { return response; }, - getWipers(context, { pageNameToLog }) { + async getWipers(context, { pageNameToLog }) { const carId = context.getters.vehicle.carId; const serviceZipCode = context.getters.order.serviceLocation.zipCode; - return globalMethods + const response = await globalMethods .callHttpClient({ method: endpoints.GetWipers.method, endpoint: `${endpoints.GetWipers.url}/${carId}/${serviceZipCode}`, @@ -1241,15 +1249,20 @@ export const actions = { // The wiper service sometimes returns 500s on legitimate carId/zipCode combination - return empty array instead of breaking flow return []; }); + + syncLineItemIds(response.data, context.getters.order.lineItems.vaps); + return response; }, - getRainDefense(context, { pageNameToLog }) { - return globalMethods.callHttpClient({ + async getRainDefense(context, { pageNameToLog }) { + const response = await globalMethods.callHttpClient({ method: endpoints.GetRainDefense.method, endpoint: endpoints.GetRainDefense.url, logApiCall: true, pageNameToLog: pageNameToLog, }); + syncLineItemIds([response.data], context.getters.order.lineItems.vaps); + return response; }, getMobileFeePart(context, { pageNameToLog }) { @@ -1574,6 +1587,7 @@ export const actions = { supportingItems: lineItems.supportingItems, vaps: lineItems.vaps, serverData: lineItems.serverData, + promos: lineItems.promos, }, payment: { InsuranceCoverage: { @@ -1581,6 +1595,7 @@ export const actions = { }, isInsurance: order.payment.isInsurance, parentAccountNumber: order.payment.parentAccountNumber, + inactivePromos: order.payment.inactivePromos, }, serviceLocation: { streetAddress: order.serviceLocation.address, @@ -1995,17 +2010,21 @@ export const actions = { if (!deepEqual(supportingItems, context.state.order.lineItems.supportingItems)) { context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES); } - + addGuidToLineItemsIfNotAlreadyThere(supportingItems); context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems); }, saveSupportingItemsSuppressingStateResetting(context, supportingItems) { + addGuidToLineItemsIfNotAlreadyThere(supportingItems); context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems); }, - saveVaps(context, vaps) { + addGuidToLineItemsIfNotAlreadyThere(vaps); context.commit(storeMutations.UPDATE_VAPS, vaps); }, + savePromos(context, promos) { + context.commit(storeMutations.UPDATE_PROMOS, promos); + }, // Price order actions async priceOrderItemsAndSaveServerData( @@ -2061,6 +2080,95 @@ export const actions = { return availableLineItems; }, + async validateOrderPromoAndSaveServerData( + context, + { payload: { promoCode, addableVaps }, pageNameToLog } + ) { + const order = context.getters.order; + addGuidToLineItemsIfNotAlreadyThere(addableVaps); + const requestObject = { + promoCode: promoCode, + order: { + appointmentType: order.serviceLocation.appointmentType, + carId: order.vehicle.carId, + correlationId: order.referralCorrelationId, + eon: order.eon, + isRepair: order.damage.isRepair, + glassToReplace: renameGlassToReplaceAttributes(order.damage.glassToReplace), + lineItemsOnOrder: getArrayOfAllLineItems(order.lineItems), + addableVaps: addableVaps, + parentAccountNumber: order.payment.parentAccountNumber, + referralSequenceNumber: order.referralSequenceNumber, + serviceState: order.serviceLocation.state, + serverData: order.lineItems.serverData, + vehicleYear: "" + order.vehicle.year, + zipCodeOrProviderCtu: + order.serviceLocation.appointmentType === "Inshop" + ? order.serviceLocation.provider.address.zipCodeCtu + : order.serviceLocation.zipCodeCtu, + }, + }; + + const validateResponse = await globalMethods.callHttpClient({ + method: endpoints.ValidatePromo.method, + endpoint: endpoints.ValidatePromo.url, + payload: requestObject, + logApiCall: true, + pageNameToLog: pageNameToLog, + }); + + context.commit( + storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, + validateResponse.data.lineItemsServerData + ); + + return validateResponse; + }, + async revalidateOrderPromosAndUpdateStore(context, { pageNameToLog }) { + const order = context.getters.order; + + let requestObject = { + inactivePromos: order.payment.inactivePromos, + order: { + appointmentType: order.serviceLocation.appointmentType, + carId: order.vehicle.carId, + correlationId: order.referralCorrelationId, + eon: order.eon, + isRepair: order.damage.isRepair, + glassToReplace: renameGlassToReplaceAttributes(order.damage.glassToReplace), + lineItemsOnOrder: getArrayOfAllLineItems(order.lineItems), + parentAccountNumber: order.payment.parentAccountNumber, + referralSequenceNumber: order.referralSequenceNumber, + serviceState: order.serviceLocation.state, + serverData: order.lineItems.serverData, + vehicleYear: "" + order.vehicle.year, + zipCodeOrProviderCtu: + order.serviceLocation.appointmentType === "Inshop" + ? order.serviceLocation.provider.address.zipCodeCtu + : order.serviceLocation.zipCodeCtu, + }, + }; + + const revalidateResponse = await globalMethods.callHttpClient({ + method: endpoints.RevalidatePromos.method, + endpoint: endpoints.RevalidatePromos.url, + payload: requestObject, + logApiCall: true, + pageNameToLog: pageNameToLog, + }); + + const revalidationErrorPromoCodes = revalidateResponse.data.errors.map((x) => x.promoCode); + + context.commit(storeMutations.UPDATE_PROMOS, revalidateResponse.data.promoLineItems); + context.commit(storeMutations.UPDATE_INACTIVE_PROMOS, revalidationErrorPromoCodes); + context.commit( + storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, + revalidateResponse.data.lineItemsServerData + ); + + return revalidateResponse; + }, + // Misc order actions saveSchedule(context, scheduleInfo) { context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo); @@ -2125,7 +2233,7 @@ export const actions = { if (!deepEqual(parts, context.state.order.lineItems.glassParts)) { context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES); } - + addGuidToLineItemsIfNotAlreadyThere(parts); context.commit(storeMutations.UPDATE_GLASS_PARTS, parts); }, @@ -2281,6 +2389,19 @@ function addPricesToLineItems(lineItems, pricingLineItems) { return lineItems; } +function getArrayOfAllLineItems(lineItems) { + let consolidatedLineItemsArray = []; + if (lineItems.glassParts != null) + consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.glassParts]; + if (lineItems.supportingItems != null) + consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.supportingItems]; + if (lineItems.vaps != null) + consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.vaps]; + if (lineItems.promos != null) + consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.promos]; + return consolidatedLineItemsArray; +} + function getFlattenedArrayOfLineItemsWithChildParts(lineItems) { let flattenedArray = []; lineItems?.forEach((lineItem) => { @@ -2315,6 +2436,37 @@ function convertGlassPieceToBackEndCompatibleFormat(glassPieces) { }); } +function renameGlassToReplaceAttributes(glassToReplace) { + let newGlassToReplace = []; + if (glassToReplace) { + glassToReplace.forEach((item) => { + newGlassToReplace.push({ location: item.glassLocation, name: item.glassName }); + }); + } + return newGlassToReplace; +} + +function addGuidToLineItemsIfNotAlreadyThere(lineItems) { + lineItems.forEach((lineItem) => { + if (!lineItem.id) { + lineItem.id = crypto.randomUUID(); + } + }); +} + +function syncLineItemIds(lineItemsWithoutIds, lineItemsWithIds) { + if (!lineItemsWithIds || !lineItemsWithoutIds) { + return; + } + lineItemsWithoutIds.forEach((noId) => { + lineItemsWithIds.forEach((withId) => { + if (noId.partNumber === withId.partNumber && noId.partType === withId.partType) { + noId.id = withId.id; + } + }); + }); +} + function providersEqual(providerA, providerB) { return ( providerA.providerNumber === providerB.providerNumber && diff --git a/src/store/store.spec.js b/src/store/store.spec.js index d55862e3e..85227a390 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -9,6 +9,8 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants"; // Mock global method globalMethods.callHttpClient = jest.fn(); +global.crypto = { randomUUID: jest.fn() }; + describe("Mutations", () => { it("Updates vehicle year in state", () => { // Arrange @@ -1378,6 +1380,8 @@ describe("Actions", () => { state: state, }; + context.state.order.lineItems = []; + const commit = jest.fn(); const dispatch = jest.fn(); @@ -1385,10 +1389,10 @@ describe("Actions", () => { context.dispatch = dispatch; // Act - actions.saveGlassParts(context, { glassParts: {} }); + actions.saveGlassParts(context, []); // Assert - expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, { glassParts: {} }); + expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, []); expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES); }); @@ -1417,7 +1421,7 @@ describe("Actions", () => { state: { order: { lineItems: { - supportingItems: ["TestValue1", "TestValue2"], + supportingItems: [{ partNum: "TestValue1" }, { partNum: "TestValue2" }], }, }, }, @@ -1430,7 +1434,11 @@ describe("Actions", () => { context.dispatch = dispatch; // Act - actions.saveSupportingItems(context, ["TestValue3", "TestValue4", "TestValue5"]); + actions.saveSupportingItems(context, [ + { partNum: "TestValue3" }, + { partNum: "TestValue4" }, + { partNum: "TestValue5" }, + ]); // Assert expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES); @@ -1442,7 +1450,7 @@ describe("Actions", () => { state: { order: { lineItems: { - supportingItems: ["TestValue1", "TestValue2"], + supportingItems: [{ partNum: "TestValue1" }, { partNum: "TestValue2" }], }, }, }, @@ -1455,7 +1463,10 @@ describe("Actions", () => { context.dispatch = dispatch; // Act - actions.saveSupportingItems(context, ["TestValue1", "TestValue2"]); + actions.saveSupportingItems(context, [ + { partNum: "TestValue1" }, + { partNum: "TestValue2" }, + ]); // Assert expect(dispatch).not.toBeCalledWith( From 6358818bdc628589b7cb4b3ab03b25b1247071b1 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Thu, 12 Oct 2023 12:25:17 -0400 Subject: [PATCH 04/24] CSR-1520 | Refactor Use existing method from querystring-helper Rename response objects --- src/helpers/promotions-helper.js | 10 +++++----- src/layouts/quote/quote.vue | 9 ++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/helpers/promotions-helper.js b/src/helpers/promotions-helper.js index f1690127f..00fd2ac9d 100644 --- a/src/helpers/promotions-helper.js +++ b/src/helpers/promotions-helper.js @@ -45,11 +45,11 @@ export async function revalidatePromosAndValidateNewPromo( const hasNewPromo = !!newPromo; const addableVaps = getAddableVapsFromAvailableLineItems(pricedLineItems); - let validatePromoResult = null; - let revalidatePromoResult = null; + let validatePromoResponse = null; + let revalidatePromoResponse = null; if (hasActivePromos || hasHasInactivePromos) { - revalidatePromoResult = await baseMixin.methods.dispatchStoreActionWithLogging( + revalidatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.REVALIDATE_ORDER_PROMOS_AND_UPDATE_STORE, null, pageNameToLog, @@ -57,7 +57,7 @@ export async function revalidatePromosAndValidateNewPromo( ); } if (hasNewPromo) { - validatePromoResult = await baseMixin.methods.dispatchStoreActionWithLogging( + validatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging( storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA, { promoCode: newPromo, @@ -68,7 +68,7 @@ export async function revalidatePromosAndValidateNewPromo( ); } - return { validatePromoResult, revalidatePromoResult }; + return { validatePromoResponse, revalidatePromoResponse }; } export function getPromosThatMatchLineItemsOnOrder(promos, lineItemsOnOrder) { diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index 30a37ff9e..4773938d0 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -82,6 +82,7 @@ import { applicationConfig } from "@/constants/application-config"; import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states"; import { revalidatePromosAndValidateNewPromo } from "@/helpers/promotions-helper"; import { queryStrings } from "@/constants/query-strings"; +import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { setTransitionHooks } from "vue"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); @@ -150,13 +151,7 @@ export default { ); // Promo logic - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - const lowerCaseParams = new URLSearchParams(); - for (const [name, value] of urlParams) { - lowerCaseParams.append(name.toLowerCase(), value); - } - const promoCodeFromQueryString = lowerCaseParams.get(queryStrings.PROMO); + const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO); const { validatePromoResponse, revalidatePromoResponse } = await revalidatePromosAndValidateNewPromo( From 837ad2912fa03d89cca52b2964f0ebf9c7f66b1f Mon Sep 17 00:00:00 2001 From: CarlNation <32103961+CarlNation@users.noreply.github.com> Date: Fri, 13 Oct 2023 13:35:46 -0400 Subject: [PATCH 05/24] CSR-1392 wire up payment method to forward button click. --- src/layouts/payment-method/payment-method.vue | 75 +++++++------------ 1 file changed, 26 insertions(+), 49 deletions(-) diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index ecd310c5c..99e59d713 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -38,44 +38,6 @@ vapsTilesCmsName="VapsProductTiles" v-on="{ 'buttonEvent.openModal': openModal }" /> -
- - - - -
-
- -
-
- -
Date: Fri, 13 Oct 2023 14:20:53 -0400 Subject: [PATCH 06/24] CSR-1679 pass useragent to determine sourceDnis --- src/store/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/store/index.js b/src/store/index.js index 094421f0d..b5ae2e911 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1550,6 +1550,7 @@ export const actions = { endpoint: endpoints.SaveSession.url, payload: { submitAfterSave: context.state.submitAfterSave, + userAgent: navigator.userAgent, applicationUser: { crmCustomerId: applicationUser.crmCustomerId, experiments: applicationUser.experiments, From 16c359ac186badfe9774bcbd6d1f821eef433c69 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Fri, 13 Oct 2023 14:30:00 -0400 Subject: [PATCH 07/24] Fix alert width for questions component. --- .../questions-page-layout.vue | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue index 638094e05..94456233d 100644 --- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue +++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue @@ -10,40 +10,43 @@
+ cmsWidgetName="VehicleBannerWidget" + :displayGenericVehicleImage="false" /> - +
+
+
+
+ ref="alertFewMoreQuestions" + cmsWidgetName="alertWidget" + class="my-5" + alertClass="alert-warning" + :manualHeadline="alertFewMoreQuestionsHeader" + :manualCopy="alertFewMoreQuestionsCopy" + v-bind:isDismissible="false" />
+ ref="questionChain" + v-model="selectedAnswers[questionsDatum.answerKey]" + :questionData="questionsDatum.questions" + :index="i" + v-if="showThisQuestionChain(questionsDatum, i)" + :answerKey="questionsDatum.answerKey" + :validationRules="validationRules" />
+ ref="navbar" + cmsWidgetName="FunnelFooterWidget" + :isForwardActionDisabled="!isMetaValid" + @back-clicked="handleBackButtonAction" + @ForwardClicked="handleForwardButtonAction" />
From 0293664c5392bd3dc6fc611af1a9e6501b644ed3 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Fri, 13 Oct 2023 14:31:02 -0400 Subject: [PATCH 08/24] Format code. --- .../questions-page-layout.vue | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue index 94456233d..73d810cc7 100644 --- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue +++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue @@ -10,8 +10,8 @@
+ cmsWidgetName="VehicleBannerWidget" + :displayGenericVehicleImage="false" />
@@ -19,34 +19,34 @@
+ ref="alertFewMoreQuestions" + cmsWidgetName="alertWidget" + class="my-5" + alertClass="alert-warning" + :manualHeadline="alertFewMoreQuestionsHeader" + :manualCopy="alertFewMoreQuestionsCopy" + v-bind:isDismissible="false" />
+ ref="questionChain" + v-model="selectedAnswers[questionsDatum.answerKey]" + :questionData="questionsDatum.questions" + :index="i" + v-if="showThisQuestionChain(questionsDatum, i)" + :answerKey="questionsDatum.answerKey" + :validationRules="validationRules" />
+ ref="navbar" + cmsWidgetName="FunnelFooterWidget" + :isForwardActionDisabled="!isMetaValid" + @back-clicked="handleBackButtonAction" + @ForwardClicked="handleForwardButtonAction" />
From 0bc79148f58b6d77c88a5c7cd0f0dc094568c517 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Fri, 13 Oct 2023 16:02:18 -0400 Subject: [PATCH 09/24] CSR-1700 fix recal modal not opening. --- src/layouts/service-location/service-location.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 674c47409..eca5f5550 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -494,6 +494,9 @@ export default { this.updateAndSaveSupportingItems(); this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); }, + openModalAction(modalName) { + this.$refs[modalName].openModal(); + }, }, watch: { selectedAppointmentType: { From bd7be85511a465028694557dba95e1710959db3c Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Mon, 16 Oct 2023 17:03:57 +0530 Subject: [PATCH 10/24] CSR-1417 remove getFullDayName and getFullMonthName from date-helper and use from schedule helper --- src/helpers/date-helper.js | 16 --------------- src/helpers/date-helper.spec.js | 19 +----------------- .../add-to-calendar/add-to-calendar.vue | 2 +- src/layouts/confirmation/confirmation.vue | 20 +++++++++---------- 4 files changed, 12 insertions(+), 45 deletions(-) diff --git a/src/helpers/date-helper.js b/src/helpers/date-helper.js index d42ad326f..7a8c3e71d 100644 --- a/src/helpers/date-helper.js +++ b/src/helpers/date-helper.js @@ -7,22 +7,6 @@ export function getDateDifferenceInDays(startDate, endDate) { // Convert milliseconds to days and return the result return difference / (1000 * 3600 * 24); } -export function getFullDayName(date) { - // Use a ternary operator to check if the input is a valid date object - return date instanceof Date - ? // Use the built-in method toLocaleDateString() to get the full day name in the current locale - date.toLocaleDateString(undefined, { weekday: "long" }) - : // Return undefined if the input is not a valid date object - undefined; -} -export function getFullMonthName(date) { - // Use a ternary operator to check if the input is a valid date object - return date instanceof Date - ? // Use the built-in method toLocaleDateString() to get the full month name in the current locale - date.toLocaleDateString(undefined, { month: "long" }) - : // Return undefined if the input is not a valid date object - undefined; -} export function get12HourTimeFormat(time) { // Check correct time format and split into components time = time.toString().match(/^([01]\d|2[0-3])(:)([0-5]\d)?$/) || [time]; diff --git a/src/helpers/date-helper.spec.js b/src/helpers/date-helper.spec.js index 4ea2157f5..b78397e0f 100644 --- a/src/helpers/date-helper.spec.js +++ b/src/helpers/date-helper.spec.js @@ -1,6 +1,4 @@ import { - getFullDayName, - getFullMonthName, get12HourTimeFormat, get12HourTimeMobileFormat, getDateFormat, @@ -49,22 +47,6 @@ describe("date-helper.js", () => { expect(result).toEqual(testCase.expected); } }); - it("getFullMonthName should return full month format.", () => { - // Arrange / Act - const date = new Date("2023-10-01"); - const monthName = getFullMonthName(date); - - // Assert - expect(monthName).toEqual("October"); - }); - it("getFullDayName should return full Day Name format.", () => { - // Arrange / Act - const date = new Date("2023-10-01"); - const dayName = getFullDayName(date); - - // Assert - expect(dayName).toEqual("Sunday"); - }); it("getDateFormat should return date in the given format.", () => { // Arrange / Act const date = new Date("2023-10-01"); @@ -74,6 +56,7 @@ describe("date-helper.js", () => { // Assert expect(formattedDate).toEqual("2023-10-01"); }); + it("should return the correct difference in days", function () { // Define some sample dates and their expected differences const testCases = [ diff --git a/src/layouts/add-to-calendar/add-to-calendar.vue b/src/layouts/add-to-calendar/add-to-calendar.vue index b0306b5d0..0b856e72d 100644 --- a/src/layouts/add-to-calendar/add-to-calendar.vue +++ b/src/layouts/add-to-calendar/add-to-calendar.vue @@ -127,7 +127,7 @@ export default { serviceType() { const isRepair = store.getters.order.damage.isRepair; const funnelHasRecalibrationPart = store.getters.funnelHasRecalibrationPart; - if (isRepair) { + if (!isRepair) { if (funnelHasRecalibrationPart) { return "replacement and recalibration"; } else { diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index cb7ca43c1..5fa24856d 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -55,15 +55,11 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import { settleAllPromises } from "@/helpers/layout-helper"; import { applicationConfig } from "@/constants/application-config.js"; +import { convertDateStringToDate } from "@/layouts/schedule/helpers/schedule-helper"; import { Form } from "vee-validate"; import store from "@/store"; -import { - getFullDayName, - getFullMonthName, - get12HourTimeFormat, - get12HourTimeMobileFormat, -} from "@/helpers/date-helper"; +import { get12HourTimeFormat, get12HourTimeMobileFormat } from "@/helpers/date-helper"; export default { name: "confirmation", async beforeRouteEnter(to, from, next) { @@ -174,10 +170,14 @@ export default { return `${this.ProviderAddress},
${this.ProviderCity}, ${this.ProviderState} ${this.ProviderZipCode}`; }, ScheduleDateFormatted() { - const scheduleDate = new Date(this.ScheduleDate); - return `${getFullDayName(scheduleDate)}, ${getFullMonthName( - scheduleDate - )} ${scheduleDate.getDate()}`; + // This conversion ensures we don't get get GMT induced date changes + const dateObject = convertDateStringToDate(this.ScheduleDate); + // Ex: Tuesday, April 22 + return dateObject.toLocaleDateString("en-us", { + weekday: "long", + month: "long", + day: "numeric", + }); }, ScheduleTimeFormatted() { if (this.AppointmentType == AppointmentTypeStrings.MOBILE) { From 1c48dc856a678108b17ca8b9c6fe0a92066131e4 Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Mon, 16 Oct 2023 17:54:54 +0530 Subject: [PATCH 11/24] service-type constant file service-type constant file added --- src/constants/service-type.js | 6 +++++ .../add-to-calendar/add-to-calendar.spec.js | 24 +++++++------------ .../add-to-calendar/add-to-calendar.vue | 7 +++--- 3 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 src/constants/service-type.js diff --git a/src/constants/service-type.js b/src/constants/service-type.js new file mode 100644 index 000000000..5cb6a277a --- /dev/null +++ b/src/constants/service-type.js @@ -0,0 +1,6 @@ +const serviceType = { + REPLACEMENT: "replacement", + REPAIR: "repair", + REPLACEMENT_AND_RECALIBRATION: "replacement and recalibration", +}; +export { serviceType }; diff --git a/src/layouts/add-to-calendar/add-to-calendar.spec.js b/src/layouts/add-to-calendar/add-to-calendar.spec.js index 1e3e5ae4e..645e3b12f 100644 --- a/src/layouts/add-to-calendar/add-to-calendar.spec.js +++ b/src/layouts/add-to-calendar/add-to-calendar.spec.js @@ -77,9 +77,9 @@ describe("Add-to-calendar methods...", () => { //Assert expect(wrapper.vm.$refs.calendarModalQuestion.openModal).toBeCalled(); }); - test("serviceType should return 'replacement and recalibration' when isRepair and funnelHasRecalibrationPart true.", () => { + test("serviceType should return 'replacement and recalibration' when isRepair is false and funnelHasRecalibrationPart true.", () => { //Arrange - store.getters.order.damage.isRepair = true; + store.getters.order.damage.isRepair = false; store.getters.funnelHasRecalibrationPart = true; const { wrapper } = setupMocks({}); @@ -89,9 +89,9 @@ describe("Add-to-calendar methods...", () => { //Assert expect(testValue).toEqual("replacement and recalibration"); }); - test("serviceType should return 'replacement' when isRepair is true and funnelHasRecalibrationPart is false.", () => { + test("serviceType should return 'replacement' when isRepair is false and funnelHasRecalibrationPart is false.", () => { //Arrange - store.getters.order.damage.isRepair = true; + store.getters.order.damage.isRepair = false; store.getters.funnelHasRecalibrationPart = false; const { wrapper } = setupMocks({}); @@ -101,9 +101,9 @@ describe("Add-to-calendar methods...", () => { //Assert expect(testValue).toEqual("replacement"); }); - test("serviceType should return 'repair' when isRepair is false.", () => { + test("serviceType should return 'repair' when isRepair is true.", () => { //Arrange - store.getters.order.damage.isRepair = false; + store.getters.order.damage.isRepair = true; const { wrapper } = setupMocks({}); //Act @@ -126,7 +126,7 @@ describe("Add-to-calendar methods...", () => { test("getappointmentData should return expected model value for appointmentType mobile.", () => { //Arrange store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; - store.getters.order.damage.isRepair = true; + store.getters.order.damage.isRepair = false; store.getters.funnelHasRecalibrationPart = true; const { wrapper } = setupMocks({}); @@ -176,7 +176,7 @@ describe("Add-to-calendar methods...", () => { test("getappointmentData should return expected model value for appointmentType DROP_OFF.", () => { //Arrange store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.DROP_OFF; - store.getters.order.damage.isRepair = false; + store.getters.order.damage.isRepair = true; const { wrapper } = setupMocks({}); //Act @@ -188,7 +188,7 @@ describe("Add-to-calendar methods...", () => { test("getappointmentData should return expected model value for appointmentType IN_SHOP.", () => { //Arrange store.getters.order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP; - store.getters.order.damage.isRepair = false; + store.getters.order.damage.isRepair = true; const { wrapper } = setupMocks({}); //Act @@ -239,12 +239,6 @@ function setupMocks({ customMountOptions }) { }); const wrapper = shallowMount(addToCalendar, mountOptions); - - //wrapper.vm.setCmsContent = jest.fn(); - //wrapper.vm.$refs.datePicker.initializeComponent = jest.fn(); - //wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn(); - //wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn(); - return { wrapper }; } diff --git a/src/layouts/add-to-calendar/add-to-calendar.vue b/src/layouts/add-to-calendar/add-to-calendar.vue index 0b856e72d..3f32a525b 100644 --- a/src/layouts/add-to-calendar/add-to-calendar.vue +++ b/src/layouts/add-to-calendar/add-to-calendar.vue @@ -32,6 +32,7 @@ import { import { getCalendarFile, download } from "@/helpers/add-to-calendar-helper"; import { AppointmentTypeStrings, RouteCodeFlags } from "@/constants/schedule-constants"; import store from "@/store"; +import { serviceType } from "@/constants/service-type"; export default { name: "add-to-calendar", @@ -129,12 +130,12 @@ export default { const funnelHasRecalibrationPart = store.getters.funnelHasRecalibrationPart; if (!isRepair) { if (funnelHasRecalibrationPart) { - return "replacement and recalibration"; + return serviceType.REPLACEMENT_AND_RECALIBRATION; } else { - return "replacement"; + return serviceType.REPLACEMENT; } } - return "repair"; + return serviceType.REPAIR; }, routeCode() { return store.getters.order.schedule.routeCode; From f0d7926b1007287bd5ba9ca659a35b720156c93d Mon Sep 17 00:00:00 2001 From: Leah Schumann Date: Mon, 16 Oct 2023 08:31:16 -0400 Subject: [PATCH 12/24] Additional refactoring per tech review --- .../service-location-helper.js | 12 +++ .../mobile-location-modal-questions.vue | 8 +- .../service-location/service-location.vue | 90 +++++++++---------- .../service-zip-modal-question.vue | 9 +- .../shop-question/shop-question.vue | 68 +++++++------- 5 files changed, 86 insertions(+), 101 deletions(-) diff --git a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js index cb4c06100..26f2e6962 100644 --- a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js +++ b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js @@ -46,6 +46,18 @@ export async function getServiceabilityDetails(serviceZipCode, lineItems, pageNa return Promise.resolve(serviceabilityDetails); } +export async function getShopProviderData(serviceZipCode) { + const shopProviderData = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.GET_PROVIDERS, + { + serviceZipCode: serviceZipCode, + }, + "service-location" + ); + + return Promise.resolve(shopProviderData); +} + export async function getAvailabilityRating( startDate, endDate, diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index 58420ce3a..8bac8dbb4 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -145,9 +145,6 @@ export default { alertInvalidZipWidgetName: String, customComponentId: String, validationRules: String, - onZipUpdateCallback: { - type: Function, - }, }, computed: { mobileLocationLinkPromptText() { @@ -256,10 +253,7 @@ export default { this.$emit("updated-serviceability", serviceabilityDetails.data); this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase); - if (this.onZipUpdateCallback) { - await this.onZipUpdateCallback(serviceZipCode); - } - // Update the page level model + // update the page level model this.$emit("update:modelValue", this.internalModel); this.closeModal(); diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 674c47409..b1f296a60 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -20,7 +20,7 @@ @updated-contains-military-base="setContainsMilitaryBase" linkWidgetName="ServiceZipLinkWidget" modalWidgetName="ServiceZipModalWidget" - :onZipUpdateCallback="reloadShopData" /> + /> + /> @@ -135,6 +135,7 @@ import { settleAllPromises } from "@/helpers/layout-helper"; import { getPricedMobileFeePart, getServiceabilityDetails, + getShopProviderData, } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper"; import { Provider } from "@/layouts/service-location/classes/provider"; @@ -180,7 +181,7 @@ export default { mobileFeePart: null, zipContainsMilitaryBase: false, zipCodeCtu: null, - providerData: null, + shopProviderData: null, }; }, async beforeRouteEnter(to, from, next) { @@ -188,7 +189,7 @@ export default { const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const serviceZipCode = store.getters.order.serviceLocation.zipCode; - const getZipCodeData = baseMixin.methods.getZipCodeData(serviceZipCode, "service-location"); + const zipCodeDataPromise = baseMixin.methods.getZipCodeData(serviceZipCode, "service-location"); const serviceabilityDetailsPromise = getServiceabilityDetails( serviceZipCode, @@ -198,7 +199,7 @@ export default { const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, "service-location"); - const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode); + const shopProviderDataPromise = getShopProviderData(serviceZipCode); // shopQuestion.methods.loadInitialData(serviceZipCode); // Settle promises and get results const promiseResultMap = [ @@ -206,6 +207,10 @@ export default { resultKey: "cmsContent", promise: cmsContentPromise, }, + { + resultKey: "zipCodeData", + promise: zipCodeDataPromise, + }, { resultKey: "mobileFeePart", promise: mobileFeePartPromise, @@ -215,12 +220,8 @@ export default { promise: serviceabilityDetailsPromise, }, { - resultKey: "zipCodeData", - promise: getZipCodeData, - }, - { - resultKey: "shopQuestionInitialData", - promise: shopQuestionInitialDataPromise, + resultKey: "shopProviderData", + promise: shopProviderDataPromise, }, ]; @@ -232,14 +233,9 @@ export default { vm.setData( resultMap.zipCodeData, resultMap.serviceabilityDetails, - resultMap.mobileFeePart + resultMap.mobileFeePart, + resultMap.shopProviderData ); - - // Initialize the Shop Question component - vm.$refs.shopQuestion.initializeComponent(resultMap.shopQuestionInitialData); - - // Initialize the page level shop data - vm.providerData = resultMap.shopQuestionInitialData; }); }, computed: { @@ -254,7 +250,7 @@ export default { if (newValue.zipCode !== this.zipCode) { this.resetMobileLocation(); this.selectedAppointmentType = null; - this.selectedProvider = null; + this.selectedProvider = new Provider(); } this.state = newValue.state; @@ -277,20 +273,14 @@ export default { }; }, set: function (newValue) { - this.streetAddress = newValue.addressQuestions.streetAddress; - this.apartmentNumberOrBusinessName = - newValue.addressQuestions.apartmentNumberOrBusinessName; - this.city = newValue.addressQuestions.city; - this.state = newValue.addressQuestions.state; - this.zipCode = newValue.addressQuestions.zipCode; - this.isVehicleProtected = newValue.isVehicleProtected; - - if (newValue.zipCode !== this.zipCode) { - if (!this.selectedAppointmentType == "Mobile") { - this.selectedAppointmentType = null; - } - this.selectedProvider = null; + if (newValue.addressQuestions.zipCode !== this.zipCode) { + getShopProviderData(newValue.addressQuestions.zipCode).then((result) => { + this.shopProviderData = result.data; + this.selectedProvider = new Provider(this.shopProviderData.mobileProviderNumber); + }); } + + this.setMobileLocation(newValue); }, }, isServiceableMobile() { @@ -355,7 +345,7 @@ export default { store.getters.payment.isInsurance !== null ); }, - setData(zipCodeData, serviceabilityDetails, mobileFeePart) { + setData(zipCodeData, serviceabilityDetails, mobileFeePart, shopProviderData) { if (zipCodeData) { this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase; this.zipCodeCtu = zipCodeData.zipCodeCtu; @@ -368,6 +358,10 @@ export default { if (mobileFeePart) { this.mobileFeePart = mobileFeePart; } + + if (shopProviderData) { + this.shopProviderData = shopProviderData; + } }, setContainsMilitaryBase(val) { if (this.zipContainsMilitaryBase !== val) { @@ -416,12 +410,18 @@ export default { this.isRecalibrationServiceableMobile = serviceabilityDetails.isRecalibrationServiceableMobile; }, + setMobileLocation(mobileLocation) { + this.streetAddress = mobileLocation.addressQuestions.streetAddress; + this.apartmentNumberOrBusinessName = + mobileLocation.addressQuestions.apartmentNumberOrBusinessName; + this.city = mobileLocation.addressQuestions.city; + this.state = mobileLocation.addressQuestions.state; + this.zipCode = mobileLocation.addressQuestions.zipCode; + this.isVehicleProtected = mobileLocation.isVehicleProtected; + }, async reloadShopData(zipCode) { await this.$refs.shopQuestion.reloadShopData(zipCode); }, - setUpdatedShopList(providerData) { - this.providerData = providerData; - }, openRecalibrationInformationModal() { this.recalibrationInformationModal.openModal(); }, @@ -497,18 +497,12 @@ export default { }, watch: { selectedAppointmentType: { - handler(newValue) { + async handler(newValue) { if (newValue === "Mobile") { - this.selectedProvider = new Provider(this.providerData.mobileProviderNumber); - } else { - this.selectedProvider = new Provider(); - } - }, - }, - providerData: { - handler(newValue) { - if (newValue === "Mobile") { - this.selectedProvider = new Provider(this.providerData.mobileProviderNumber); + getShopProviderData(this.zipCode).then(async (result) => { + this.shopProviderData = result.data; + this.selectedProvider = new Provider(this.shopProviderData.mobileProviderNumber); + }); } else { this.selectedProvider = new Provider(); } diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue index 34a3d23c1..35276228d 100644 --- a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue +++ b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue @@ -68,9 +68,6 @@ export default { }, linkWidgetName: String, modalWidgetName: String, - onZipUpdateCallback: { - type: Function, - }, }, computed: { serviceZipLinkText() { @@ -164,13 +161,9 @@ export default { this.$emit("updated-serviceability", serviceabilityDetails.data); this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase); - // Update the page level model + // update the page level model this.$emit("update:modelValue", this.internalModel); - if (this.onZipUpdateCallback) { - await this.onZipUpdateCallback(serviceZipCode); - } - this.closeModal(); } } else { diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue index c3f31c68d..30b39f1b0 100644 --- a/src/layouts/service-location/shop-question/shop-question.vue +++ b/src/layouts/service-location/shop-question/shop-question.vue @@ -63,7 +63,6 @@ export default { mixins: [baseMixin], data() { return { - shopProviders: [], shopListButton: shopListButton, answers: [], shopIndex: 0, @@ -76,6 +75,7 @@ export default { default: () => null, }, selectedAppointmentType: String, + shopProviderData: Array, cmsWidgetName: String, validationRules: String, isDisplayed: Boolean, @@ -84,6 +84,9 @@ export default { questionText() { return this.getCmsContent(this.cmsWidgetName, "QuestionText"); }, + shopProviders() { + return this.shopProviderData?.shopProviders ?? []; + }, selectedValue: { get: function () { return this.modelValue; @@ -124,22 +127,23 @@ export default { }, }, methods: { - loadInitialData(serviceZipCode) { - return this.loadData(serviceZipCode); - }, - loadData(serviceZipCode) { - return baseMixin.methods.dispatchStoreActionWithLogging( - storeActions.GET_PROVIDERS, - { - serviceZipCode: serviceZipCode, - }, - "service-location" - ); - }, - initializeComponent(shopQuestionInitialData) { - this.shopProviders = shopQuestionInitialData.shopProviders; - this.$emit("updated-shop-list", shopQuestionInitialData); - }, + // loadInitialData(serviceZipCode) { + // return this.loadData(serviceZipCode); + // }, + // loadData(serviceZipCode) { + // return getShopProviders(serviceZipCode); + // // return baseMixin.methods.dispatchStoreActionWithLogging( + // // storeActions.GET_PROVIDERS, + // // { + // // serviceZipCode: serviceZipCode, + // // }, + // // "service-location" + // // ); + // }, + // initializeComponent(shopQuestionInitialData) { + // this.shopProviders = shopQuestionInitialData.shopProviders; + // this.$emit("updated-shop-list", shopQuestionInitialData); + // }, async getNextShopsFromList(numberToGet = 3) { const shopIterator = (array, n) => { const l = array.length; @@ -199,15 +203,6 @@ export default { this.answers = []; this.shopIndex = 0; }, - async reloadShopData(serviceZipCode) { - const result = await this.loadData(serviceZipCode); - this.initializeComponent(result.data); - - this.resetAnswers(); - - await nextTick(); - await this.getNextShopsFromList(); - }, getSelectedProviderObject(providerNumber) { const provider = this.shopProviders?.find((provider) => provider.providerNumber == providerNumber) ?? @@ -239,19 +234,16 @@ export default { shopProviders: { async handler(newValue) { await nextTick(); + const selectedShopIndex = this.getSelectedProviderIndex( + newValue, + this.selectedProviderNumber + ); - if (this.selectedAppointmentType) { - const selectedShopIndex = this.getSelectedProviderIndex( - newValue, - this.selectedProviderNumber - ); - - if (selectedShopIndex >= 3) { - await this.getNextShopsFromList(selectedShopIndex + 1); - } else { - await this.getNextShopsFromList(); - await nextTick(); - } + if (selectedShopIndex >= 3) { + await this.getNextShopsFromList(selectedShopIndex + 1); + } else { + await this.getNextShopsFromList(); + await nextTick(); } }, }, From 3d23bdbabcb1ed2e71b6ec8be3a24c6aaf2a2fb6 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Mon, 16 Oct 2023 08:38:23 -0400 Subject: [PATCH 13/24] CSR-1593 set column width to control overly-wide components on larger screens. Change scroll to auto for modals to avoid ghost scrollbars when not needed. Center content on all screen sizes. --- src/digital-components/modal/modal.vue | 2 +- .../questions-page-layout.vue | 6 +++--- src/layouts/address-lookup/address-lookup.vue | 2 +- .../address-vehicles/address-vehicles.vue | 2 +- src/layouts/confirmation/confirmation.vue | 2 +- .../customer-details/customer-details.vue | 2 +- src/layouts/estimate/estimate.vue | 2 +- .../license-plate-lookup.vue | 2 +- src/layouts/payment-method/payment-method.vue | 2 +- src/layouts/quote/quote.vue | 2 +- src/layouts/schedule/schedule.vue | 2 +- .../service-location/service-location.vue | 8 ++++---- src/layouts/vehicle-damage/vehicle-damage.vue | 8 ++++---- src/layouts/vehicle-parts/vehicle-parts.vue | 4 ++-- src/layouts/vin-lookup/vin-lookup.vue | 2 +- src/styles/common-styles.scss | 18 ++++++++++++++++++ 16 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index e0353829c..413ca0369 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -179,7 +179,7 @@ export default { } } @include media-breakpoint-up(md) { - overflow: scroll; + overflow: auto; flex: none; } } diff --git a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue index 73d810cc7..8fc382bf0 100644 --- a/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue +++ b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue @@ -7,7 +7,7 @@ -
+
-
+
-
+
-
+
-
+
-
+
diff --git a/src/layouts/customer-details/customer-details.vue b/src/layouts/customer-details/customer-details.vue index 9568b2a13..6e5fc0f09 100644 --- a/src/layouts/customer-details/customer-details.vue +++ b/src/layouts/customer-details/customer-details.vue @@ -6,7 +6,7 @@
-
+
-
+
diff --git a/src/layouts/license-plate-lookup/license-plate-lookup.vue b/src/layouts/license-plate-lookup/license-plate-lookup.vue index ac167cae9..fc3756b6d 100644 --- a/src/layouts/license-plate-lookup/license-plate-lookup.vue +++ b/src/layouts/license-plate-lookup/license-plate-lookup.vue @@ -6,7 +6,7 @@
-
+
-
+
-
+
-
+