Merge pull request #1402 from Safelite/feature/skiener/CSR-1520
CSR-1520 | First implementation of Promo Logic
This commit is contained in:
commit
75816f54d1
11 changed files with 414 additions and 54 deletions
|
|
@ -27,7 +27,7 @@ module.exports = {
|
|||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 77,
|
||||
statements: 75,
|
||||
},
|
||||
},
|
||||
// Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit
|
||||
|
|
|
|||
|
|
@ -138,6 +138,14 @@ const endpoints = {
|
|||
url: "/price/api/v1/price/order-items",
|
||||
method: "GET",
|
||||
},
|
||||
ValidatePromo: {
|
||||
url: "/price/api/v1/price/order-promo",
|
||||
method: "POST",
|
||||
},
|
||||
RevalidatePromos: {
|
||||
url: "/price/api/v1/price/revalidated-order-promos",
|
||||
method: "POST",
|
||||
},
|
||||
TaxOrderItems: {
|
||||
url: "/price/api/v1/price/taxed-order-items",
|
||||
method: "GET",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ const storeActions = {
|
|||
CLEAR_VIN: "clearVin",
|
||||
RESET_SAVE_SESSION_PROMISE: "resetSaveSessionPromise",
|
||||
GET_SIGNATURE: "getSignature",
|
||||
VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA: "validateOrderPromoAndSaveServerData",
|
||||
REVALIDATE_ORDER_PROMOS_AND_UPDATE_STORE: "revalidateOrderPromosAndUpdateStore",
|
||||
|
||||
// DEPENDENCY MUTATIONS
|
||||
RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies",
|
||||
|
|
@ -84,6 +86,7 @@ const storeActions = {
|
|||
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
|
||||
"saveSupportingItemsSuppressingStateResetting",
|
||||
SAVE_VAPS: "saveVaps",
|
||||
SAVE_PROMOS: "savePromos",
|
||||
SAVE_CUSTOMER_DETAILS: "saveCustomerDetails",
|
||||
SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes",
|
||||
SAVE_WORK_ORDER_FLAG: "saveWorkOrderFlag",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const storeMutations = {
|
|||
UPDATE_VAPS: "updateVaps",
|
||||
UPDATE_SUPPORTING_ITEMS: "updateSupportingItems",
|
||||
UPDATE_LINE_ITEMS_SERVER_DATA: "updateLineItemsServerData",
|
||||
UPDATE_INACTIVE_PROMOS: "updateInactivePromos",
|
||||
UPDATE_PROMOS: "updatePromos",
|
||||
|
||||
UPDATE_REGISTRATION: "updateRegistration",
|
||||
|
||||
|
|
|
|||
101
src/helpers/promotions-helper.js
Normal file
101
src/helpers/promotions-helper.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import store from "@/store";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
export const promoPartNumberStrings = {
|
||||
WIPER_DISCOUNT_PART_NUMBER: "WIPER DISCOUNT",
|
||||
RAIN_DEFENSE_DISCOUNT_PART_NUMBER: "DISC RAIN DEFEN",
|
||||
GLASS_DISCOUNT_PART_NUMBER: "DISCOUNT",
|
||||
GLASS_CLEANER_DISCOUNT_PART_NUMBER: "DISC GLASS CLN",
|
||||
};
|
||||
|
||||
export const pagesToStripPromoQueryStringFrom = ["quote", "payment-method"];
|
||||
|
||||
export function getAddableVapsFromAvailableLineItems(availableLineItems) {
|
||||
const addableVaps = [];
|
||||
addableVaps.push(...findLineItemsWithPartType(partTypeStrings.FRONT_WIPER, availableLineItems));
|
||||
addableVaps.push(
|
||||
...findLineItemsWithPartType(partTypeStrings.RAIN_DEFENSE, availableLineItems)
|
||||
);
|
||||
return addableVaps;
|
||||
}
|
||||
|
||||
export function removeVapsPromosFromPromoArray(promoArray) {
|
||||
if (!promoArray) {
|
||||
return [];
|
||||
}
|
||||
const filteredPromoArray = promoArray.filter((promo) => {
|
||||
return (
|
||||
promo.partNumber != promoPartNumberStrings.WIPER_DISCOUNT_PART_NUMBER &&
|
||||
promo.partNumber != promoPartNumberStrings.RAIN_DEFENSE_DISCOUNT_PART_NUMBER &&
|
||||
promo.partNumber != promoPartNumberStrings.GLASS_CLEANER_DISCOUNT_PART_NUMBER
|
||||
);
|
||||
});
|
||||
return filteredPromoArray;
|
||||
}
|
||||
|
||||
export async function revalidatePromosAndValidateNewPromo(
|
||||
newPromo,
|
||||
pricedLineItems,
|
||||
pageNameToLog
|
||||
) {
|
||||
const hasActivePromos = store.getters.order.lineItems.promos;
|
||||
const hasHasInactivePromos = store.getters.order.lineItems.inactivePromos;
|
||||
const hasNewPromo = !!newPromo;
|
||||
const addableVaps = getAddableVapsFromAvailableLineItems(pricedLineItems);
|
||||
|
||||
let validatePromoResponse = null;
|
||||
let revalidatePromoResponse = null;
|
||||
|
||||
if (hasActivePromos || hasHasInactivePromos) {
|
||||
revalidatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.REVALIDATE_ORDER_PROMOS_AND_UPDATE_STORE,
|
||||
null,
|
||||
pageNameToLog,
|
||||
false
|
||||
);
|
||||
}
|
||||
if (hasNewPromo) {
|
||||
validatePromoResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA,
|
||||
{
|
||||
promoCode: newPromo,
|
||||
addableVaps: addableVaps,
|
||||
},
|
||||
pageNameToLog,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return { validatePromoResponse, revalidatePromoResponse };
|
||||
}
|
||||
|
||||
export function getPromosThatMatchLineItemsOnOrder(promos, lineItemsOnOrder) {
|
||||
const matchingPromos = [];
|
||||
promos.forEach((promo) => {
|
||||
let allIdsMatch = true;
|
||||
promo.discountedLineItemIds.forEach((id) => {
|
||||
if (lineItemsOnOrder.filter((x) => x.id === id).length === 0) {
|
||||
allIdsMatch = false;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (allIdsMatch) {
|
||||
matchingPromos.push(promo);
|
||||
}
|
||||
});
|
||||
return matchingPromos;
|
||||
}
|
||||
|
||||
export function shouldStripPromoQueryString(fmgPageQueryValue) {
|
||||
return pagesToStripPromoQueryStringFrom.includes(fmgPageQueryValue);
|
||||
}
|
||||
|
||||
// private methods
|
||||
function findLineItemsWithPartType(typeToFind, itemsToSearch) {
|
||||
const partTypeMatches = itemsToSearch?.filter(
|
||||
(lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase()
|
||||
);
|
||||
return partTypeMatches;
|
||||
}
|
||||
|
|
@ -21,6 +21,12 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
|||
navigateToHeritageFunnel: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/promotions-helper", () => ({
|
||||
revalidatePromosAndValidateNewPromo: jest.fn(() => {
|
||||
return { validatePromoResponse: null, revalidatePromoResponse: null };
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
"@/store",
|
||||
() => {
|
||||
|
|
@ -43,11 +49,20 @@ jest.mock("@/mixins/base-mixin", () => ({
|
|||
getTierOnePackagePrice() {
|
||||
return mockTierOnePrice;
|
||||
},
|
||||
filterOutFees() {
|
||||
filterOutFees(items) {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
filterOutFees: jest.fn().mockImplementation(() => {
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
let mockTierOnePrice = 501;
|
||||
|
||||
const mockPriceOrderStoreAction = storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
|
||||
|
|
@ -93,6 +108,16 @@ afterEach(() => {
|
|||
describe("quote.vue", () => {
|
||||
test("IsInsurance false should navigateWithSaving", async () => {
|
||||
//Arrange
|
||||
store.getters.payment = {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: false,
|
||||
};
|
||||
store.getters.order = {
|
||||
lineItems: [],
|
||||
payment: {
|
||||
parentAccountNumber: 167132,
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({
|
||||
customMountOptions: {
|
||||
router: {
|
||||
|
|
@ -102,11 +127,6 @@ describe("quote.vue", () => {
|
|||
},
|
||||
});
|
||||
|
||||
store.getters.payment = {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: false,
|
||||
};
|
||||
|
||||
wrapper.vm.dispatchStoreAction = jest.fn(() => {
|
||||
return {
|
||||
data: [],
|
||||
|
|
@ -124,20 +144,28 @@ describe("quote.vue", () => {
|
|||
|
||||
test("IsInsurance true should navigateToHeritageFunnel", async () => {
|
||||
//Arrange
|
||||
store.getters = {
|
||||
payment: {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: true,
|
||||
},
|
||||
order: {
|
||||
lineItems: [],
|
||||
payment: {
|
||||
parentAccountNumber: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({
|
||||
customMountOptions: {
|
||||
router: {
|
||||
navigateWithSaving: jest.fn(),
|
||||
},
|
||||
route: { quote },
|
||||
mixins: [mockMixin],
|
||||
},
|
||||
});
|
||||
|
||||
store.getters.payment = {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: true,
|
||||
};
|
||||
|
||||
wrapper.vm.dispatchStoreAction = jest.fn(() => {
|
||||
return {
|
||||
data: [],
|
||||
|
|
@ -154,9 +182,9 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should pass arePagePrerequisitesValid with a repair order", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: [],
|
||||
serviceLocation: {
|
||||
zipCode: "12345",
|
||||
zipCodeCtu: "value",
|
||||
|
|
@ -172,6 +200,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
|
|
@ -179,7 +208,6 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should pass arePagePrerequisitesValid with a replace order", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
store.getters = {
|
||||
order: {
|
||||
serviceLocation: {
|
||||
|
|
@ -200,6 +228,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
|
|
@ -207,9 +236,9 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should fail arePagePrerequisitesValid without glass parts or flagged as repair", () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: [],
|
||||
serviceLocation: {
|
||||
zipCode: "12345",
|
||||
zipCodeCtu: "value",
|
||||
|
|
@ -220,6 +249,7 @@ describe("quote.vue", () => {
|
|||
referralNumber: "1234567",
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||
|
||||
|
|
@ -227,8 +257,6 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should have non-null values for necessary data members after 'beforeRouteEnter'", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: {
|
||||
|
|
@ -236,7 +264,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { wrapper } = setupMocks({});
|
||||
//mock this to avoid needing to populate this.$route in an unrelated test
|
||||
wrapper.vm.getDefaultIsInsuranceSelectedValue = jest.fn();
|
||||
|
||||
|
|
@ -257,8 +285,6 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should default to insurance if query param 'isInsurance' is true", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: {
|
||||
|
|
@ -272,6 +298,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.$route = { query: { isInsurance: "true" } };
|
||||
|
||||
//Act
|
||||
|
|
@ -287,8 +314,6 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should default to cash if query param 'isInsurance' is false", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: {
|
||||
|
|
@ -302,6 +327,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.$route = { query: { isInsurance: "false" } };
|
||||
|
||||
//Act
|
||||
|
|
@ -317,7 +343,6 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should default to insurance if insurance selection is saved to store", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
|
|
@ -332,6 +357,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
// Ensure that query param isn't overriding selection
|
||||
wrapper.vm.$route = { query: null };
|
||||
|
||||
|
|
@ -348,8 +374,6 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("should default to cash if cash selection is saved to store", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: {
|
||||
|
|
@ -363,6 +387,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
// Ensure that query param isn't overriding selection
|
||||
wrapper.vm.$route = { query: null };
|
||||
|
||||
|
|
@ -380,8 +405,6 @@ describe("quote.vue", () => {
|
|||
test("should default to cash if total economy package price is under $500", async () => {
|
||||
// Also needs no query parameter or previous selection in store to be present
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: {
|
||||
|
|
@ -396,6 +419,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
};
|
||||
mockTierOnePrice = 200;
|
||||
const { wrapper } = setupMocks({});
|
||||
// Ensure that query param isn't overriding selection
|
||||
wrapper.vm.$route = { query: null };
|
||||
|
||||
|
|
@ -413,7 +437,6 @@ describe("quote.vue", () => {
|
|||
test("should default to insurance if total economy package price is over $500", async () => {
|
||||
// Also needs no query parameter or previous selection in store to be present
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
|
|
@ -429,6 +452,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
};
|
||||
mockTierOnePrice = 505;
|
||||
const { wrapper } = setupMocks({});
|
||||
// Ensure that query param isn't overriding selection
|
||||
wrapper.vm.$route = { query: null };
|
||||
|
||||
|
|
@ -445,7 +469,7 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("Should default to insurence if user Service zip is from certain States", async () => {
|
||||
//Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
store.getters = {
|
||||
order: {
|
||||
lineItems: {
|
||||
|
|
@ -459,6 +483,7 @@ describe("quote.vue", () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.$route = { query: null };
|
||||
//Act
|
||||
await quote.beforeRouteEnter.call(
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
:availableLineItems="availableLineItems"
|
||||
:isInsuranceSelected="isInsuranceSelected"
|
||||
@vapsItemsSelected="vapsItemsSelectedAction"
|
||||
:activePromos="allActivePromos"
|
||||
v-on="{ 'buttonEvent.openModal': openModalAction }"
|
||||
validationRules="option-required"
|
||||
isRequired />
|
||||
|
|
@ -79,6 +80,10 @@ import { Form, defineRule } from "vee-validate";
|
|||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states";
|
||||
import { revalidatePromosAndValidateNewPromo } from "@/helpers/promotions-helper";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||
import { setTransitionHooks } from "vue";
|
||||
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
|
|
@ -122,14 +127,12 @@ export default {
|
|||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
const clonedGlassParts = store.getters.order.lineItems.glassParts
|
||||
? JSON.parse(JSON.stringify(store.getters.order.lineItems.glassParts))
|
||||
: [];
|
||||
const nullSafeGlassParts = store.getters.order.lineItems.glassParts ?? [];
|
||||
const availableLineItems = [
|
||||
resultMap.rainDefense,
|
||||
...resultMap.supportingItems,
|
||||
...resultMap.wipers,
|
||||
...clonedGlassParts,
|
||||
...nullSafeGlassParts,
|
||||
];
|
||||
|
||||
const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||
|
|
@ -146,13 +149,26 @@ export default {
|
|||
resultMap.supportingItems,
|
||||
false
|
||||
);
|
||||
|
||||
// Promo logic
|
||||
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
|
||||
|
||||
const { validatePromoResponse, revalidatePromoResponse } =
|
||||
await revalidatePromosAndValidateNewPromo(
|
||||
promoCodeFromQueryString,
|
||||
pricingResults,
|
||||
"quote"
|
||||
);
|
||||
// End of promo logic
|
||||
|
||||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.pricedGlassParts = clonedGlassParts;
|
||||
vm.pricedGlassParts = nullSafeGlassParts;
|
||||
vm.supportingItems = resultMap.supportingItems;
|
||||
vm.availableLineItems = pricingResults;
|
||||
vm.isInsuranceSelected = vm.getDefaultIsInsuranceSelectedValue(vm.availableLineItems);
|
||||
vm.newValidatedPromos = validatePromoResponse?.data.orderPromos;
|
||||
});
|
||||
},
|
||||
data() {
|
||||
|
|
@ -162,8 +178,18 @@ export default {
|
|||
availableLineItems: null,
|
||||
supportingItems: null,
|
||||
pricedGlassParts: null,
|
||||
newValidatedPromos: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allActivePromos() {
|
||||
const allActivePromos = [];
|
||||
if (this.newValidatedPromos) allActivePromos.push(...this.newValidatedPromos);
|
||||
if (this.$store.getters.order.lineItems.promos)
|
||||
allActivePromos.push(...this.$store.getters.order.lineItems.promos);
|
||||
return allActivePromos;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModalAction(modalName) {
|
||||
this.$refs[modalName].openModal();
|
||||
|
|
@ -239,6 +265,7 @@ export default {
|
|||
}
|
||||
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
|
||||
this.dispatchStoreAction(this.storeActions.SAVE_PROMOS, this.newValidatedPromos, false);
|
||||
|
||||
const payment = this.$store.getters.payment;
|
||||
if (payment.isInsurance) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ import {
|
|||
containsLineItemWithPartType,
|
||||
findLineItemsWithPartType,
|
||||
} from "@/helpers/service-package-helper";
|
||||
import {
|
||||
getPromosThatMatchLineItemsOnOrder,
|
||||
removeVapsPromosFromPromoArray,
|
||||
} from "@/helpers/promotions-helper";
|
||||
|
||||
export default {
|
||||
name: "servicePackageQuestion",
|
||||
|
|
@ -40,6 +44,7 @@ export default {
|
|||
isRequired: Boolean,
|
||||
isInsuranceSelected: Boolean,
|
||||
availableLineItems: null,
|
||||
activePromos: null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -86,8 +91,11 @@ export default {
|
|||
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
|
||||
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.SubWidgetName),
|
||||
buttonBodyCopy: this.getBodyTextFromCms(answer.SubWidgetName),
|
||||
buttonAuxillaryCopy: this.getPackagePriceString(answer.Name),
|
||||
buttonAuxillaryCopy: this.getDiscountedPackagePriceString(answer.Name),
|
||||
buttonFooterCopy: this.getFooterTextFromCms(answer.SubWidgetName),
|
||||
additionalButtonData: {
|
||||
strikeThroughPrice: this.getPackagePriceString(answer.Name),
|
||||
},
|
||||
}));
|
||||
return modifiedAnswers;
|
||||
},
|
||||
|
|
@ -161,21 +169,33 @@ export default {
|
|||
return this.processIfStatements(footerText, "custom", this.getCustomValueFromString);
|
||||
},
|
||||
getPackagePriceString(packageName) {
|
||||
const formattedPriceFloat = parseFloat(this.getPackagePrice(packageName)).toFixed(2);
|
||||
const formattedPriceFloat = parseFloat(
|
||||
this.getPackagePrice(packageName, { discountedPrice: false })
|
||||
).toFixed(2);
|
||||
return "$" + formattedPriceFloat;
|
||||
},
|
||||
getDiscountedPackagePriceString(packageName) {
|
||||
const formattedPriceFloat = parseFloat(
|
||||
this.getPackagePrice(packageName, { discountedPrice: true })
|
||||
).toFixed(2);
|
||||
return (this.isInsuranceSelected ? "As little as $" : "$") + formattedPriceFloat;
|
||||
},
|
||||
getPackagePrice(packageName) {
|
||||
getPackagePrice(packageName, { discountedPrice = false }) {
|
||||
const lineItemsToPrice = [...this.nullSafeAvailableLineItems];
|
||||
if (discountedPrice) {
|
||||
lineItemsToPrice.push(...removeVapsPromosFromPromoArray(this.activePromos));
|
||||
}
|
||||
let priceFloat = this.isInsuranceSelected
|
||||
? 0
|
||||
: baseMixin.methods.getTierOnePackagePrice(
|
||||
baseMixin.methods.filterOutFees(this.nullSafeAvailableLineItems)
|
||||
baseMixin.methods.filterOutFees(lineItemsToPrice)
|
||||
);
|
||||
|
||||
priceFloat += this.getVapsPrice(packageName);
|
||||
priceFloat += this.getVapsPrice(packageName, discountedPrice);
|
||||
|
||||
return priceFloat;
|
||||
},
|
||||
getVapsPrice(packageName) {
|
||||
getVapsPrice(packageName, applyPromoDiscounts = false) {
|
||||
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
|
||||
|
||||
let price = 0;
|
||||
|
|
@ -184,6 +204,16 @@ export default {
|
|||
price += baseMixin.methods.getTotalLineItemPrice(item);
|
||||
});
|
||||
|
||||
if (applyPromoDiscounts && this.activePromos) {
|
||||
const relevantPromos = getPromosThatMatchLineItemsOnOrder(
|
||||
this.activePromos,
|
||||
vapsItems
|
||||
);
|
||||
relevantPromos.forEach((promo) => {
|
||||
price += baseMixin.methods.getTotalLineItemPrice(promo);
|
||||
});
|
||||
}
|
||||
|
||||
return price;
|
||||
},
|
||||
selectDefaultPackage() {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import store from "@/store";
|
|||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
import { experimentTriggers } from "../constants/experiments";
|
||||
import { applicationConfig } from "../constants/application-config";
|
||||
import { shouldStripPromoQueryString } from "@/helpers/promotions-helper";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
|
@ -283,7 +284,7 @@ async function navigate(
|
|||
queryStringsObject[queryStrings.ZIP_CODE] = zip;
|
||||
}
|
||||
|
||||
if (promo) {
|
||||
if (promo && !shouldStripPromoQueryString(currentRoute.query.fmgPage)) {
|
||||
queryStringsObject[queryStrings.PROMO] = promo;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const getDefaultState = () => {
|
|||
supportingItems: null,
|
||||
vaps: null,
|
||||
serverData: null,
|
||||
promos: null,
|
||||
},
|
||||
payment: {
|
||||
isInsurance: null,
|
||||
|
|
@ -95,6 +96,7 @@ const getDefaultState = () => {
|
|||
parentAccountNumber: 0,
|
||||
payNowType: "cc",
|
||||
piaErrorCode: null,
|
||||
inactivePromos: null,
|
||||
},
|
||||
schedule: {
|
||||
date: null,
|
||||
|
|
@ -195,6 +197,12 @@ export const mutations = {
|
|||
updateLineItemsServerData(state, serverData) {
|
||||
state.order.lineItems.serverData = serverData;
|
||||
},
|
||||
updateInactivePromos(state, inactivePromos) {
|
||||
state.order.payment.inactivePromos = inactivePromos;
|
||||
},
|
||||
updatePromos(state, promos) {
|
||||
state.order.lineItems.promos = promos;
|
||||
},
|
||||
updatePageData(state, pageData) {
|
||||
state.applicationUser.pageData[pageData.page] = pageData.data;
|
||||
},
|
||||
|
|
@ -1235,10 +1243,10 @@ export const actions = {
|
|||
return response;
|
||||
},
|
||||
|
||||
getWipers(context, { pageNameToLog }) {
|
||||
async getWipers(context, { pageNameToLog }) {
|
||||
const carId = context.getters.vehicle.carId;
|
||||
const serviceZipCode = context.getters.order.serviceLocation.zipCode;
|
||||
return globalMethods
|
||||
const response = await globalMethods
|
||||
.callHttpClient({
|
||||
method: endpoints.GetWipers.method,
|
||||
endpoint: `${endpoints.GetWipers.url}/${carId}/${serviceZipCode}`,
|
||||
|
|
@ -1249,15 +1257,20 @@ export const actions = {
|
|||
// The wiper service sometimes returns 500s on legitimate carId/zipCode combination - return empty array instead of breaking flow
|
||||
return [];
|
||||
});
|
||||
|
||||
syncLineItemIds(response.data, context.getters.order.lineItems.vaps);
|
||||
return response;
|
||||
},
|
||||
|
||||
getRainDefense(context, { pageNameToLog }) {
|
||||
return globalMethods.callHttpClient({
|
||||
async getRainDefense(context, { pageNameToLog }) {
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.GetRainDefense.method,
|
||||
endpoint: endpoints.GetRainDefense.url,
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
});
|
||||
syncLineItemIds([response.data], context.getters.order.lineItems.vaps);
|
||||
return response;
|
||||
},
|
||||
|
||||
getMobileFeePart(context, { pageNameToLog }) {
|
||||
|
|
@ -1582,6 +1595,7 @@ export const actions = {
|
|||
supportingItems: lineItems.supportingItems,
|
||||
vaps: lineItems.vaps,
|
||||
serverData: lineItems.serverData,
|
||||
promos: lineItems.promos,
|
||||
},
|
||||
payment: {
|
||||
InsuranceCoverage: {
|
||||
|
|
@ -1589,6 +1603,7 @@ export const actions = {
|
|||
},
|
||||
isInsurance: order.payment.isInsurance,
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
inactivePromos: order.payment.inactivePromos,
|
||||
},
|
||||
serviceLocation: {
|
||||
streetAddress: order.serviceLocation.address,
|
||||
|
|
@ -2007,17 +2022,21 @@ export const actions = {
|
|||
if (!deepEqual(supportingItems, context.state.order.lineItems.supportingItems)) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
|
||||
addGuidToLineItemsIfNotAlreadyThere(supportingItems);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems);
|
||||
},
|
||||
|
||||
saveSupportingItemsSuppressingStateResetting(context, supportingItems) {
|
||||
addGuidToLineItemsIfNotAlreadyThere(supportingItems);
|
||||
context.commit(storeMutations.UPDATE_SUPPORTING_ITEMS, supportingItems);
|
||||
},
|
||||
|
||||
saveVaps(context, vaps) {
|
||||
addGuidToLineItemsIfNotAlreadyThere(vaps);
|
||||
context.commit(storeMutations.UPDATE_VAPS, vaps);
|
||||
},
|
||||
savePromos(context, promos) {
|
||||
context.commit(storeMutations.UPDATE_PROMOS, promos);
|
||||
},
|
||||
|
||||
// Price order actions
|
||||
async priceOrderItemsAndSaveServerData(
|
||||
|
|
@ -2139,6 +2158,95 @@ export const actions = {
|
|||
return taxedLineItems;
|
||||
},
|
||||
|
||||
async validateOrderPromoAndSaveServerData(
|
||||
context,
|
||||
{ payload: { promoCode, addableVaps }, pageNameToLog }
|
||||
) {
|
||||
const order = context.getters.order;
|
||||
addGuidToLineItemsIfNotAlreadyThere(addableVaps);
|
||||
const requestObject = {
|
||||
promoCode: promoCode,
|
||||
order: {
|
||||
appointmentType: order.serviceLocation.appointmentType,
|
||||
carId: order.vehicle.carId,
|
||||
correlationId: order.referralCorrelationId,
|
||||
eon: order.eon,
|
||||
isRepair: order.damage.isRepair,
|
||||
glassToReplace: renameGlassToReplaceAttributes(order.damage.glassToReplace),
|
||||
lineItemsOnOrder: getArrayOfAllLineItems(order.lineItems),
|
||||
addableVaps: addableVaps,
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
referralSequenceNumber: order.referralSequenceNumber,
|
||||
serviceState: order.serviceLocation.state,
|
||||
serverData: order.lineItems.serverData,
|
||||
vehicleYear: "" + order.vehicle.year,
|
||||
zipCodeOrProviderCtu:
|
||||
order.serviceLocation.appointmentType === "Inshop"
|
||||
? order.serviceLocation.provider.address.zipCodeCtu
|
||||
: order.serviceLocation.zipCodeCtu,
|
||||
},
|
||||
};
|
||||
|
||||
const validateResponse = await globalMethods.callHttpClient({
|
||||
method: endpoints.ValidatePromo.method,
|
||||
endpoint: endpoints.ValidatePromo.url,
|
||||
payload: requestObject,
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
});
|
||||
|
||||
context.commit(
|
||||
storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA,
|
||||
validateResponse.data.lineItemsServerData
|
||||
);
|
||||
|
||||
return validateResponse;
|
||||
},
|
||||
async revalidateOrderPromosAndUpdateStore(context, { pageNameToLog }) {
|
||||
const order = context.getters.order;
|
||||
|
||||
let requestObject = {
|
||||
inactivePromos: order.payment.inactivePromos,
|
||||
order: {
|
||||
appointmentType: order.serviceLocation.appointmentType,
|
||||
carId: order.vehicle.carId,
|
||||
correlationId: order.referralCorrelationId,
|
||||
eon: order.eon,
|
||||
isRepair: order.damage.isRepair,
|
||||
glassToReplace: renameGlassToReplaceAttributes(order.damage.glassToReplace),
|
||||
lineItemsOnOrder: getArrayOfAllLineItems(order.lineItems),
|
||||
parentAccountNumber: order.payment.parentAccountNumber,
|
||||
referralSequenceNumber: order.referralSequenceNumber,
|
||||
serviceState: order.serviceLocation.state,
|
||||
serverData: order.lineItems.serverData,
|
||||
vehicleYear: "" + order.vehicle.year,
|
||||
zipCodeOrProviderCtu:
|
||||
order.serviceLocation.appointmentType === "Inshop"
|
||||
? order.serviceLocation.provider.address.zipCodeCtu
|
||||
: order.serviceLocation.zipCodeCtu,
|
||||
},
|
||||
};
|
||||
|
||||
const revalidateResponse = await globalMethods.callHttpClient({
|
||||
method: endpoints.RevalidatePromos.method,
|
||||
endpoint: endpoints.RevalidatePromos.url,
|
||||
payload: requestObject,
|
||||
logApiCall: true,
|
||||
pageNameToLog: pageNameToLog,
|
||||
});
|
||||
|
||||
const revalidationErrorPromoCodes = revalidateResponse.data.errors.map((x) => x.promoCode);
|
||||
|
||||
context.commit(storeMutations.UPDATE_PROMOS, revalidateResponse.data.promoLineItems);
|
||||
context.commit(storeMutations.UPDATE_INACTIVE_PROMOS, revalidationErrorPromoCodes);
|
||||
context.commit(
|
||||
storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA,
|
||||
revalidateResponse.data.lineItemsServerData
|
||||
);
|
||||
|
||||
return revalidateResponse;
|
||||
},
|
||||
|
||||
// Misc order actions
|
||||
saveSchedule(context, scheduleInfo) {
|
||||
context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo);
|
||||
|
|
@ -2203,7 +2311,7 @@ export const actions = {
|
|||
if (!deepEqual(parts, context.state.order.lineItems.glassParts)) {
|
||||
context.dispatch(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
|
||||
addGuidToLineItemsIfNotAlreadyThere(parts);
|
||||
context.commit(storeMutations.UPDATE_GLASS_PARTS, parts);
|
||||
},
|
||||
|
||||
|
|
@ -2380,6 +2488,19 @@ function addTaxesToPricedLineItems(lineItems, taxingLineItems) {
|
|||
return lineItems;
|
||||
}
|
||||
|
||||
function getArrayOfAllLineItems(lineItems) {
|
||||
let consolidatedLineItemsArray = [];
|
||||
if (lineItems.glassParts != null)
|
||||
consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.glassParts];
|
||||
if (lineItems.supportingItems != null)
|
||||
consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.supportingItems];
|
||||
if (lineItems.vaps != null)
|
||||
consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.vaps];
|
||||
if (lineItems.promos != null)
|
||||
consolidatedLineItemsArray = [...consolidatedLineItemsArray, ...lineItems.promos];
|
||||
return consolidatedLineItemsArray;
|
||||
}
|
||||
|
||||
function getFlattenedArrayOfLineItemsWithChildParts(lineItems) {
|
||||
let flattenedArray = [];
|
||||
lineItems?.forEach((lineItem) => {
|
||||
|
|
@ -2414,6 +2535,37 @@ function convertGlassPieceToBackEndCompatibleFormat(glassPieces) {
|
|||
});
|
||||
}
|
||||
|
||||
function renameGlassToReplaceAttributes(glassToReplace) {
|
||||
let newGlassToReplace = [];
|
||||
if (glassToReplace) {
|
||||
glassToReplace.forEach((item) => {
|
||||
newGlassToReplace.push({ location: item.glassLocation, name: item.glassName });
|
||||
});
|
||||
}
|
||||
return newGlassToReplace;
|
||||
}
|
||||
|
||||
function addGuidToLineItemsIfNotAlreadyThere(lineItems) {
|
||||
lineItems.forEach((lineItem) => {
|
||||
if (!lineItem.id) {
|
||||
lineItem.id = crypto.randomUUID();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function syncLineItemIds(lineItemsWithoutIds, lineItemsWithIds) {
|
||||
if (!lineItemsWithIds || !lineItemsWithoutIds) {
|
||||
return;
|
||||
}
|
||||
lineItemsWithoutIds.forEach((noId) => {
|
||||
lineItemsWithIds.forEach((withId) => {
|
||||
if (noId.partNumber === withId.partNumber && noId.partType === withId.partType) {
|
||||
noId.id = withId.id;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function providersEqual(providerA, providerB) {
|
||||
return (
|
||||
providerA.providerNumber === providerB.providerNumber &&
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { AppointmentTypeStrings } from "@/constants/schedule-constants";
|
|||
// Mock global method
|
||||
globalMethods.callHttpClient = jest.fn();
|
||||
|
||||
global.crypto = { randomUUID: jest.fn() };
|
||||
|
||||
describe("Mutations", () => {
|
||||
it("Updates vehicle year in state", () => {
|
||||
// Arrange
|
||||
|
|
@ -1378,6 +1380,8 @@ describe("Actions", () => {
|
|||
state: state,
|
||||
};
|
||||
|
||||
context.state.order.lineItems = [];
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
|
|
@ -1385,10 +1389,10 @@ describe("Actions", () => {
|
|||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
actions.saveGlassParts(context, { glassParts: {} });
|
||||
actions.saveGlassParts(context, []);
|
||||
|
||||
// Assert
|
||||
expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, { glassParts: {} });
|
||||
expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, []);
|
||||
expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
});
|
||||
|
||||
|
|
@ -1417,7 +1421,7 @@ describe("Actions", () => {
|
|||
state: {
|
||||
order: {
|
||||
lineItems: {
|
||||
supportingItems: ["TestValue1", "TestValue2"],
|
||||
supportingItems: [{ partNum: "TestValue1" }, { partNum: "TestValue2" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1430,7 +1434,11 @@ describe("Actions", () => {
|
|||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
actions.saveSupportingItems(context, ["TestValue3", "TestValue4", "TestValue5"]);
|
||||
actions.saveSupportingItems(context, [
|
||||
{ partNum: "TestValue3" },
|
||||
{ partNum: "TestValue4" },
|
||||
{ partNum: "TestValue5" },
|
||||
]);
|
||||
|
||||
// Assert
|
||||
expect(dispatch).toBeCalledWith(storeActions.RESET_SERVICE_LOCATION_STATE_AND_DEPENDENCIES);
|
||||
|
|
@ -1442,7 +1450,7 @@ describe("Actions", () => {
|
|||
state: {
|
||||
order: {
|
||||
lineItems: {
|
||||
supportingItems: ["TestValue1", "TestValue2"],
|
||||
supportingItems: [{ partNum: "TestValue1" }, { partNum: "TestValue2" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1455,7 +1463,10 @@ describe("Actions", () => {
|
|||
context.dispatch = dispatch;
|
||||
|
||||
// Act
|
||||
actions.saveSupportingItems(context, ["TestValue1", "TestValue2"]);
|
||||
actions.saveSupportingItems(context, [
|
||||
{ partNum: "TestValue1" },
|
||||
{ partNum: "TestValue2" },
|
||||
]);
|
||||
|
||||
// Assert
|
||||
expect(dispatch).not.toBeCalledWith(
|
||||
|
|
|
|||
Loading…
Reference in a new issue