From b6e948008af1e1490fdae8f8f2d339377f60a290 Mon Sep 17 00:00:00 2001 From: hiteshkumar87 Date: Wed, 17 Jan 2024 18:23:07 +0530 Subject: [PATCH 01/14] restore user's experiments restore user's experiments on payment method page after reset state. --- src/mixins/experiment-mixin.js | 13 +++---------- src/store/index.js | 6 ++++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/mixins/experiment-mixin.js b/src/mixins/experiment-mixin.js index a7ffd52f0..11b573683 100644 --- a/src/mixins/experiment-mixin.js +++ b/src/mixins/experiment-mixin.js @@ -9,16 +9,9 @@ export default { return Object.hasOwn(store.getters.experimentSettings, settingName); }, getSettingValue(settingName) { - if (this.hasSetting(settingName)) { - return store.getters.experimentSettings[settingName]; - } else if (store.getters.hasSubmittedOrder) { - const experimentSettings = store.getters.submittedOrder.experiments - .filter((x) => x.isActive) - .reduce((r, c) => ({ ...r, ...c.settings }), {}); - return experimentSettings[settingName] ?? null; - } else { - return null; - } + return this.hasSetting(settingName) + ? store.getters.experimentSettings[settingName] + : null; }, }, }; diff --git a/src/store/index.js b/src/store/index.js index d69132c9b..4fe9e4b36 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2511,14 +2511,16 @@ export const actions = { // create a submitted order object from vuex. const submittedOrder = context.state.order; - //add experiments to submittedOrder - submittedOrder.experiments = context.state.applicationUser.experiments; + const experiments = context.state.applicationUser.experiments; // set to local storage window.sessionStorage.setItem("submittedOrder", JSON.stringify(submittedOrder)); // clear vuex context.commit(storeMutations.RESET_STATE); + + //restore user's experiments + context.commit(storeMutations.UPDATE_EXPERIMENTS, experiments); }, resetSubmittedOrder(context) { From f0534f27189e251014f37a0be0380a0bf202abb3 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Wed, 17 Jan 2024 09:55:03 -0500 Subject: [PATCH 02/14] Promo Unit tests - promotions-helper.js Also made promotions-helper.js functions null safe --- jest.config.js | 2 +- src/helpers/promotions-helper.js | 43 +- src/helpers/promotions-helper.spec.js | 1215 +++++++++++++++++++++++++ 3 files changed, 1245 insertions(+), 15 deletions(-) create mode 100644 src/helpers/promotions-helper.spec.js diff --git a/jest.config.js b/jest.config.js index f917aca5d..5df16a837 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,7 +29,7 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 73, + statements: 76, }, }, // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit diff --git a/src/helpers/promotions-helper.js b/src/helpers/promotions-helper.js index db6bfa01d..c9b305f99 100644 --- a/src/helpers/promotions-helper.js +++ b/src/helpers/promotions-helper.js @@ -34,7 +34,7 @@ const stackingPromoErrorCodes = [ promoErrorCodes.SIMILAR_PROMO_ALREADY_ON_ORDER, ]; -const excludeFromInactivePromoErrorCodes = [ +export const excludeFromInactivePromoErrorCodes = [ promoErrorCodes.UNKNOWN, promoErrorCodes.PROMO_NOT_YET_IN_USE, promoErrorCodes.PROMO_USAGE_COUNT_EXCEEDED, @@ -45,7 +45,7 @@ const excludeFromInactivePromoErrorCodes = [ export const pagesToStripPromoQueryStringFrom = ["quote", "payment-method"]; export function getPromosWithAddableVaps(promoList) { - if (!promoList.length) { + if (promoList == null || !promoList.length) { return []; } @@ -53,8 +53,14 @@ export function getPromosWithAddableVaps(promoList) { } export function getLineItemsThatMatchPromos(promos, availableLineItems) { + if (promos == null || availableLineItems == null) { + return []; + } const matchingLineItems = []; promos.forEach((promo) => { + if (promo.discountedLineItemIds == null) { + return; + } promo.discountedLineItemIds.forEach((discountedLineItemId) => { matchingLineItems.push( ...availableLineItems.filter((lineItem) => lineItem.id === discountedLineItemId) @@ -180,6 +186,9 @@ export function buildToastMessagesFromRevalidateOrValidatePromoResponse( oldActivePromos = [], oldInactivePromos = [] ) { + if (promoResponse == null) { + return []; + } const alerts = []; // Revalidate always has an "errors" array if (promoResponse.errors) { @@ -213,19 +222,24 @@ export function buildToastMessagesFromRevalidateOrValidatePromoResponse( ); alerts.push(createPromoSuccessAlert(promoCodeToDisplay)); } else { - alerts.push( - createPromoErrorAlert( - promoResponse.promoCode, - promoResponse.errorCode, - promoResponse.additionalInfo - ) - ); + if (promoResponse.promoCode) { + alerts.push( + createPromoErrorAlert( + promoResponse.promoCode, + promoResponse.errorCode, + promoResponse.additionalInfo + ) + ); + } } } return alerts; } export function getPromosThatMatchLineItemsOnOrder(promos, lineItemsOnOrder) { + if (promos == null || lineItemsOnOrder == null) { + return []; + } const matchingPromoCodes = []; const matchingPromos = []; const consolidatedPromosWithIds = {}; @@ -266,7 +280,7 @@ export function getVapsThatNeedToBeAddedToSatisfyPromos( availableLineItems, lineItemsOnOrder ) { - const clonedVaps = deepClone(lineItemsOnOrder.vaps ?? []); + const clonedVaps = deepClone(lineItemsOnOrder?.vaps ?? []); const promosWithAddableVaps = getPromosWithAddableVaps(promos); if (promosWithAddableVaps.length) { const matchingLineItems = getLineItemsThatMatchPromos( @@ -311,7 +325,7 @@ export function getNewlyInactivatedPromos(oldInactivePromos, newInactivePromos) const inactivePromoCodesFromResponse = getPromoCodesFromPromoObjectsWithoutDuplicates(newInactivePromos); return inactivePromoCodesFromResponse.filter( - (errorPromoCode) => !oldInactivePromos.includes(errorPromoCode) + (errorPromoCode) => !oldInactivePromos?.includes(errorPromoCode) ); } @@ -365,8 +379,9 @@ export function getPromoCodesFromPromoObjectsWithoutDuplicates(promos) { // private methods function findLineItemsWithPartType(typeToFind, itemsToSearch) { - const partTypeMatches = itemsToSearch?.filter( - (lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase() - ); + const partTypeMatches = + itemsToSearch?.filter( + (lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase() + ) ?? []; return partTypeMatches; } diff --git a/src/helpers/promotions-helper.spec.js b/src/helpers/promotions-helper.spec.js new file mode 100644 index 000000000..3e8a1d9aa --- /dev/null +++ b/src/helpers/promotions-helper.spec.js @@ -0,0 +1,1215 @@ +import * as promotionsHelper from "@/helpers/promotions-helper"; +import { partTypeStrings } from "@/constants/part-type-strings"; +import store from "@/store"; +import { storeActions } from "@/constants/store-actions"; +import baseMixin from "@/mixins/base-mixin.js"; + +let mockReturnsForStoreActions = {}; + +jest.mock("@/mixins/base-mixin", () => ({ + ...jest.requireActual("@/mixins/base-mixin"), + methods: { + dispatchStoreAction: jest.fn((action, { activePromos, inactivePromos }, someBool) => {}), + dispatchStoreActionWithLogging: jest.fn( + (action, { promoCode, addableVaps }, pageNameToLog, someBool) => { + return mockReturnsForStoreActions[action]; + } + ), + }, +})); + +afterEach(() => { + // reset store after each test + store.getters.lineItems.promos = null; + store.getters.payment.inactivePromos = null; + // reset store action returns + mockReturnsForStoreActions = {}; + // reset mock method call counters + baseMixin.methods.dispatchStoreAction.mockClear(); + baseMixin.methods.dispatchStoreActionWithLogging.mockClear(); +}); + +describe("promotions-helper.js", () => { + describe("expected return tests", () => { + describe("getPromosWithAddableVaps", () => { + it("returns an empty array when promoList is null or empty", () => { + // Arrange + const emptyPromoList = []; + const nullPromoList = null; + + // Act + const emptyPromoListReturn = + promotionsHelper.getPromosWithAddableVaps(emptyPromoList); + const nullPromoListReturn = + promotionsHelper.getPromosWithAddableVaps(nullPromoList); + // Assert + expect(emptyPromoListReturn).toEqual([]); + expect(nullPromoListReturn).toEqual([]); + }); + it("returns an empty array when no promo with addable vaps is provided", () => { + // Arrange + const promoIdentifier = "identifierString"; + const promoListWithAddableVapsPromo = [ + { promoIdentifier: promoIdentifier, partNumber: "fakekPromoPartNumber" }, + ]; + + // Act + const promoListWithAddableVapsPromoReturn = + promotionsHelper.getPromosWithAddableVaps(promoListWithAddableVapsPromo); + + // Assert + expect(promoListWithAddableVapsPromoReturn).toEqual([]); + }); + it("returns an array with the provided promo when a promo with addable vaps is provided", () => { + // Arrange + const promoIdentifier = "identifierString"; + const promoListWithAddableVapsPromo = [ + { + promoIdentifier: promoIdentifier, + partNumber: promotionsHelper.addableVapsPromoPartNumbers[0], + }, + ]; + + // Act + const promoListWithAddableVapsPromoReturn = + promotionsHelper.getPromosWithAddableVaps(promoListWithAddableVapsPromo); + + // Assert + expect(promoListWithAddableVapsPromoReturn[0].promoIdentifier).toEqual( + promoIdentifier + ); + }); + }); + describe("getLineItemsThatMatchPromos", () => { + it("returns an empty array when parameters or their children are null or empty", () => { + // Arrange + const validGuidForId = "09bd698e-97b3-4fca-a707-5cb16a93a127"; + + // setup + const nullPromos = null; + const emptyPromos = []; + const promosWithNullDiscountedLineItemIds = [{ discountedLineItemIds: null }]; + const promosWithEmptyDiscountedLineItemIds = [{ discountedLineItemIds: [] }]; + const validPromosWithOnePromo = [{ discountedLineItemIds: [validGuidForId] }]; + + // scenarios + const nullPromosReturn = promotionsHelper.getLineItemsThatMatchPromos( + nullPromos, + validAvailableLineItemsWithOneItem + ); + const emptyPromosReturn = promotionsHelper.getLineItemsThatMatchPromos( + emptyPromos, + validAvailableLineItemsWithOneItem + ); + const nullDiscountedLineItemsReturn = promotionsHelper.getLineItemsThatMatchPromos( + promosWithNullDiscountedLineItemIds, + validAvailableLineItemsWithOneItem + ); + const emptyDiscountedLineItemsReturn = promotionsHelper.getLineItemsThatMatchPromos( + promosWithEmptyDiscountedLineItemIds, + validAvailableLineItemsWithOneItem + ); + + // Null or Empty scenarios + const nullAvailableLineItemsReturn = promotionsHelper.getLineItemsThatMatchPromos( + validPromosWithOnePromo, + nullAvailableLineItems + ); + const emptyAvailableLineItemsReturn = promotionsHelper.getLineItemsThatMatchPromos( + validPromosWithOnePromo, + emptyAvailableLineItems + ); + + // Assert + // Null or Empty assertions + expect(nullPromosReturn).toEqual([]); + expect(emptyPromosReturn).toEqual([]); + expect(nullDiscountedLineItemsReturn).toEqual([]); + expect(emptyDiscountedLineItemsReturn).toEqual([]); + + // Null or Empty assertions + expect(nullAvailableLineItemsReturn).toEqual([]); + expect(emptyAvailableLineItemsReturn).toEqual([]); + }); + it("returns the matching availableLineItem when provided valid, matching data in promos", () => { + const promos = [ + { + discountedLineItemIds: [1, 2], + }, + ]; + const availableLineItems = [ + { + id: 1, + name: "Item 1", + }, + { + id: 2, + name: "Item 2", + }, + { + id: 3, + name: "Item 3", + }, + ]; + const expected = [ + { + id: 1, + name: "Item 1", + }, + { + id: 2, + name: "Item 2", + }, + ]; + expect( + promotionsHelper.getLineItemsThatMatchPromos(promos, availableLineItems) + ).toEqual(expected); + }); + }); + describe("getAddableVapsFromAvailableLineItems", () => { + it("returns empty array if availableLineItems is null or empty", () => { + // Arrange + const nullAvailableLineItems = null; + const emptyAvailableLineItems = []; + + // Act + const nullAvailableLineItemsReturn = + promotionsHelper.getAddableVapsFromAvailableLineItems(nullAvailableLineItems); + const emptyAvailableLineItemsReturn = + promotionsHelper.getAddableVapsFromAvailableLineItems(emptyAvailableLineItems); + + // Assert + expect(nullAvailableLineItemsReturn).toEqual([]); + expect(emptyAvailableLineItemsReturn).toEqual([]); + }); + it("returns provided front wiper and rain defense items from availableLineItems", () => { + // Arrange + const availableLineItemsWithFrontWipersAndRainDefense = [ + { partType: partTypeStrings.FRONT_WIPER }, + { partType: partTypeStrings.RAIN_DEFENSE }, + { partType: partTypeStrings.REAR_WIPER }, + { partType: partTypeStrings.RECALIBRATION }, + { partType: partTypeStrings.REPLACE_FEE }, + { partType: "Windshield" }, + ]; + + // Act + const addableVapsFromAvailableLineItems = + promotionsHelper.getAddableVapsFromAvailableLineItems( + availableLineItemsWithFrontWipersAndRainDefense + ); + + // Assert + const frontWiperParts = addableVapsFromAvailableLineItems.filter( + (lineItem) => lineItem.partType == partTypeStrings.FRONT_WIPER + ); + const rainDefenseParts = addableVapsFromAvailableLineItems.filter( + (lineItem) => lineItem.partType == partTypeStrings.RAIN_DEFENSE + ); + expect(addableVapsFromAvailableLineItems.length).toEqual(2); + expect(frontWiperParts.length).toEqual(1); + expect(rainDefenseParts.length).toEqual(1); + }); + }); + describe("removeVapsPromosFromPromoArray", () => { + it("returns empty array if promoArray is null or empty", () => { + // Arrange + const nullPromoArray = null; + const emptyPromoArray = []; + + // Act + const nullPromoArrayReturn = + promotionsHelper.removeVapsPromosFromPromoArray(nullPromoArray); + const emptyPromoArrayReturn = + promotionsHelper.removeVapsPromosFromPromoArray(emptyPromoArray); + + // Assert + expect(nullPromoArrayReturn).toEqual([]); + expect(emptyPromoArrayReturn).toEqual([]); + }); + it("returns list of promos without vapsPromos included in it", () => { + // Arrange + const fullPromoArray = [ + { + partNumber: + promotionsHelper.promoPartNumberStrings.WIPER_DISCOUNT_PART_NUMBER, + }, + { + partNumber: + promotionsHelper.promoPartNumberStrings + .RAIN_DEFENSE_DISCOUNT_PART_NUMBER, + }, + { + partNumber: + promotionsHelper.promoPartNumberStrings.GLASS_DISCOUNT_PART_NUMBER, + }, + { + partNumber: + promotionsHelper.promoPartNumberStrings + .GLASS_CLEANER_DISCOUNT_PART_NUMBER, + }, + ]; + + // Act + const promosWithoutVapsPromos = + promotionsHelper.removeVapsPromosFromPromoArray(fullPromoArray); + + // Assert + const glassPromoArray = promosWithoutVapsPromos.filter( + (promo) => + promo.partNumber == + promotionsHelper.promoPartNumberStrings.GLASS_DISCOUNT_PART_NUMBER + ); + expect(promosWithoutVapsPromos.length).toEqual(1); + expect(glassPromoArray.length).toEqual(1); + }); + }); + describe("revalidatePromosAndValidateQueryStringPromo", () => { + it("returns null responses if active and inactive promos are null in store and no new promo", async () => { + // Arrange + const newPromo = null; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + store.getters.order.lineItems.promos = null; + store.getters.order.payment.inactivePromos = null; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(validatePromoResponse).toBeNull(); + expect(revalidatePromoResponse).toBeNull(); + }); + it("returns null responses if active and inactive promos are empty arrays in store and no new promo", async () => { + // Arrange + const newPromo = null; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + store.getters.order.lineItems.promos = []; + store.getters.order.payment.inactivePromos = []; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(validatePromoResponse).toBeNull(); + expect(revalidatePromoResponse).toBeNull(); + }); + it("returns a revalidate response if store has active promos", async () => { + // Arrange + const newPromo = null; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + store.getters.order.lineItems.promos = ["data"]; + store.getters.order.payment.inactivePromos = []; + const revalidateResponse = { errors: [] }; + mockReturnsForStoreActions[ + storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA + ] = revalidateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(validatePromoResponse).toBeNull(); + expect(revalidatePromoResponse).toEqual({ errors: [] }); + }); + it("returns a revalidate response if store has inactive promos", async () => { + // Arrange + const newPromo = null; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + store.getters.order.lineItems.promos = []; + store.getters.order.payment.inactivePromos = ["data"]; + const revalidateResponse = { errors: [] }; + mockReturnsForStoreActions[ + storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA + ] = revalidateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(validatePromoResponse).toBeNull(); + expect(revalidatePromoResponse).toEqual(revalidateResponse); + }); + it("returns a validate response if a new promo is provided", async () => { + // Arrange + const newPromo = "testPromo"; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + const validateResponse = { orderPromos: [] }; + mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] = + validateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(revalidatePromoResponse).toBeNull(); + expect(validatePromoResponse).toEqual(validateResponse); + }); + it("returns a validate and revalidate response if a new promo is provided and inactive/active promos are in the store", async () => { + // Arrange + const newPromo = "testPromo"; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + const validateResponse = { orderPromos: [] }; + const revalidateResponse = { errors: [] }; + store.getters.order.lineItems.promos = ["data"]; + store.getters.order.payment.inactivePromos = ["data"]; + mockReturnsForStoreActions[ + storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA + ] = revalidateResponse; + mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] = + validateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(revalidatePromoResponse).toEqual(revalidateResponse); + expect(validatePromoResponse).toEqual(validateResponse); + }); + }); + describe("buildToastMessagesFromRevalidateOrValidatePromoResponse", () => { + it("returns an empty array if promoResponse parameter is null or an unexpected format", () => { + // Arrange + const nullPromoResponse = null; + const unexpectedPromoResponse = { unexpectedKey: "unexpectedValue" }; + + // Act + const nullPromoResponseReturn = + promotionsHelper.buildToastMessagesFromRevalidateOrValidatePromoResponse( + nullPromoResponse + ); + const unexpectedPromoResponseReturn = + promotionsHelper.buildToastMessagesFromRevalidateOrValidatePromoResponse( + unexpectedPromoResponse + ); + + // Assert + expect(nullPromoResponseReturn).toEqual([]); + expect(unexpectedPromoResponseReturn).toEqual([]); + }); + describe("validatePromoResponse scenarios", () => { + it("returns an alert for success and error scenarios", () => { + // Arrange + const successPromoResponse = { orderPromos: [{ promoCode: "testPromo" }] }; + const errorPromoResponse = { promoCode: "errorTestPromo" }; + + // Act + const successPromoResponseReturn = + promotionsHelper.buildToastMessagesFromRevalidateOrValidatePromoResponse( + successPromoResponse + ); + const errorPromoResponseReturn = + promotionsHelper.buildToastMessagesFromRevalidateOrValidatePromoResponse( + errorPromoResponse + ); + + // Assert + // This depends on "createPromoSuccessAlert" implementation, which is bad practice + // but jest has trouble mocking same file dependencies + expect(successPromoResponseReturn[0]).toHaveProperty("type", "alert-success"); + expect(errorPromoResponseReturn[0]).toHaveProperty("type", "alert-danger"); + }); + it("returns only one success message for a bundle validate promo response", () => { + // Arrange + const bundlePromoResponse = { + orderPromos: [ + { promoCode: "bundlePromo/458" }, + { promoCode: "bundlePromo/459" }, + ], + }; + // Act + const bundlePromoResponseReturn = + promotionsHelper.buildToastMessagesFromRevalidateOrValidatePromoResponse( + bundlePromoResponse + ); + + // Assert + // This depends on "createPromoSuccessAlert" implementation, which is bad practice + // but jest has trouble mocking same file dependencies + expect(bundlePromoResponseReturn).toHaveLength(1); + }); + }); + describe("revalidatePromoResponse scenarios", () => { + it("returns alerts for new success and new error scenario", () => { + // Arrange + const successAndErrorPromoResponse = { + promoLineItems: [{ promoCode: "successPromo" }], + errors: [{ promoCode: "errorPromo" }], + }; + + // Act + const successAndErrorPromoResponseReturn = + promotionsHelper.buildToastMessagesFromRevalidateOrValidatePromoResponse( + successAndErrorPromoResponse + ); + + // Assert + // This depends on "createPromoSuccessAlert" implementation, which is bad practice + // but jest has trouble mocking same file dependencies + const successAlert = successAndErrorPromoResponseReturn.filter((alert) => + alert.messageCopy.toLowerCase().includes("successpromo") + ); + expect(successAlert[0]).toHaveProperty("type", "alert-success"); + const errorAlert = successAndErrorPromoResponseReturn.filter((alert) => + alert.messageCopy.toLowerCase().includes("errorpromo") + ); + expect(errorAlert[0]).toHaveProperty("type", "alert-danger"); + }); + it("returns only alerts for new success and new error scenario", () => { + // Arrange + const successAndErrorPromoResponse = { + promoLineItems: [ + { promoCode: "successPromo" }, + { promoCode: "successPromoAlreadyActive" }, + ], + errors: [ + { promoCode: "errorPromo" }, + { promoCode: "errorPromoAlreadyInactive" }, + ], + }; + const oldActivePromos = [{ promoCode: "successPromoAlreadyActive" }]; + const oldInactivePromos = ["errorPromoAlreadyInactive"]; + // Act + const successAndErrorPromoResponseReturn = + promotionsHelper.buildToastMessagesFromRevalidateOrValidatePromoResponse( + successAndErrorPromoResponse, + oldActivePromos, + oldInactivePromos + ); + + // Assert + // This depends on "createPromoSuccessAlert" implementation, which is bad practice + // but jest has trouble mocking same file dependencies + const successAlert = successAndErrorPromoResponseReturn.filter((alert) => + alert.messageCopy.toLowerCase().includes("successpromo") + ); + expect(successAlert[0]).toHaveProperty("type", "alert-success"); + const errorAlert = successAndErrorPromoResponseReturn.filter((alert) => + alert.messageCopy.toLowerCase().includes("errorpromo") + ); + expect(errorAlert[0]).toHaveProperty("type", "alert-danger"); + }); + }); + }); + describe("getPromosThatMatchLineItemsOnOrder", () => { + it("returns an empty array when either parameter is null or empty", () => { + // Arrange + const nullPromos = null; + const emptyPromos = []; + const validPromos = [{ promoCode: "validPromo", discountedLineItemIds: [1] }]; + + const nullLineItemsOnOrder = null; + const emptyLineItemsOnOrder = []; + const validLineItemsOnOrder = [{ id: 1 }]; + + // Act + // Bad promos + const nullPromosReturn = promotionsHelper.getPromosThatMatchLineItemsOnOrder( + nullPromos, + validLineItemsOnOrder + ); + const emptyPromosReturn = promotionsHelper.getPromosThatMatchLineItemsOnOrder( + emptyPromos, + validLineItemsOnOrder + ); + + // Bad lineItemsOnOrder + const nullLineItemsOnOrderReturn = + promotionsHelper.getPromosThatMatchLineItemsOnOrder( + validPromos, + nullLineItemsOnOrder + ); + const emptyLineItemsOnOrderReturn = + promotionsHelper.getPromosThatMatchLineItemsOnOrder( + validPromos, + emptyLineItemsOnOrder + ); + + // Bad both + const nullBothReturn = promotionsHelper.getPromosThatMatchLineItemsOnOrder( + nullPromos, + nullLineItemsOnOrder + ); + const emptyBothReturn = promotionsHelper.getPromosThatMatchLineItemsOnOrder( + emptyPromos, + emptyLineItemsOnOrder + ); + + // Assert + expect(nullPromosReturn).toEqual([]); + expect(emptyPromosReturn).toEqual([]); + expect(nullLineItemsOnOrderReturn).toEqual([]); + expect(emptyLineItemsOnOrderReturn).toEqual([]); + expect(nullBothReturn).toEqual([]); + expect(emptyBothReturn).toEqual([]); + }); + it("returns only matching promos with all discountedLineItemIds in lineItemsOnOrder", () => { + // Arrange + const promos = [ + { promoCode: "validPromo", discountedLineItemIds: [1, 2] }, + { promoCode: "invalidPromo", discountedLineItemIds: [3, 4] }, + ]; + const lineItemsOnOrder = [{ id: 1 }, { id: 2 }, { id: 3 }]; + + const expectedReturnPromos = [ + { promoCode: "validPromo", discountedLineItemIds: [1, 2] }, + ]; + + // Act + const returnedItems = promotionsHelper.getPromosThatMatchLineItemsOnOrder( + promos, + lineItemsOnOrder + ); + + // Assert + expect(returnedItems).toEqual(expectedReturnPromos); + }); + it("returns a bundle promo only if BOTH bundle pieces are satisfied", () => { + // Arrange + const promos = [ + { promoCode: "validPromo/400", discountedLineItemIds: [1, 2] }, + { promoCode: "validPromo/401", discountedLineItemIds: [3, 4] }, + { promoCode: "invalidPromo/500", discountedLineItemIds: [5, 6] }, + { promoCode: "invalidPromo/501", discountedLineItemIds: [7, 8] }, + ]; + // NOTE: invalidPromo/500 is satisfied with below lineItems while /501 is not + const lineItemsOnOrder = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + { id: 6 }, + { id: 7 }, + ]; + + const expectedReturnPromos = [ + { promoCode: "validPromo/400", discountedLineItemIds: [1, 2] }, + { promoCode: "validPromo/401", discountedLineItemIds: [3, 4] }, + ]; + + // Act + const returnedItems = promotionsHelper.getPromosThatMatchLineItemsOnOrder( + promos, + lineItemsOnOrder + ); + + // Assert + expect(returnedItems).toEqual(expectedReturnPromos); + }); + }); + describe("getVapsThatNeedToBeAddedToSatisfyPromos", () => { + it("returns an empty array when promos or availableLineItems are null or empty", () => { + // Arrange + const nullPromos = null; + const emptyPromos = []; + const validPromos = [ + { + promoCode: "validPromo", + partNumber: promotionsHelper.addableVapsPromoPartNumbers[0], + discountedLineItemIds: [1], + }, + ]; + + const nullAvailableLineItems = null; + const emptyAvailableLineItems = []; + const validAvailableLineItems = [{ id: 1 }]; + + const nullLineItemsOnOrder = null; + const emptyLineItemsOnOrder = []; + const validLineItemsOnOrder = { vaps: [{ id: 1 }] }; + + // Act + // Bad promos + const nullPromosReturn = promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + nullPromos, + validAvailableLineItems, + validLineItemsOnOrder + ); + const emptyPromosReturn = promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + emptyPromos, + validAvailableLineItems, + validLineItemsOnOrder + ); + + // Bad availableLineItems + const nullAvailableLineItemsReturn = + promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + validPromos, + nullAvailableLineItems, + validLineItemsOnOrder + ); + const emptyAvailableLineItemsReturn = + promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + validPromos, + emptyAvailableLineItems, + validLineItemsOnOrder + ); + + // Bad all + const nullAllReturn = promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + nullPromos, + nullAvailableLineItems, + nullLineItemsOnOrder + ); + const emptyAllReturn = promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + emptyPromos, + emptyAvailableLineItems, + emptyLineItemsOnOrder + ); + + // Assert + expect(nullPromosReturn).toEqual([]); + expect(emptyPromosReturn).toEqual([]); + expect(nullAvailableLineItemsReturn).toEqual([]); + expect(emptyAvailableLineItemsReturn).toEqual([]); + expect(nullAllReturn).toEqual([]); + expect(emptyAllReturn).toEqual([]); + }); + it("returns vaps that need to be provided to satisfy promo only", () => { + // Arrange + const promos = [ + { + promoCode: "validPromo", + partNumber: promotionsHelper.addableVapsPromoPartNumbers[0], + discountedLineItemIds: [1], + }, + { + promoCode: "invalidPromo", + partNumber: "notAVapsPromo", + discountedLineItemIds: [2], + }, + ]; + const availableLineItems = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const lineItemsOnOrder = []; + + const expectedReturn = [{ id: 1 }]; + + // Act + const actualReturn = promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + promos, + availableLineItems, + lineItemsOnOrder + ); + + // Assert + expect(actualReturn).toEqual(expectedReturn); + }); + it("returns no vaps if those vaps are already on the order", () => { + // Arrange + const promos = [ + { + promoCode: "validPromo", + partNumber: promotionsHelper.addableVapsPromoPartNumbers[0], + discountedLineItemIds: [1], + }, + { + promoCode: "invalidPromo", + partNumber: "notAVapsPromo", + discountedLineItemIds: [2], + }, + ]; + const availableLineItems = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const lineItemsOnOrder = { vaps: [{ id: 1 }] }; + + const expectedReturn = []; + + // Act + const actualReturn = promotionsHelper.getVapsThatNeedToBeAddedToSatisfyPromos( + promos, + availableLineItems, + lineItemsOnOrder + ); + + // Assert + expect(actualReturn).toEqual(expectedReturn); + }); + }); + describe("removeCurrentlyActivePromoCodesFromInactivePromos", () => { + it("returns an empty array if inactivePromos is null or empty, don't break if activePromoObjects is null or empty", () => { + // Arrange + const nullActivePromoObjects = null; + const emptyActivePromoObjects = []; + const validActivePromoObjects = [{ promoCode: "validPromo" }]; + + const nullInactivePromos = null; + const emptyInactivePromos = []; + // Act + const nullInactivePromosReturn = + promotionsHelper.removeCurrentlyActivePromoCodesFromInactivePromos( + validActivePromoObjects, + nullInactivePromos + ); + const emptyInactivePromosReturn = + promotionsHelper.removeCurrentlyActivePromoCodesFromInactivePromos( + validActivePromoObjects, + emptyInactivePromos + ); + + const allNullReturn = + promotionsHelper.removeCurrentlyActivePromoCodesFromInactivePromos( + nullActivePromoObjects, + nullInactivePromos + ); + const allEmptyReturn = + promotionsHelper.removeCurrentlyActivePromoCodesFromInactivePromos( + emptyActivePromoObjects, + emptyInactivePromos + ); + + // Assert + expect(nullInactivePromosReturn).toEqual([]); + expect(emptyInactivePromosReturn).toEqual([]); + expect(allNullReturn).toEqual([]); + expect(allEmptyReturn).toEqual([]); + }); + it("returns inactivePromos with active promos removed", () => { + // Arrange + const activePromoObjects = [{ promoCode: "activePromo" }]; + const inactivePromos = ["activePromo", "inactivePromo"]; + + const expectedReturn = ["inactivePromo"]; + + // Act + const actualReturn = + promotionsHelper.removeCurrentlyActivePromoCodesFromInactivePromos( + activePromoObjects, + inactivePromos + ); + + // Assert + expect(actualReturn).toEqual(expectedReturn); + }); + it("returns inactivePromos with active bundle promos removed", () => { + // Arrange + const activePromoObjects = [ + { promoCode: "activeBundlePromo/400" }, + { promoCode: "activeBundlePromo/401" }, + ]; + const inactivePromos = ["activeBundlePromo", "inactivePromo"]; + + const expectedReturn = ["inactivePromo"]; + + // Act + const actualReturn = + promotionsHelper.removeCurrentlyActivePromoCodesFromInactivePromos( + activePromoObjects, + inactivePromos + ); + + // Assert + expect(actualReturn).toEqual(expectedReturn); + }); + it("returns all inactivePromos when no matching activePromo is supplied", () => { + // Arrange + const activePromoObjects = [{ promoCode: "activePromo" }]; + const inactivePromos = ["inactivePromo1", "inactivePromo2"]; + + // Act + const actualReturn = + promotionsHelper.removeCurrentlyActivePromoCodesFromInactivePromos( + activePromoObjects, + inactivePromos + ); + + // Assert + expect(actualReturn).toEqual(inactivePromos); + }); + }); + describe("shouldStripPromoQueryString", () => { + it("returns false when provided with a null parameter", () => { + // Arrange + const nullFmgPageQueryValue = null; + + // Act + const actualReturn = + promotionsHelper.shouldStripPromoQueryString(nullFmgPageQueryValue); + + // Assert + expect(actualReturn).toBeFalsy(); + }); + it("returns true when provided with a parameter from 'pagesToStripPromoQueryStringFrom", () => { + // Arrange + const fmgPageQueryValue = promotionsHelper.pagesToStripPromoQueryStringFrom[0]; + + // Act + const actualReturn = + promotionsHelper.shouldStripPromoQueryString(fmgPageQueryValue); + + // Assert + expect(actualReturn).toBeTruthy(); + }); + }); + describe("getNewlyInactivatedPromos", () => { + it("returns an empty array if newInactivePromos is null or empty, does not break with null oldInactivePromos", () => { + // Arrange + const nullNewInactivePromos = null; + const emptyNewInactivePromos = []; + const validNewInactivePromos = [{ promoCode: "validPromo" }]; + + const nullOldInactivePromos = null; + const validOldInactivePromos = ["validPromo"]; + + // Act + const nullNewInactivePromosReturn = promotionsHelper.getNewlyInactivatedPromos( + validOldInactivePromos, + nullNewInactivePromos + ); + const emptyNewInactivePromosReturn = promotionsHelper.getNewlyInactivatedPromos( + validOldInactivePromos, + emptyNewInactivePromos + ); + + const nullOldInactivePromosReturn = promotionsHelper.getNewlyInactivatedPromos( + nullOldInactivePromos, + validNewInactivePromos + ); + + const allNullReturn = promotionsHelper.getNewlyInactivatedPromos( + nullOldInactivePromos, + nullNewInactivePromos + ); + + // Assert + expect(nullNewInactivePromosReturn).toEqual([]); + expect(emptyNewInactivePromosReturn).toEqual([]); + expect(allNullReturn).toEqual([]); + // This return was to make sure no errors occurred + expect(nullOldInactivePromosReturn).not.toBeNull(); + }); + it("returns only inactivePromos that were not already in oldInactivePromos", () => { + // Arrange + const newInactivePromos = [ + { promoCode: "filteredPromo" }, + { promoCode: "nonFilteredPromo" }, + ]; + const oldInactivePromos = ["filteredPromo"]; + + const expectedReturn = ["nonFilteredPromo"]; + + // Act + const actualReturn = promotionsHelper.getNewlyInactivatedPromos( + oldInactivePromos, + newInactivePromos + ); + + // Assert + expect(actualReturn).toEqual(expectedReturn); + }); + it("returns only one new inactivePromos when a new promo bundle is inactivated", () => { + // Arrange + const newInactivePromos = [ + { promoCode: "nonFilteredPromo/400" }, + { promoCode: "nonFilteredPromo/401" }, + ]; + const oldInactivePromos = ["filteredPromo"]; + + const expectedReturn = ["nonFilteredPromo"]; + + // Act + const actualReturn = promotionsHelper.getNewlyInactivatedPromos( + oldInactivePromos, + newInactivePromos + ); + + // Assert + expect(actualReturn).toEqual(expectedReturn); + }); + }); + describe("createPromoErrorAlert", () => { + it("returns different messages for stacking error codes and any others", () => { + // Arrange + const promoCode = "testPromo"; + const additionalInfo = ["testAdditionalInfo"]; + + const stackingErrorCode = + promotionsHelper.promoErrorCodes.PROMO_STACKING_NOT_ALLOWED; + const nonStackingErrorCode = "nonStackingErrorCode"; + + // Act + const stackingReturn = promotionsHelper.createPromoErrorAlert( + promoCode, + stackingErrorCode, + additionalInfo + ); + const nonStackingReturn = promotionsHelper.createPromoErrorAlert( + promoCode, + nonStackingErrorCode, + additionalInfo + ); + + // Assert + expect(stackingReturn).not.toEqual(nonStackingReturn); + }); + }); + describe("getPromoCodesFromPromoObjectsWithoutDuplicates", () => { + it("returns an empty array when promos is null or empty", () => { + // Arrange + const nullPromos = null; + const emptyPromos = []; + + // Act + const nullPromosReturn = + promotionsHelper.getPromoCodesFromPromoObjectsWithoutDuplicates(nullPromos); + const emptyPromosReturn = + promotionsHelper.getPromoCodesFromPromoObjectsWithoutDuplicates(emptyPromos); + + // Assert + expect(nullPromosReturn).toEqual([]); + expect(emptyPromosReturn).toEqual([]); + }); + it("returns promo codes excluding normal and bundle duplicates", () => { + // Arrange + const promos = [ + { promoCode: "duplicatePromo" }, + { promoCode: "duplicatePromo" }, + { promoCode: "bundlePromo/400" }, + { promoCode: "bundlePromo/401" }, + ]; + const expectedReturn = ["duplicatePromo", "bundlePromo"]; + + // Act + const actualReturn = + promotionsHelper.getPromoCodesFromPromoObjectsWithoutDuplicates(promos); + + // Assert + expect(actualReturn).toEqual(expectedReturn); + }); + }); + }); + describe("expected method calls tests", () => { + describe("revalidatePromosAndValidateQueryStringPromo", () => { + it("should call saveActiveAndOrInactivePromos store method upon revalidation AND validation", async () => { + // Arrange + const newPromo = "testPromo"; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + const revalidateResponse = { errors: [] }; + const validateResponse = { orderPromos: [] }; + store.getters.order.lineItems.promos = ["data"]; + store.getters.order.payment.inactivePromos = ["data"]; + mockReturnsForStoreActions[ + storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA + ] = revalidateResponse; + mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] = + validateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + expect.anything(), + expect.anything() + ); + expect(baseMixin.methods.dispatchStoreAction).toBeCalledTimes(2); + }); + it("should save an inactive promo if the newPromo is invalid, not on the order, and has the right error code", async () => { + // This is behavior needed to let a query string promo activate if they later + // change their order and it becomes valid + // Arrange + const newPromo = "testPromo"; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + // Query string promos are added to inactive if it's a valid promo that doesn't currently work + // No suitable item for promo is an example of this + const validateResponse = { + promoCode: newPromo, + errorCode: promotionsHelper.promoErrorCodes.NO_SUITABLE_ITEM_FOR_PROMO, + }; + mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] = + validateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { inactivePromos: ["testPromo"] }, + expect.anything() + ); + }); + it("should not save an inactive promo if the promo is already an active promo (read comment below)", async () => { + // should NOT save an inactive promo if the newPromo is invalid, + // not on the order, and has the right error code - but the promo + // is already in the store as active + // This is behavior needed to make sure the query string inactive logic above + // doesn't result in duplicated promos + // Arrange + const newPromo = "testPromo"; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + store.getters.order.lineItems.promos = [{ promoCode: newPromo }]; + const revalidateResponse = { errors: [] }; + const validateResponse = { + promoCode: newPromo, + errorCode: promotionsHelper.promoErrorCodes.NO_SUITABLE_ITEM_FOR_PROMO, + }; + mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] = + validateResponse; + mockReturnsForStoreActions[ + storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA + ] = revalidateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).not.toBeCalledWith( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { inactivePromos: ["testPromo"] }, + expect.anything() + ); + }); + it("should not save an inactive promo if the promo is on the 'excludeFromInactivePromoErrorCodes' list", async () => { + // The excludeFromInactivePromoErrorCodes list includes promos that will never be valid + // so there is no value in adding them to the 'inactivePromos' in the store for future + // use + // Arrange + const newPromo = "testPromo"; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + const validateResponse = { + promoCode: newPromo, + errorCode: promotionsHelper.excludeFromInactivePromoErrorCodes[0], + }; + mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] = + validateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).not.toBeCalledWith( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + expect.anything(), + expect.anything() + ); + }); + it("should not save an inactive promo if the promo is already in the store as inactive", async () => { + // Arrange + const newPromo = "testPromo"; + const pricedLineItems = null; + const pageNameToLog = "testPage"; + const revalidateResponse = { errors: [] }; + const validateResponse = { promoCode: newPromo, errorCode: "someError" }; + store.getters.order.payment.inactivePromos = [newPromo]; + mockReturnsForStoreActions[ + storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA + ] = revalidateResponse; + mockReturnsForStoreActions[storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA] = + validateResponse; + + // Act + const { validatePromoResponse, revalidatePromoResponse } = + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + newPromo, + pricedLineItems, + pageNameToLog + ); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).not.toBeCalledWith( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { inactivePromos: [newPromo] }, + expect.anything() + ); + }); + it("should call SAVE_INACTIVE_AND_OR_ACTIVE_PROMOS with a list of strings for the inactivePromos parameter", async () => { + // The store method expects a list of strings for inactivePromos, this call needs to satisfy that + // Arrange + store.getters.order.lineItems.promos = ["data"]; + store.getters.order.payment.inactivePromos = ["data"]; + const revalidateResponse = { + promoLineItems: ["data"], + errors: [ + { id: 1, promoCode: "testPromo1" }, + { id: 2, promoCode: "testPromo2" }, + ], + }; + mockReturnsForStoreActions[ + storeActions.REVALIDATE_ORDER_PROMOS_AND_SAVE_SERVER_DATA + ] = revalidateResponse; + // Act + await promotionsHelper.revalidatePromosAndValidateQueryStringPromo( + null, + null, + null + ); + + // Assert + expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith( + storeActions.SAVE_ACTIVE_AND_OR_INACTIVE_PROMOS, + { + activePromos: expect.anything(), + inactivePromos: ["testPromo1", "testPromo2"], + }, + false + ); + }); + }); + }); +}); From 6fe0f5f1cd750f322028ab644d1d4308432a3474 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Thu, 18 Jan 2024 12:57:25 -0500 Subject: [PATCH 03/14] CSR-1922 fix misalignment of green checkmark on confirmation page. --- src/layouts/confirmation/confirmation.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index d9fe2e479..8b5a0ce32 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -387,7 +387,9 @@ export default { text-decoration: underline; } .header-container { - text-align: center; + display: flex; + align-items: center; + justify-content: center; padding: 1rem 0 1.5rem 0; p { @@ -395,6 +397,11 @@ export default { font-weight: 400; font-size: 1.25rem; color: $black; + margin: 0; + } + + img { + margin-right: .5rem; } } From cffa7c0f07a72196d34825af5462891d112d0a78 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Thu, 18 Jan 2024 13:12:05 -0500 Subject: [PATCH 04/14] Format code. --- src/layouts/confirmation/confirmation.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index 8b5a0ce32..2724dbcb6 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -401,7 +401,7 @@ export default { } img { - margin-right: .5rem; + margin-right: 0.5rem; } } From d60cb9c45c640f112e169a36593a216fca71a2f6 Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Thu, 18 Jan 2024 14:05:47 -0500 Subject: [PATCH 05/14] CSR-1925 set background color of loading modal to white rather than gray. --- src/fmg-components/loading-modal/loading-modal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fmg-components/loading-modal/loading-modal.vue b/src/fmg-components/loading-modal/loading-modal.vue index 820ff6f7b..53bb2b2e0 100644 --- a/src/fmg-components/loading-modal/loading-modal.vue +++ b/src/fmg-components/loading-modal/loading-modal.vue @@ -129,7 +129,7 @@ export default { overflow-x: hidden; overflow-y: auto; outline: 0; - background: $gray-100; + background: $white; &.full-screen { top: 0; From 2a511ef77a4d8118c7563790249215ed8396ecbc Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Mon, 22 Jan 2024 13:14:14 -0500 Subject: [PATCH 06/14] Promo unit tests - store methods --- jest.config.js | 2 +- src/store/store.spec.js | 696 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 697 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 5df16a837..878990186 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,7 +29,7 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 76, + statements: 78, }, }, // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 81ef8f052..17dc27ec2 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -2766,6 +2766,702 @@ describe("Actions", () => { testCapabilityQuestionAnswerDependenciesHaveBeenReset(context, true); }); }); + describe("saveActiveAndOrInactivePromos", () => { + it("should save empty arrays to promos and inactivePromos when provided with empty or null parameters", () => { + // Arrange + const context = state; + context.commit = jest.fn(); + + const emptyActivePromos = []; + const emptyInactivePromos = []; + const nullActivePromos = []; + const nullInactivePromos = []; + + // Act + actions.saveActiveAndOrInactivePromos(context, { + activePromos: emptyActivePromos, + inactivePromos: emptyInactivePromos, + }); + actions.saveActiveAndOrInactivePromos(context, { + activePromos: nullActivePromos, + inactivePromos: nullInactivePromos, + }); + // Assert + expect(context.commit).toHaveBeenNthCalledWith(1, storeMutations.UPDATE_PROMOS, []); + expect(context.commit).toHaveBeenNthCalledWith( + 2, + storeMutations.UPDATE_INACTIVE_PROMOS, + [] + ); + expect(context.commit).toHaveBeenNthCalledWith(3, storeMutations.UPDATE_PROMOS, []); + expect(context.commit).toHaveBeenNthCalledWith( + 4, + storeMutations.UPDATE_INACTIVE_PROMOS, + [] + ); + }); + it("should save both active and inactivePromos when method is supplied with both active and inactive", () => { + // Arrange + const context = state; + context.commit = jest.fn(); + + const activePromos = [{ promoCode: "activePromo" }]; + const inactivePromos = ["inactivePromo"]; + + // Act + actions.saveActiveAndOrInactivePromos(context, { + activePromos: activePromos, + inactivePromos: inactivePromos, + }); + + // Assert + expect(context.commit).toBeCalledWith(storeMutations.UPDATE_PROMOS, activePromos); + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_INACTIVE_PROMOS, + inactivePromos + ); + }); + it("should remove any inactivePromos that are already active when supplied with both active and inactive", () => { + // Arrange + const context = state; + context.commit = jest.fn(); + + const activePromos = [{ promoCode: "duplicatePromo" }]; + const inactivePromos = ["duplicatePromo", "uniquePromo"]; + + const expectedInactivePromos = ["uniquePromo"]; + + // Act + actions.saveActiveAndOrInactivePromos(context, { + activePromos: activePromos, + inactivePromos: inactivePromos, + }); + + // Assert + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_INACTIVE_PROMOS, + expectedInactivePromos + ); + }); + it("should save active promos from store and inactive promos from method call when only inactivePromos is provided", () => { + // Arrange + const context = state; + context.commit = jest.fn(); + + const activePromos = [{ promoCode: "storedPromo" }]; + const inactivePromos = ["inactivePromo"]; + context["getters"] = { lineItems: { promos: activePromos } }; + + // Act + actions.saveActiveAndOrInactivePromos(context, { inactivePromos: inactivePromos }); + + // Assert + expect(context.commit).toBeCalledWith(storeMutations.UPDATE_PROMOS, activePromos); + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_INACTIVE_PROMOS, + inactivePromos + ); + }); + it("should save active promos but remove duplicates from store and inactive promos from method call when only inactivePromos is provided", () => { + // If only inactivePromos is supplied, it will remove duplicates from active promos in the store + // Arrange + const context = state; + context.commit = jest.fn(); + + const activePromos = [{ promoCode: "storedPromo" }, { promoCode: "duplicatePromo" }]; + const inactivePromos = ["duplicatePromo"]; + context["getters"] = { lineItems: { promos: activePromos } }; + + const expectedSavedActivePromos = [{ promoCode: "storedPromo" }]; + + // Act + actions.saveActiveAndOrInactivePromos(context, { inactivePromos: inactivePromos }); + + // Assert + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_PROMOS, + expectedSavedActivePromos + ); + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_INACTIVE_PROMOS, + inactivePromos + ); + }); + it("should save inactivePromos from the store and provided activePromos when only activePromos is provided", () => { + // Arrange + const context = state; + context.commit = jest.fn(); + + const activePromos = [{ promoCode: "activePromo" }]; + const inactivePromos = ["inactivePromo"]; + context["getters"] = { payment: { inactivePromos: inactivePromos } }; + + // Act + actions.saveActiveAndOrInactivePromos(context, { activePromos: activePromos }); + + // Assert + expect(context.commit).toBeCalledWith(storeMutations.UPDATE_PROMOS, activePromos); + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_INACTIVE_PROMOS, + inactivePromos + ); + }); + it("should save inactivePromos without duplicates from activePromos from the store and provided activePromos when only activePromos is provided", () => { + // Arrange + const context = state; + context.commit = jest.fn(); + + const activePromos = [{ promoCode: "activePromo" }, { promoCode: "duplicatePromo" }]; + const inactivePromos = ["inactivePromo", "duplicatePromo"]; + context["getters"] = { payment: { inactivePromos: inactivePromos } }; + + const expectedInactivePromos = ["inactivePromo"]; + + // Act + actions.saveActiveAndOrInactivePromos(context, { activePromos: activePromos }); + + // Assert + expect(context.commit).toBeCalledWith(storeMutations.UPDATE_PROMOS, activePromos); + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_INACTIVE_PROMOS, + expectedInactivePromos + ); + }); + }); + describe("validateOrderPromoAndSaveServerData", () => { + it("should add GUIDs if not there to provided addableVaps before sending the http call", async () => { + // Arrange + const context = state; + const promoCode = "testPromo"; + const lineItemsToUse = { vaps: [1], promos: [2] }; + const addableVaps = [{ partNumber: "addableVap" }]; + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + }, + referralSequenceNumber: "test", + lineItems: { + serverData: "test", + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: {}, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + actions.validateOrderPromoAndSaveServerData(context, { + payload: { + promoCode: promoCode, + lineItemsToUse: lineItemsToUse, + addableVaps: addableVaps, + }, + pageNameToLog: "test", + }); + + const firstCallArgs = globalMethods.callHttpClient.mock.calls[0]; + + // Assert + expect(firstCallArgs[0].payload.addableVaps[0].id).toEqual("GUID"); + }); + it("should use lineItems from the store if not provided in the call", async () => { + // Arrange + const context = state; + const promoCode = "testPromo"; + const lineItemsToUse = null; + const addableVaps = [{ partNumber: "addableVap" }]; + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + }, + referralSequenceNumber: "test", + lineItems: { + vaps: [1], + promos: [2], + serverData: "test", + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: {}, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + const expectedLineItemsOnOrder = [1, 2]; + + // Act + actions.validateOrderPromoAndSaveServerData(context, { + payload: { + promoCode: promoCode, + lineItemsToUse: lineItemsToUse, + addableVaps: addableVaps, + }, + pageNameToLog: "test", + }); + const firstCallArgs = globalMethods.callHttpClient.mock.calls[0]; + + // Assert + expect(firstCallArgs[0].payload.order.lineItemsOnOrder).toEqual( + expectedLineItemsOnOrder + ); + }); + it("should not blow up if an error comes back from the http call", async () => { + // Arrange + const context = state; + const promoCode = "testPromo"; + const lineItemsToUse = null; + const addableVaps = [{ partNumber: "addableVap" }]; + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + }, + referralSequenceNumber: "test", + lineItems: { + vaps: [1], + promos: [2], + serverData: "test", + }, + }, + }; + + globalMethods.callHttpClient = jest.fn(() => + Promise.reject(new Error("Error message")) + ); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + try { + await actions.validateOrderPromoAndSaveServerData(context, { + payload: { + promoCode: promoCode, + lineItemsToUse: lineItemsToUse, + addableVaps: addableVaps, + }, + pageNameToLog: "test", + }); + } catch (error) { + // Assert + fail("Unhandled error occurred"); + } + }); + it("should always save serverData if any is received from the http call", async () => { + // Arrange + const context = state; + const promoCode = "testPromo"; + const lineItemsToUse = null; + const addableVaps = [{ partNumber: "addableVap" }]; + context.commit = jest.fn(() => {}); + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + }, + referralSequenceNumber: "test", + lineItems: { + vaps: [1], + promos: [2], + serverData: "test", + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: { serverData: "serverData" }, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + await actions.validateOrderPromoAndSaveServerData(context, { + payload: { + promoCode: promoCode, + lineItemsToUse: lineItemsToUse, + addableVaps: addableVaps, + }, + pageNameToLog: "test", + }); + + // Assert + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, + expect.anything() + ); + }); + }); + describe("revalidateOrderPromosAndSaveServerData", () => { + it("should add GUIDs if not there to provided vaps before sending the http call", async () => { + // Arrange + const context = state; + context.commit = jest.fn(() => {}); + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + inactivePromos: ["inactiveTest"], + }, + referralSequenceNumber: "test", + lineItems: { + serverData: "test", + promos: [{ promoCode: "test" }], + vaps: [{ partNumber: "testVap" }], + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: { lineItemsServerData: "testData" }, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + actions.revalidateOrderPromosAndSaveServerData(context, { + payload: {}, + pageNameToLog: "test", + }); + + const firstCallArgs = globalMethods.callHttpClient.mock.calls[0]; + + // Assert + const vapsLineItem = firstCallArgs[0].payload.order.lineItemsOnOrder.filter( + (lineItem) => { + return lineItem.partNumber == "testVap"; + } + ); + expect(vapsLineItem[0].id).toEqual("GUID"); + }); + it("uses provided parameters in favor of store values", async () => { + // Arrange + const context = state; + context.commit = jest.fn(() => {}); + + const activePromosToUse = [{ promoCode: "providedPromo" }]; + const inactivePromosToUse = ["providedInactivePromo"]; + const vapsProvided = [{ id: 123 }]; + const lineItemsToUse = { vaps: vapsProvided }; + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + inactivePromos: ["inactiveTest"], + }, + referralSequenceNumber: "test", + lineItems: { + serverData: "test", + promos: [{ promoCode: "test" }], + vaps: [{ partNumber: "testVap" }], + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: { lineItemsServerData: "testData" }, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + actions.revalidateOrderPromosAndSaveServerData(context, { + payload: { + activePromosToUse: activePromosToUse, + inactivePromosToUse: inactivePromosToUse, + lineItemsToUse: lineItemsToUse, + }, + pageNameToLog: "test", + }); + + const firstCallArgs = globalMethods.callHttpClient.mock.calls[0]; + const expectedInactivePromos = ["providedInactivePromo"]; + const expectedLineItemsOnOrder = [...vapsProvided, ...activePromosToUse]; + + // Assert + expect(firstCallArgs[0].payload.inactivePromos).toEqual(expectedInactivePromos); + expect(firstCallArgs[0].payload.order.lineItemsOnOrder).toEqual( + expectedLineItemsOnOrder + ); + }); + it("can build a valid payload with store data only", async () => { + // Arrange + const context = state; + context.commit = jest.fn(() => {}); + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + inactivePromos: ["inactiveTest"], + }, + referralSequenceNumber: "test", + lineItems: { + serverData: "test", + promos: [{ promoCode: "test" }], + vaps: [{ partNumber: "testVap", id: "providedId" }], + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: { lineItemsServerData: "testData" }, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + actions.revalidateOrderPromosAndSaveServerData(context, { + payload: {}, + pageNameToLog: "test", + }); + + const expectedLineItemsOnOrder = [ + ...context.getters.order.lineItems.vaps, + ...context.getters.order.lineItems.promos, + ]; + const firstCallArgs = globalMethods.callHttpClient.mock.calls[0]; + // Assert + expect(firstCallArgs[0].payload.inactivePromos).toEqual( + context.getters.order.payment.inactivePromos + ); + expect(firstCallArgs[0].payload.order.lineItemsOnOrder).toEqual( + expectedLineItemsOnOrder + ); + }); + it("removes any promos from inactivePromos that are already active before sending the request", async () => { + // Arrange + const context = state; + context.commit = jest.fn(() => {}); + + const activePromosToUse = [{ promoCode: "providedPromoDuplicate" }]; + const inactivePromosToUse = ["providedPromoDuplicate"]; + const vapsProvided = [{ id: 123 }]; + const lineItemsToUse = { vaps: vapsProvided }; + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + }, + referralSequenceNumber: "test", + lineItems: { + serverData: "test", + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: { lineItemsServerData: "testData" }, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + actions.revalidateOrderPromosAndSaveServerData(context, { + payload: { + activePromosToUse: activePromosToUse, + inactivePromosToUse: inactivePromosToUse, + lineItemsToUse: lineItemsToUse, + }, + pageNameToLog: "test", + }); + + const firstCallArgs = globalMethods.callHttpClient.mock.calls[0]; + // Assert + expect(firstCallArgs[0].payload.inactivePromos).toEqual([]); + }); + it("saves serverData to the store", async () => { + // Arrange + const context = state; + context.commit = jest.fn(() => {}); + + const activePromosToUse = [{ promoCode: "providedPromoDuplicate" }]; + const inactivePromosToUse = ["providedPromoDuplicate"]; + const vapsProvided = [{ id: 123 }]; + const lineItemsToUse = { vaps: vapsProvided }; + + context["getters"] = { + order: { + serviceLocation: { + appointmentType: "test", + state: "test", + zipCodeCtu: "test", + }, + vehicle: { + carId: "test", + year: "test", + }, + referralCorrelationId: "test", + eon: "test", + damage: { + isRepair: true, + glassToReplace: null, + }, + payment: { + parentAccountNumber: "test", + }, + referralSequenceNumber: "test", + lineItems: { + serverData: "test", + }, + }, + }; + + globalMethods.callHttpClient = jest.fn().mockResolvedValue({ + data: { lineItemsServerData: "testData" }, + }); + + crypto.randomUUID = jest.fn(() => "GUID"); + + // Act + await actions.revalidateOrderPromosAndSaveServerData(context, { + payload: { + activePromosToUse: activePromosToUse, + inactivePromosToUse: inactivePromosToUse, + lineItemsToUse: lineItemsToUse, + }, + pageNameToLog: "test", + }); + + // Assert + expect(context.commit).toBeCalledWith( + storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, + expect.anything() + ); + }); + }); }); describe("Getters", () => { From 8cfa7499ab87869d79a2c9a587a0578336dd06dd Mon Sep 17 00:00:00 2001 From: Bryan Mauger Date: Mon, 22 Jan 2024 13:25:41 -0500 Subject: [PATCH 07/14] CSR-1925 set background color of modal overlay from black to white. --- src/fmg-components/loading-modal/loading-modal.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fmg-components/loading-modal/loading-modal.vue b/src/fmg-components/loading-modal/loading-modal.vue index 53bb2b2e0..2abcfdc96 100644 --- a/src/fmg-components/loading-modal/loading-modal.vue +++ b/src/fmg-components/loading-modal/loading-modal.vue @@ -118,8 +118,7 @@ export default { left: 0; height: 100%; width: 100%; - background-color: $black; - opacity: 0.4; + background-color: $white; z-index: 1056; } From 8ce1dd7d000e2a008502b4dbf7351eaf78df5dcd Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Tue, 23 Jan 2024 14:17:41 -0500 Subject: [PATCH 08/14] Capture workOrderId from save response and add to vuex --- src/constants/store-mutations.js | 1 + src/helpers/heritage-integration/order-helper.js | 1 + src/store/index.js | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 3e779e027..5b0b66509 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -53,6 +53,7 @@ const storeMutations = { UPDATE_IS_PIA: "updateIsPia", UPDATE_PIA_TYPE: "updatePiaType", WORK_ORDER_NUMBER: "updateWorkOrderNumber", + WORK_ORDER_ID: "updateWorkOrderId", Customer_Portal_Login_Token: "updateCustomerPortalLoginToken", LOCK_TOKEN: "updateLockToken", UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index e0873d822..38f4161b9 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -162,6 +162,7 @@ async function saveSessionHelper( crmCustomerId: savedSessionInfo.data.crmCustomerId?.toString(), eon: savedSessionInfo.data.eon, workOrderNumber: savedSessionInfo.data.workOrderNumber, + workOrderId: savedSessionInfo.data.workOrderId, customerPortalLoginToken: savedSessionInfo.data.customerPortalLoginToken, lockToken: savedSessionInfo.data.lockToken, settledTenderAmount: savedSessionInfo.data.settledTenderAmount, diff --git a/src/store/index.js b/src/store/index.js index 12140f66b..c78a7968d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -132,6 +132,7 @@ const getDefaultState = () => { referralCorrelationId: null, eon: null, workOrderNumber: null, + workOrderId: null, customerPortalLoginToken: null, lockToken: null, settledTenderAmount: 0, @@ -254,6 +255,9 @@ export const mutations = { updateWorkOrderNumber(state, workOrderNumber) { state.order.workOrderNumber = workOrderNumber; }, + updateWorkOrderId(state, workOrderId) { + state.order.workOrderId = workOrderId; + }, updateCustomerPortalLoginToken(state, customerPortalLoginToken) { state.order.customerPortalLoginToken = customerPortalLoginToken; }, @@ -1027,6 +1031,7 @@ export const actions = { savedSessionId, crmCustomerId, workOrderNumber, + workOrderId, customerPortalLoginToken, lockToken, settledTenderAmount, @@ -1041,6 +1046,7 @@ export const actions = { context.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId); context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId); context.commit(storeMutations.WORK_ORDER_NUMBER, workOrderNumber); + context.commit(storeMutations.WORK_ORDER_ID, workOrderId); context.commit(storeMutations.LOCK_TOKEN, lockToken); context.commit(storeMutations.Customer_Portal_Login_Token, customerPortalLoginToken); context.commit(storeMutations.UPDATE_SETTLED_TENDER_AMOUNT, settledTenderAmount); From fe1376438e1215153fd17f764f70a434d2963229 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 24 Jan 2024 10:24:40 -0500 Subject: [PATCH 09/14] Collate & Push info to data layer on confirmation enter --- src/layouts/confirmation/confirmation.vue | 2 + src/mixins/analytics-mixin.js | 76 +++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/layouts/confirmation/confirmation.vue b/src/layouts/confirmation/confirmation.vue index 2724dbcb6..92a86e609 100644 --- a/src/layouts/confirmation/confirmation.vue +++ b/src/layouts/confirmation/confirmation.vue @@ -75,6 +75,7 @@ import cart from "@/fmg-components/cart/cart"; //Supporting files import baseMixin from "@/mixins/base-mixin.js"; +import analyticsMixin from "@/mixins/analytics-mixin"; import store from "@/store"; import { storeActions } from "@/constants/store-actions"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; @@ -134,6 +135,7 @@ export default { vm.setCmsContent(resultMap.cmsContent); vm.lineItems = lineItemsFromSubmittedOrder; vm.vaps = availableVaps; + analyticsMixin.methods.pushSubmittedOrderToDataLayer(); }); }, data() { diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 3f20adb59..36b5f619f 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -21,6 +21,7 @@ import { GaEvents, ValueToLogTypes, } from "@/constants/analytics"; +import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import store from "@/store"; import baseMixin from "@/mixins/base-mixin"; @@ -119,6 +120,81 @@ export default { await this.logPageView(analyticsPageEvents.ENTRY); }, + pushSubmittedOrderToDataLayer() { + // check if submitted order exists; exit if not. + const hasSubmittedOrder = store.getters.hasSubmittedOrder; + + if (!hasSubmittedOrder) { + return; + } + + const order = store.getters.submittedOrder; + + // assemble data for payload + // // reduce promocode array + const promos = order.lineItems.promos ?? []; + const promoString = + promos.length === 0 ? "" : promos.reduce((prev, next) => `${prev},${next}`); + + // // reduce glass array + const glassToReplace = order.damage.glassToReplace ?? []; + const glassToReplaceNames = glassToReplace.map( + (glassPiece) => `${glassPiece.glassLocation}/${glassPiece.glassName}` + ); + const glassString = + glassToReplaceNames.length === 0 + ? "" + : glassToReplaceNames.reduce((prev, next) => `${prev},${next}`); + + // // calculate subtotal + const lineItems = order.lineItems; + const combinedLineItems = [ + ...(lineItems.glassParts ?? []), + ...(lineItems.supportingItems ?? []), + ...(lineItems.vaps ?? []), + ...(lineItems.promos ?? []), + ]; + const subtotal = baseMixin.methods.getTotalPriceOfAllLineItemsAndChildParts( + combinedLineItems, + false + ); + + // // get correct zip code + const providerZip = order.serviceLocation.provider.address.zipCode; + const serviceZip = + order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE + ? order.serviceLocation.zipCode + : providerZip; + + // // calculate total + const total = baseMixin.methods.getTotalPriceOfAllLineItemsAndChildParts( + combinedLineItems, + true + ); + + const payload = { + serviceZipCode: serviceZip, + damage: order.damage.isRepair ? "repair" : "replace", + accountType: order.payment.isInsurance ? "insurance" : "cash", + promoCodes: promoString, + vehicleYear: order.vehicle.year, + vehicleMake: order.vehicle.make, + vehicleModel: order.vehicle.model, + vehicleStyle: order.vehicle.style, + glassToReplace: glassString, + workOrderId: order.workOrderId, + providerCtu: order.serviceLocation.zipCodeCtu, + orderNumber: order.workOrderNumber, + priceTotal: total, + priceSubTotal: subtotal, + isRecalibrationOnOrder: store.getters.isRecalibrationOnSubmittedOrder, + appointmentType: order.serviceLocation.appointmentType, + }; + + // push to data layer. + pushToDataLayerIfDefined(payload); + }, + pushExperimentsToDataLayer() { const experiments = store.getters.applicationUser.experiments; experiments?.forEach((exp) => { From 3827fd238c68de42b4329bfa9653af3ce6edf95f Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 24 Jan 2024 10:31:38 -0500 Subject: [PATCH 10/14] fix key --- src/mixins/analytics-mixin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 36b5f619f..93af6e1d4 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -174,7 +174,7 @@ export default { const payload = { serviceZipCode: serviceZip, - damage: order.damage.isRepair ? "repair" : "replace", + damageType: order.damage.isRepair ? "repair" : "replace", accountType: order.payment.isInsurance ? "insurance" : "cash", promoCodes: promoString, vehicleYear: order.vehicle.year, From 5aa51fa8cacc3807bc35173b9a654f87c38f6627 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 24 Jan 2024 14:16:42 -0500 Subject: [PATCH 11/14] Properly extract promo string --- src/mixins/analytics-mixin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index 93af6e1d4..e0d865c35 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -133,8 +133,9 @@ export default { // assemble data for payload // // reduce promocode array const promos = order.lineItems.promos ?? []; + const promoCodes = promos.map((promo) => promo.promoCode); const promoString = - promos.length === 0 ? "" : promos.reduce((prev, next) => `${prev},${next}`); + promoCodes.length === 0 ? "" : promoCodes.reduce((prev, next) => `${prev},${next}`); // // reduce glass array const glassToReplace = order.damage.glassToReplace ?? []; From 3893e3ae2d4127b6e7396809b62817cfc5de556f Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Wed, 24 Jan 2024 14:16:57 -0500 Subject: [PATCH 12/14] Test coverage --- src/mixins/analytics-mixin.spec.js | 164 +++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/src/mixins/analytics-mixin.spec.js b/src/mixins/analytics-mixin.spec.js index 567d9154e..ff6c30e7e 100644 --- a/src/mixins/analytics-mixin.spec.js +++ b/src/mixins/analytics-mixin.spec.js @@ -22,6 +22,46 @@ import { getUserIdValue, } from "@/helpers/heritage-integration/cookie-helper"; +const parts = { + windshield: { + name: "windshield", + canSafeliteRecalibrate: true, + childParts: [], + color: "Green Tint", + description: + "solar, soundproofing, lane keep assist, lane departure warning system, w/adaptive cruise control", + id: "db22fd44-10dd-456f-979b-ff88cf68cca6", + partNumber: "FW04896GTYN", + partType: "WINDSHIELD", + recalibrationType: "STATIC", + requiresCapabilityQuestions: false, + requiresRecalibration: true, + salesTax: 63.86, + sellingPrice: 791.46, + }, + frontWipers: { + name: "front wipers", + partNumber: "SBB16", + description: "SAFELITE BEAM BLADE 16", + partType: "FRONT WIPER", + price: 32.64, + }, + rearWipers: { + name: "rear wipers", + partNumber: "SBBR12A", + description: "SAFELITE REAR BLADE 12A", + partType: "REAR WIPER", + price: 24.48, + }, + rainDefense: { + name: "rain defense", + partNumber: "RAIN DEFENSE", + description: null, + partType: "RAIN DEFENSE", + price: 35.5, + }, +}; + describe("analyticsMixin.js", () => { beforeEach(() => { removeAllTestCookies(); @@ -284,6 +324,130 @@ describe("analyticsMixin.js", () => { ]); }); + describe("pushSubmittedOrderToDataLayer", () => { + beforeEach(() => { + store.getters.hasSubmittedOrder = true; + store.getters.submittedOrder = { + vehicle: { + year: "2020", + make: "acura", + model: "mdx", + style: "4-door sedan", + carId: "dummyCarId", + category: "dummyCategory", + vin: "dummyVin", + }, + serviceLocation: { + address: "add1", + address2: "add2", + city: "city", + state: "state", + zipCode: "zip", + zipCodeCtu: "zipCtu", + appointmentType: "IN_SHOP", + isVehicleProtected: true, + provider: { + providerNumber: 2, + address: { + streetAddress: "add3", + city: "city2", + state: "state2", + zipCode: "zip2", + zipCodeCtu: "zipCtu2", + }, + }, + techNotes: "", + }, + customer: { + firstName: "first", + lastName: "last", + emailAddress: "builddigitaltest@safelite.com", + phoneNumber: "555-555-5555", + isSmsOptIn: false, + }, + damage: { + isRepair: false, + numberOfChips: null, + glassToReplace: [{ glassName: "single", location: "windshield" }], + }, + lineItems: { + glassParts: [parts.windshield], + supportingItems: [], + vaps: [parts.frontWipers], + promos: [{ promoCode: "promoTEST" }, { promoCode: "promoTEST2" }], + }, + payment: { + isInsurance: false, + insuranceCoverage: { + isVerified: null, + coverageStatus: null, + coverageVerificationType: null, + }, + isPia: true, + piaType: "Afterpay", + inactivePromos: [], + }, + schedule: { + date: "date", + startTime: "start", + endTime: "end", + jobMinMinutes: "30", + jobMaxMinutes: "45", + }, + workOrderNumber: "01820-111111", + workOrderId: "222222222222", + }; + }); + + test("Pushes to data layer if nominal", () => { + // Arrange + const mockDataLayerFn = jest.fn(); + + window.dataLayer = { + push: mockDataLayerFn, + }; + + // Act + analyticsMixin.methods.pushSubmittedOrderToDataLayer(); + + // Assert + expect(mockDataLayerFn).toHaveBeenCalled(); + }); + + test("Does not push to data layer if no submitted order available.", () => { + // Arrange + store.getters.hasSubmittedOrder = false; + store.getters.submittedOrder = undefined; + + const mockDataLayerFn = jest.fn(); + + window.dataLayer = { + push: mockDataLayerFn, + }; + + // Act + analyticsMixin.methods.pushSubmittedOrderToDataLayer(); + + // Assert + expect(mockDataLayerFn).not.toHaveBeenCalled(); + }); + + test("Glass and Promo strings correctly formatted", () => { + // Arrange + window.dataLayer = []; + + // Act + analyticsMixin.methods.pushSubmittedOrderToDataLayer(); + + // Assert + const glassString = window.dataLayer[0].glassToReplace; + const promoString = window.dataLayer[0].promoCodes; + + expect(glassString).toMatch(/(\w+\/\w+)?(,\w+\/\w+)*/); + expect(promoString).toMatch(/(\w+)?(,\w+)*/); + }); + }); + test("Obj is not null after action prepended", () => { //Arrange const obj = { baseMethodName: "testMethodName", data: "testData" }; From 2d70f9ae812829031e8f9e1996aac997a4802a42 Mon Sep 17 00:00:00 2001 From: CarlNation <32103961+CarlNation@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:16:05 -0500 Subject: [PATCH 13/14] CSR-1921 correct pay later after ccd payment failure CSR-1921 correct pay later after ccd payment failure --- src/constants/application-config.js | 1 + src/layouts/payment/payment.vue | 33 +++++++++++++++++++++++++++++ src/ux-components/alert/alert.vue | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/constants/application-config.js b/src/constants/application-config.js index 1e2324ba7..244882509 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -22,6 +22,7 @@ const applicationConfig = { "//" + location.host + "/fmg/?fmgPage=payment-method&src=concept-funnel", + CONFIRMATION_URL: location.protocol + "//" + location.host + "/fmg/?fmgPage=confirmation", GOOGLE_CALENDAR: "https://www.google.com/calendar/render?action=TEMPLATE", YAHOO_CALENDAR: "https://calendar.yahoo.com/?v=60", OUTLOOK_CALENDAR: diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 73050bf61..201578435 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -17,6 +17,7 @@ v-if="shouldDisplayPiaCCAlert" cmsWidgetName="PIACCErrorAlertWidget" alertClass="alert-danger" + @textLinkClicked="paymentFailedPayLater" v-bind:isDismissible="false" /> @@ -27,6 +28,7 @@ v-if="shouldDisplayPiaAPAlert" cmsWidgetName="PIAAPErrorAlertWidget" alertClass="alert-danger" + @textLinkClicked="paymentFailedPayLater" v-bind:isDismissible="false" /> @@ -216,6 +218,7 @@ import { import { queryStrings } from "@/constants/query-strings"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { deepClone } from "@/helpers/object-helper"; +import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.js"; import { routerParams } from "@/router/router-constants/router-params"; @@ -573,6 +576,36 @@ export default { getDisplayAmountDue() { return baseMixin.methods.getDisplayAmountDue(store.getters.order.lineItems); }, + async paymentFailedPayLater() { + this.$refs.loadingModal.isModalVisible = true; + + await this.dispatchStoreAction( + storeActions.SAVE_PAYMENT_METHOD_CHOICE, + paymentMethods.LATER, + false + ); + + await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); + try { + await submitWorkOrder({ + pageNameToLog: "payment", + submitAfterSave: true, + }); + + await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_ORDER); + window.location = applicationConfig.CONFIRMATION_URL; + } catch (error) { + console.log("error: response from submit work order(payment pg):" + error.message); + this.$router.navigateWithoutSaving( + this.navigationScenarios.PIA_ERROR, + this.$route, + {}, + { [routerParams.DISPLAY_PIA_ALERT]: true } + ); + this.$refs.loadingModal.isModalVisible = false; + return; + } + }, backButtonAction() { // The only way I could figure out how to get navigation to work on a page with the iframe. // This is also how a cancel from Paypal would work. Just a redirect to the payment-method page. diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue index 7274ae487..637ba3e1d 100644 --- a/src/ux-components/alert/alert.vue +++ b/src/ux-components/alert/alert.vue @@ -23,7 +23,7 @@ Date: Fri, 26 Jan 2024 09:25:43 -0500 Subject: [PATCH 14/14] prettier --- src/layouts/payment/payment.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/payment/payment.vue b/src/layouts/payment/payment.vue index 201578435..365dccf01 100644 --- a/src/layouts/payment/payment.vue +++ b/src/layouts/payment/payment.vue @@ -578,7 +578,7 @@ export default { }, async paymentFailedPayLater() { this.$refs.loadingModal.isModalVisible = true; - + await this.dispatchStoreAction( storeActions.SAVE_PAYMENT_METHOD_CHOICE, paymentMethods.LATER,