From f0534f27189e251014f37a0be0380a0bf202abb3 Mon Sep 17 00:00:00 2001 From: Scott Kiener Date: Wed, 17 Jan 2024 09:55:03 -0500 Subject: [PATCH] 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 + ); + }); + }); + }); +});