diff --git a/jest.config.js b/jest.config.js index f114ef2c3..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: 77, + 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 eb318581a..320952b4d 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", + }, TaxOrderItems: { url: "/price/api/v1/price/taxed-order-items", method: "GET", diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index e5e28dae9..5620612bb 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -51,6 +51,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", @@ -84,6 +86,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 632379de3..af3a0a5ad 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -26,6 +26,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/fmg-components/layouts/questions-page-layout/questions-page-layout.vue b/src/fmg-components/layouts/questions-page-layout/questions-page-layout.vue index 638094e05..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 @@ -14,7 +14,10 @@ :displayGenericVehicleImage="false" /> - + + +
+
{ + 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 validatePromoResponse = null; + let revalidatePromoResponse = null; + + if (hasActivePromos || hasHasInactivePromos) { + revalidatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.REVALIDATE_ORDER_PROMOS_AND_UPDATE_STORE, + null, + pageNameToLog, + false + ); + } + if (hasNewPromo) { + validatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA, + { + promoCode: newPromo, + addableVaps: addableVaps, + }, + pageNameToLog, + false + ); + } + + return { validatePromoResponse, revalidatePromoResponse }; +} + +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/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 }" /> -
- - - - -
-
- -
-
- -
({ 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 e2836ed9e..4773938d0 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,10 @@ 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 { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { setTransitionHooks } from "vue"; defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); export default { @@ -122,14 +127,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 +149,26 @@ export default { resultMap.supportingItems, false ); + + // Promo logic + const promoCodeFromQueryString = getQuerystringParameter(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 +178,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 +265,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/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index b1f296a60..ac7bfcb68 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: { 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 0e7cc2a71..b5ae2e911 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -85,6 +85,7 @@ const getDefaultState = () => { supportingItems: null, vaps: null, serverData: null, + promos: null, }, payment: { isInsurance: null, @@ -95,6 +96,7 @@ const getDefaultState = () => { parentAccountNumber: 0, payNowType: "cc", piaErrorCode: null, + inactivePromos: null, }, schedule: { date: null, @@ -195,6 +197,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; }, @@ -1235,10 +1243,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}`, @@ -1249,15 +1257,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 }) { @@ -1537,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, @@ -1582,6 +1596,7 @@ export const actions = { supportingItems: lineItems.supportingItems, vaps: lineItems.vaps, serverData: lineItems.serverData, + promos: lineItems.promos, }, payment: { InsuranceCoverage: { @@ -1589,6 +1604,7 @@ export const actions = { }, isInsurance: order.payment.isInsurance, parentAccountNumber: order.payment.parentAccountNumber, + inactivePromos: order.payment.inactivePromos, }, serviceLocation: { streetAddress: order.serviceLocation.address, @@ -2007,17 +2023,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( @@ -2139,6 +2159,95 @@ export const actions = { return taxedLineItems; }, + 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); @@ -2203,7 +2312,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); }, @@ -2380,6 +2489,19 @@ function addTaxesToPricedLineItems(lineItems, taxingLineItems) { 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) => { @@ -2414,6 +2536,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 9ae8065ae..1c9a5e92b 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(